diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index e983474..18b074e 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -23,3 +23,4 @@ license, including: jQuery - Copyright (c) 2008 - 2009 John Resig + Symfony2 - Copyright (c) 2004 - 2011 Fabien Potencier diff --git a/includes/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php b/includes/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php new file mode 100644 index 0000000..278f510 --- /dev/null +++ b/includes/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ClassLoader; + +/** + * ApcUniversalClassLoader implements a "universal" autoloader cached in APC for PHP 5.3. + * + * It is able to load classes that use either: + * + * * The technical interoperability standards for PHP 5.3 namespaces and + * class names (http://groups.google.com/group/php-standards/web/psr-0-final-proposal); + * + * * The PEAR naming convention for classes (http://pear.php.net/). + * + * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be + * looked for in a list of locations to ease the vendoring of a sub-set of + * classes for large projects. + * + * Example usage: + * + * require 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php'; + * require 'vendor/symfony/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php'; + * + * use Symfony\Component\ClassLoader\ApcUniversalClassLoader; + * + * $loader = new ApcUniversalClassLoader('apc.prefix.'); + * + * // register classes with namespaces + * $loader->registerNamespaces(array( + * 'Symfony\Component' => __DIR__.'/component', + * 'Symfony' => __DIR__.'/framework', + * 'Sensio' => array(__DIR__.'/src', __DIR__.'/vendor'), + * )); + * + * // register a library using the PEAR naming convention + * $loader->registerPrefixes(array( + * 'Swift_' => __DIR__.'/Swift', + * )); + * + * // activate the autoloader + * $loader->register(); + * + * 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. + * + * @author Fabien Potencier + * @author Kris Wallsmith + * + * @api + */ +class ApcUniversalClassLoader extends UniversalClassLoader +{ + private $prefix; + + /** + * Constructor. + * + * @param string $prefix A prefix to create a namespace in APC + * + * @api + */ + public function __construct($prefix) + { + if (!extension_loaded('apc')) { + throw new \RuntimeException('Unable to use ApcUniversalClassLoader as APC is not enabled.'); + } + + $this->prefix = $prefix; + } + + /** + * Finds a file by class name while caching lookups to APC. + * + * @param string $class A class name to resolve to file + */ + public function findFile($class) + { + if (false === $file = apc_fetch($this->prefix.$class)) { + apc_store($this->prefix.$class, $file = parent::findFile($class)); + } + + return $file; + } +} diff --git a/includes/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/includes/Symfony/Component/ClassLoader/ClassCollectionLoader.php new file mode 100644 index 0000000..da777f2 --- /dev/null +++ b/includes/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ClassLoader; + +/** + * ClassCollectionLoader. + * + * @author Fabien Potencier + */ +class ClassCollectionLoader +{ + static private $loaded; + + /** + * Loads a list of classes and caches them in one big file. + * + * @param array $classes An array of classes to load + * @param string $cacheDir A cache directory + * @param string $name The cache name prefix + * @param Boolean $autoReload Whether to flush the cache when the cache is stale or not + * @param Boolean $adaptive Whether to remove already declared classes or not + * @param string $extension File extension of the resulting file + * + * @throws \InvalidArgumentException When class can't be loaded + */ + static public function load($classes, $cacheDir, $name, $autoReload, $adaptive = false, $extension = '.php') + { + // each $name can only be loaded once per PHP process + if (isset(self::$loaded[$name])) { + return; + } + + self::$loaded[$name] = true; + + if ($adaptive) { + // don't include already declared classes + $classes = array_diff($classes, get_declared_classes(), get_declared_interfaces()); + + // the cache is different depending on which classes are already declared + $name = $name.'-'.substr(md5(implode('|', $classes)), 0, 5); + } + + $cache = $cacheDir.'/'.$name.$extension; + + // auto-reload + $reload = false; + if ($autoReload) { + $metadata = $cacheDir.'/'.$name.$extension.'.meta'; + if (!file_exists($metadata) || !file_exists($cache)) { + $reload = true; + } else { + $time = filemtime($cache); + $meta = unserialize(file_get_contents($metadata)); + + if ($meta[1] != $classes) { + $reload = true; + } else { + foreach ($meta[0] as $resource) { + if (!file_exists($resource) || filemtime($resource) > $time) { + $reload = true; + + break; + } + } + } + } + } + + if (!$reload && file_exists($cache)) { + require_once $cache; + + return; + } + + $files = array(); + $content = ''; + foreach ($classes as $class) { + if (!class_exists($class) && !interface_exists($class) && (!function_exists('trait_exists') || !trait_exists($class))) { + throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class)); + } + + $r = new \ReflectionClass($class); + $files[] = $r->getFileName(); + + $c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($r->getFileName())); + + // add namespace declaration for global code + if (!$r->inNamespace()) { + $c = "\nnamespace\n{\n".self::stripComments($c)."\n}\n"; + } else { + $c = self::fixNamespaceDeclarations(' + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ClassLoader; + +/** + * Checks that the class is actually declared in the included file. + * + * @author Fabien Potencier + */ +class DebugUniversalClassLoader extends UniversalClassLoader +{ + /** + * Replaces all regular UniversalClassLoader instances by a DebugUniversalClassLoader ones. + */ + static public function enable() + { + if (!is_array($functions = spl_autoload_functions())) { + return; + } + + foreach ($functions as $function) { + spl_autoload_unregister($function); + } + + foreach ($functions as $function) { + if (is_array($function) && $function[0] instanceof UniversalClassLoader) { + $loader = new static(); + $loader->registerNamespaceFallbacks($function[0]->getNamespaceFallbacks()); + $loader->registerPrefixFallbacks($function[0]->getPrefixFallbacks()); + $loader->registerNamespaces($function[0]->getNamespaces()); + $loader->registerPrefixes($function[0]->getPrefixes()); + + $function[0] = $loader; + } + + spl_autoload_register($function); + } + } + + /** + * {@inheritDoc} + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + require $file; + + if (!class_exists($class, false) && !interface_exists($class, false) && (!function_exists('trait_exists') || !trait_exists($class, false))) { + throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); + } + } + } +} diff --git a/includes/Symfony/Component/ClassLoader/LICENSE b/includes/Symfony/Component/ClassLoader/LICENSE new file mode 100644 index 0000000..89df448 --- /dev/null +++ b/includes/Symfony/Component/ClassLoader/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2011 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/Symfony/Component/ClassLoader/MapClassLoader.php b/includes/Symfony/Component/ClassLoader/MapClassLoader.php new file mode 100644 index 0000000..cf17d42 --- /dev/null +++ b/includes/Symfony/Component/ClassLoader/MapClassLoader.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ClassLoader; + +/** + * A class loader that uses a mapping file to look up paths. + * + * @author Fabien Potencier + */ +class MapClassLoader +{ + private $map = array(); + + /** + * Constructor. + * + * @param array $map A map where keys are classes and values the absolute file path + */ + public function __construct(array $map) + { + $this->map = $map; + } + + /** + * Registers this instance as an autoloader. + * + * @param Boolean $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + */ + public function loadClass($class) + { + if ('\\' === $class[0]) { + $class = substr($class, 1); + } + + if (isset($this->map[$class])) { + require $this->map[$class]; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|null The path, if found + */ + public function findFile($class) + { + if ('\\' === $class[0]) { + $class = substr($class, 1); + } + + if (isset($this->map[$class])) { + return $this->map[$class]; + } + } +} diff --git a/includes/Symfony/Component/ClassLoader/UniversalClassLoader.php b/includes/Symfony/Component/ClassLoader/UniversalClassLoader.php new file mode 100644 index 0000000..d296b94 --- /dev/null +++ b/includes/Symfony/Component/ClassLoader/UniversalClassLoader.php @@ -0,0 +1,265 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ClassLoader; + +/** + * UniversalClassLoader implements a "universal" autoloader for PHP 5.3. + * + * It is able to load classes that use either: + * + * * The technical interoperability standards for PHP 5.3 namespaces and + * class names (http://groups.google.com/group/php-standards/web/psr-0-final-proposal); + * + * * The PEAR naming convention for classes (http://pear.php.net/). + * + * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be + * looked for in a list of locations to ease the vendoring of a sub-set of + * classes for large projects. + * + * Example usage: + * + * $loader = new UniversalClassLoader(); + * + * // register classes with namespaces + * $loader->registerNamespaces(array( + * 'Symfony\Component' => __DIR__.'/component', + * 'Symfony' => __DIR__.'/framework', + * 'Sensio' => array(__DIR__.'/src', __DIR__.'/vendor'), + * )); + * + * // register a library using the PEAR naming convention + * $loader->registerPrefixes(array( + * 'Swift_' => __DIR__.'/Swift', + * )); + * + * // activate the autoloader + * $loader->register(); + * + * 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. + * + * @author Fabien Potencier + * + * @api + */ +class UniversalClassLoader +{ + private $namespaces = array(); + private $prefixes = array(); + private $namespaceFallbacks = array(); + private $prefixFallbacks = array(); + + /** + * Gets the configured namespaces. + * + * @return array A hash with namespaces as keys and directories as values + */ + public function getNamespaces() + { + return $this->namespaces; + } + + /** + * Gets the configured class prefixes. + * + * @return array A hash with class prefixes as keys and directories as values + */ + public function getPrefixes() + { + return $this->prefixes; + } + + /** + * Gets the directory(ies) to use as a fallback for namespaces. + * + * @return array An array of directories + */ + public function getNamespaceFallbacks() + { + return $this->namespaceFallbacks; + } + + /** + * Gets the directory(ies) to use as a fallback for class prefixes. + * + * @return array An array of directories + */ + public function getPrefixFallbacks() + { + return $this->prefixFallbacks; + } + + /** + * Registers the directory to use as a fallback for namespaces. + * + * @param array $dirs An array of directories + * + * @api + */ + public function registerNamespaceFallbacks(array $dirs) + { + $this->namespaceFallbacks = $dirs; + } + + /** + * Registers the directory to use as a fallback for class prefixes. + * + * @param array $dirs An array of directories + * + * @api + */ + public function registerPrefixFallbacks(array $dirs) + { + $this->prefixFallbacks = $dirs; + } + + /** + * Registers an array of namespaces + * + * @param array $namespaces An array of namespaces (namespaces as keys and locations as values) + * + * @api + */ + public function registerNamespaces(array $namespaces) + { + foreach ($namespaces as $namespace => $locations) { + $this->namespaces[$namespace] = (array) $locations; + } + } + + /** + * Registers a namespace. + * + * @param string $namespace The namespace + * @param array|string $paths The location(s) of the namespace + * + * @api + */ + public function registerNamespace($namespace, $paths) + { + $this->namespaces[$namespace] = (array) $paths; + } + + /** + * Registers an array of classes using the PEAR naming convention. + * + * @param array $classes An array of classes (prefixes as keys and locations as values) + * + * @api + */ + public function registerPrefixes(array $classes) + { + foreach ($classes as $prefix => $locations) { + $this->prefixes[$prefix] = (array) $locations; + } + } + + /** + * Registers a set of classes using the PEAR naming convention. + * + * @param string $prefix The classes prefix + * @param array|string $paths The location(s) of the classes + * + * @api + */ + public function registerPrefix($prefix, $paths) + { + $this->prefixes[$prefix] = (array) $paths; + } + + /** + * Registers this instance as an autoloader. + * + * @param Boolean $prepend Whether to prepend the autoloader or not + * + * @api + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + require $file; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|null The path, if found + */ + public function findFile($class) + { + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $namespace = substr($class, 0, $pos); + foreach ($this->namespaces as $ns => $dirs) { + if (0 !== strpos($namespace, $ns)) { + continue; + } + + foreach ($dirs as $dir) { + $className = substr($class, $pos + 1); + $file = $dir.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $namespace).DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $className).'.php'; + if (file_exists($file)) { + return $file; + } + } + } + + foreach ($this->namespaceFallbacks as $dir) { + $file = $dir.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; + if (file_exists($file)) { + return $file; + } + } + } else { + // PEAR-like class name + foreach ($this->prefixes as $prefix => $dirs) { + if (0 !== strpos($class, $prefix)) { + continue; + } + + foreach ($dirs as $dir) { + $file = $dir.DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $class).'.php'; + if (file_exists($file)) { + return $file; + } + } + } + + foreach ($this->prefixFallbacks as $dir) { + $file = $dir.DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $class).'.php'; + if (file_exists($file)) { + return $file; + } + } + } + } +} diff --git a/includes/Symfony/Component/ClassLoader/composer.json b/includes/Symfony/Component/ClassLoader/composer.json new file mode 100644 index 0000000..35b573e --- /dev/null +++ b/includes/Symfony/Component/ClassLoader/composer.json @@ -0,0 +1,22 @@ +{ + "name": "symfony/class-loader", + "type": "library", + "description": "Symfony ClassLoader Component", + "keywords": [], + "homepage": "http://symfony.com", + "version": "2.0.4", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.2" + } +} diff --git a/includes/Symfony/Component/HttpFoundation/ApacheRequest.php b/includes/Symfony/Component/HttpFoundation/ApacheRequest.php new file mode 100644 index 0000000..2721581 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/ApacheRequest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Request represents an HTTP request from an Apache server. + * + * @author Fabien Potencier + */ +class ApacheRequest extends Request +{ + /** + * {@inheritdoc} + */ + protected function prepareRequestUri() + { + return $this->server->get('REQUEST_URI'); + } + + /** + * {@inheritdoc} + */ + protected function prepareBaseUrl() + { + $baseUrl = $this->server->get('SCRIPT_NAME'); + + if (false === strpos($this->server->get('REQUEST_URI'), $baseUrl)) { + // assume mod_rewrite + return rtrim(dirname($baseUrl), '/\\'); + } + + return $baseUrl; + } + + /** + * {@inheritdoc} + */ + protected function preparePathInfo() + { + return $this->server->get('PATH_INFO'); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/Cookie.php b/includes/Symfony/Component/HttpFoundation/Cookie.php new file mode 100644 index 0000000..8392812 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/Cookie.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Represents a cookie + * + * @author Johannes M. Schmitt + * + * @api + */ +class Cookie +{ + protected $name; + protected $value; + protected $domain; + protected $expire; + protected $path; + protected $secure; + protected $httpOnly; + + /** + * Constructor. + * + * @param string $name The name of the cookie + * @param string $value The value of the cookie + * @param integer|string|\DateTime $expire The time the cookie expires + * @param string $path The path on the server in which the cookie will be available on + * @param string $domain The domain that the cookie is available to + * @param Boolean $secure Whether the cookie should only be transmitted over a secure HTTPS connection from the client + * @param Boolean $httpOnly Whether the cookie will be made accessible only through the HTTP protocol + * + * @api + */ + public function __construct($name, $value = null, $expire = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true) + { + // from PHP source code + if (preg_match("/[=,; \t\r\n\013\014]/", $name)) { + throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); + } + + if (preg_match("/[,; \t\r\n\013\014]/", $value)) { + throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $value)); + } + + if (empty($name)) { + throw new \InvalidArgumentException('The cookie name cannot be empty.'); + } + + // convert expiration time to a Unix timestamp + if ($expire instanceof \DateTime) { + $expire = $expire->format('U'); + } elseif (!is_numeric($expire)) { + $expire = strtotime($expire); + + if (false === $expire || -1 === $expire) { + throw new \InvalidArgumentException('The cookie expiration time is not valid.'); + } + } + + $this->name = $name; + $this->value = $value; + $this->domain = $domain; + $this->expire = $expire; + $this->path = empty($path) ? '/' : $path; + $this->secure = (Boolean) $secure; + $this->httpOnly = (Boolean) $httpOnly; + } + + public function __toString() + { + $str = urlencode($this->getName()).'='; + + if ('' === (string) $this->getValue()) { + $str .= 'deleted; expires='.gmdate("D, d-M-Y H:i:s T", time() - 31536001); + } else { + $str .= urlencode($this->getValue()); + + if ($this->getExpiresTime() !== 0) { + $str .= '; expires='.gmdate("D, d-M-Y H:i:s T", $this->getExpiresTime()); + } + } + + if ('/' !== $this->path) { + $str .= '; path='.$this->path; + } + + if (null !== $this->getDomain()) { + $str .= '; domain='.$this->getDomain(); + } + + if (true === $this->isSecure()) { + $str .= '; secure'; + } + + if (true === $this->isHttpOnly()) { + $str .= '; httponly'; + } + + return $str; + } + + /** + * Gets the name of the cookie. + * + * @return string + * + * @api + */ + public function getName() + { + return $this->name; + } + + /** + * Gets the value of the cookie. + * + * @return string + * + * @api + */ + public function getValue() + { + return $this->value; + } + + /** + * Gets the domain that the cookie is available to. + * + * @return string + * + * @api + */ + public function getDomain() + { + return $this->domain; + } + + /** + * Gets the time the cookie expires. + * + * @return integer + * + * @api + */ + public function getExpiresTime() + { + return $this->expire; + } + + /** + * Gets the path on the server in which the cookie will be available on. + * + * @return string + * + * @api + */ + public function getPath() + { + return $this->path; + } + + /** + * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. + * + * @return Boolean + * + * @api + */ + public function isSecure() + { + return $this->secure; + } + + /** + * Checks whether the cookie will be made accessible only through the HTTP protocol. + * + * @return Boolean + * + * @api + */ + public function isHttpOnly() + { + return $this->httpOnly; + } + + /** + * Whether this cookie is about to be cleared + * + * @return Boolean + * + * @api + */ + public function isCleared() + { + return $this->expire < time(); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/Exception/AccessDeniedException.php b/includes/Symfony/Component/HttpFoundation/File/Exception/AccessDeniedException.php new file mode 100644 index 0000000..9c7fe68 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/Exception/AccessDeniedException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when the access on a file was denied. + * + * @author Bernhard Schussek + */ +class AccessDeniedException extends FileException +{ + /** + * Constructor. + * + * @param string $path The path to the accessed file + */ + public function __construct($path) + { + parent::__construct(sprintf('The file %s could not be accessed', $path)); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/Exception/FileException.php b/includes/Symfony/Component/HttpFoundation/File/Exception/FileException.php new file mode 100644 index 0000000..43c6cc8 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/Exception/FileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an error occurred in the component File + * + * @author Bernhard Schussek + */ +class FileException extends \RuntimeException +{ +} diff --git a/includes/Symfony/Component/HttpFoundation/File/Exception/FileNotFoundException.php b/includes/Symfony/Component/HttpFoundation/File/Exception/FileNotFoundException.php new file mode 100644 index 0000000..5b1aef8 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/Exception/FileNotFoundException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when a file was not found + * + * @author Bernhard Schussek + */ +class FileNotFoundException extends FileException +{ + /** + * Constructor. + * + * @param string $path The path to the file that was not found + */ + public function __construct($path) + { + parent::__construct(sprintf('The file "%s" does not exist', $path)); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/Exception/UnexpectedTypeException.php b/includes/Symfony/Component/HttpFoundation/File/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000..0444b87 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/Exception/UnexpectedTypeException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +class UnexpectedTypeException extends FileException +{ + public function __construct($value, $expectedType) + { + parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, is_object($value) ? get_class($value) : gettype($value))); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/Exception/UploadException.php b/includes/Symfony/Component/HttpFoundation/File/Exception/UploadException.php new file mode 100644 index 0000000..694e864 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/Exception/UploadException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an error occurred during file upload + * + * @author Bernhard Schussek + */ +class UploadException extends FileException +{ +} diff --git a/includes/Symfony/Component/HttpFoundation/File/File.php b/includes/Symfony/Component/HttpFoundation/File/File.php new file mode 100644 index 0000000..3a900fd --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/File.php @@ -0,0 +1,544 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; + +/** + * A file in the file system. + * + * @author Bernhard Schussek + * + * @api + */ +class File extends \SplFileInfo +{ + /** + * A map of mime types and their default extensions. + * + * @var array + */ + static protected $defaultExtensions = array( + 'application/andrew-inset' => 'ez', + 'application/appledouble' => 'base64', + 'application/applefile' => 'base64', + 'application/commonground' => 'dp', + 'application/cprplayer' => 'pqi', + 'application/dsptype' => 'tsp', + 'application/excel' => 'xls', + 'application/font-tdpfr' => 'pfr', + 'application/futuresplash' => 'spl', + 'application/hstu' => 'stk', + 'application/hyperstudio' => 'stk', + 'application/javascript' => 'js', + 'application/mac-binhex40' => 'hqx', + 'application/mac-compactpro' => 'cpt', + 'application/mbed' => 'mbd', + 'application/mirage' => 'mfp', + 'application/msword' => 'doc', + 'application/ocsp-request' => 'orq', + 'application/ocsp-response' => 'ors', + 'application/octet-stream' => 'bin', + 'application/oda' => 'oda', + 'application/ogg' => 'ogg', + 'application/pdf' => 'pdf', + 'application/x-pdf' => 'pdf', + 'application/pgp-encrypted' => '7bit', + 'application/pgp-keys' => '7bit', + 'application/pgp-signature' => 'sig', + 'application/pkcs10' => 'p10', + 'application/pkcs7-mime' => 'p7m', + 'application/pkcs7-signature' => 'p7s', + 'application/pkix-cert' => 'cer', + 'application/pkix-crl' => 'crl', + 'application/pkix-pkipath' => 'pkipath', + 'application/pkixcmp' => 'pki', + 'application/postscript' => 'ps', + 'application/presentations' => 'shw', + 'application/prs.cww' => 'cw', + 'application/prs.nprend' => 'rnd', + 'application/quest' => 'qrt', + 'application/rtf' => 'rtf', + 'application/sgml-open-catalog' => 'soc', + 'application/sieve' => 'siv', + 'application/smil' => 'smi', + 'application/toolbook' => 'tbk', + 'application/vnd.3gpp.pic-bw-large' => 'plb', + 'application/vnd.3gpp.pic-bw-small' => 'psb', + 'application/vnd.3gpp.pic-bw-var' => 'pvb', + 'application/vnd.3gpp.sms' => 'sms', + 'application/vnd.acucorp' => 'atc', + 'application/vnd.adobe.xfdf' => 'xfdf', + 'application/vnd.amiga.amu' => 'ami', + 'application/vnd.blueice.multipass' => 'mpm', + 'application/vnd.cinderella' => 'cdy', + 'application/vnd.cosmocaller' => 'cmc', + 'application/vnd.criticaltools.wbs+xml' => 'wbs', + 'application/vnd.curl' => 'curl', + 'application/vnd.data-vision.rdz' => 'rdz', + 'application/vnd.dreamfactory' => 'dfac', + 'application/vnd.fsc.weblaunch' => 'fsc', + 'application/vnd.genomatix.tuxedo' => 'txd', + 'application/vnd.hbci' => 'hbci', + 'application/vnd.hhe.lesson-player' => 'les', + 'application/vnd.hp-hpgl' => 'plt', + 'application/vnd.ibm.electronic-media' => 'emm', + 'application/vnd.ibm.rights-management' => 'irm', + 'application/vnd.ibm.secure-container' => 'sc', + 'application/vnd.ipunplugged.rcprofile' => 'rcprofile', + 'application/vnd.irepository.package+xml' => 'irp', + 'application/vnd.jisp' => 'jisp', + 'application/vnd.kde.karbon' => 'karbon', + 'application/vnd.kde.kchart' => 'chrt', + 'application/vnd.kde.kformula' => 'kfo', + 'application/vnd.kde.kivio' => 'flw', + 'application/vnd.kde.kontour' => 'kon', + 'application/vnd.kde.kpresenter' => 'kpr', + 'application/vnd.kde.kspread' => 'ksp', + 'application/vnd.kde.kword' => 'kwd', + 'application/vnd.kenameapp' => 'htke', + 'application/vnd.kidspiration' => 'kia', + 'application/vnd.kinar' => 'kne', + 'application/vnd.llamagraphics.life-balance.desktop' => 'lbd', + 'application/vnd.llamagraphics.life-balance.exchange+xml' => 'lbe', + 'application/vnd.lotus-1-2-3' => 'wks', + 'application/vnd.mcd' => 'mcd', + 'application/vnd.mfmp' => 'mfm', + 'application/vnd.micrografx.flo' => 'flo', + 'application/vnd.micrografx.igx' => 'igx', + 'application/vnd.mif' => 'mif', + 'application/vnd.mophun.application' => 'mpn', + 'application/vnd.mophun.certificate' => 'mpc', + 'application/vnd.mozilla.xul+xml' => 'xul', + 'application/vnd.ms-artgalry' => 'cil', + 'application/vnd.ms-asf' => 'asf', + 'application/vnd.ms-excel' => 'xls', + 'application/vnd.ms-excel.sheet.macroenabled.12' => 'xlsm', + 'application/vnd.ms-lrm' => 'lrm', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/vnd.ms-project' => 'mpp', + 'application/vnd.ms-tnef' => 'base64', + 'application/vnd.ms-works' => 'base64', + 'application/vnd.ms-wpl' => 'wpl', + 'application/vnd.mseq' => 'mseq', + 'application/vnd.nervana' => 'ent', + 'application/vnd.nokia.radio-preset' => 'rpst', + 'application/vnd.nokia.radio-presets' => 'rpss', + 'application/vnd.oasis.opendocument.text' => 'odt', + 'application/vnd.oasis.opendocument.text-template' => 'ott', + 'application/vnd.oasis.opendocument.text-web' => 'oth', + 'application/vnd.oasis.opendocument.text-master' => 'odm', + 'application/vnd.oasis.opendocument.graphics' => 'odg', + 'application/vnd.oasis.opendocument.graphics-template' => 'otg', + 'application/vnd.oasis.opendocument.presentation' => 'odp', + 'application/vnd.oasis.opendocument.presentation-template' => 'otp', + 'application/vnd.oasis.opendocument.spreadsheet' => 'ods', + 'application/vnd.oasis.opendocument.spreadsheet-template' => 'ots', + 'application/vnd.oasis.opendocument.chart' => 'odc', + 'application/vnd.oasis.opendocument.formula' => 'odf', + 'application/vnd.oasis.opendocument.database' => 'odb', + 'application/vnd.oasis.opendocument.image' => 'odi', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/vnd.palm' => 'prc', + 'application/vnd.picsel' => 'efif', + 'application/vnd.pvi.ptid1' => 'pti', + 'application/vnd.quark.quarkxpress' => 'qxd', + 'application/vnd.sealed.doc' => 'sdoc', + 'application/vnd.sealed.eml' => 'seml', + 'application/vnd.sealed.mht' => 'smht', + 'application/vnd.sealed.ppt' => 'sppt', + 'application/vnd.sealed.xls' => 'sxls', + 'application/vnd.sealedmedia.softseal.html' => 'stml', + 'application/vnd.sealedmedia.softseal.pdf' => 'spdf', + 'application/vnd.seemail' => 'see', + 'application/vnd.smaf' => 'mmf', + 'application/vnd.sun.xml.calc' => 'sxc', + 'application/vnd.sun.xml.calc.template' => 'stc', + 'application/vnd.sun.xml.draw' => 'sxd', + 'application/vnd.sun.xml.draw.template' => 'std', + 'application/vnd.sun.xml.impress' => 'sxi', + 'application/vnd.sun.xml.impress.template' => 'sti', + 'application/vnd.sun.xml.math' => 'sxm', + 'application/vnd.sun.xml.writer' => 'sxw', + 'application/vnd.sun.xml.writer.global' => 'sxg', + 'application/vnd.sun.xml.writer.template' => 'stw', + 'application/vnd.sus-calendar' => 'sus', + 'application/vnd.vidsoft.vidconference' => 'vsc', + 'application/vnd.visio' => 'vsd', + 'application/vnd.visionary' => 'vis', + 'application/vnd.wap.sic' => 'sic', + 'application/vnd.wap.slc' => 'slc', + 'application/vnd.wap.wbxml' => 'wbxml', + 'application/vnd.wap.wmlc' => 'wmlc', + 'application/vnd.wap.wmlscriptc' => 'wmlsc', + 'application/vnd.webturbo' => 'wtb', + 'application/vnd.wordperfect' => 'wpd', + 'application/vnd.wqd' => 'wqd', + 'application/vnd.wv.csp+wbxml' => 'wv', + 'application/vnd.wv.csp+xml' => '8bit', + 'application/vnd.wv.ssp+xml' => '8bit', + 'application/vnd.yamaha.hv-dic' => 'hvd', + 'application/vnd.yamaha.hv-script' => 'hvs', + 'application/vnd.yamaha.hv-voice' => 'hvp', + 'application/vnd.yamaha.smaf-audio' => 'saf', + 'application/vnd.yamaha.smaf-phrase' => 'spf', + 'application/vocaltec-media-desc' => 'vmd', + 'application/vocaltec-media-file' => 'vmf', + 'application/vocaltec-talker' => 'vtk', + 'application/watcherinfo+xml' => 'wif', + 'application/wordperfect5.1' => 'wp5', + 'application/x-123' => 'wk', + 'application/x-7th_level_event' => '7ls', + 'application/x-authorware-bin' => 'aab', + 'application/x-authorware-map' => 'aam', + 'application/x-authorware-seg' => 'aas', + 'application/x-bcpio' => 'bcpio', + 'application/x-bleeper' => 'bleep', + 'application/x-bzip2' => 'bz2', + 'application/x-cdlink' => 'vcd', + 'application/x-chat' => 'chat', + 'application/x-chess-pgn' => 'pgn', + 'application/x-compress' => 'z', + 'application/x-cpio' => 'cpio', + 'application/x-cprplayer' => 'pqf', + 'application/x-csh' => 'csh', + 'application/x-cu-seeme' => 'csm', + 'application/x-cult3d-object' => 'co', + 'application/x-debian-package' => 'deb', + 'application/x-director' => 'dcr', + 'application/x-dvi' => 'dvi', + 'application/x-envoy' => 'evy', + 'application/x-futuresplash' => 'spl', + 'application/x-gtar' => 'gtar', + 'application/x-gzip' => 'gz', + 'application/x-hdf' => 'hdf', + 'application/x-hep' => 'hep', + 'application/x-html+ruby' => 'rhtml', + 'application/x-httpd-miva' => 'mv', + 'application/x-httpd-php' => 'phtml', + 'application/x-ica' => 'ica', + 'application/x-imagemap' => 'imagemap', + 'application/x-ipix' => 'ipx', + 'application/x-ipscript' => 'ips', + 'application/x-java-archive' => 'jar', + 'application/x-java-jnlp-file' => 'jnlp', + 'application/x-java-serialized-object' => 'ser', + 'application/x-java-vm' => 'class', + 'application/x-javascript' => 'js', + 'application/x-koan' => 'skp', + 'application/x-latex' => 'latex', + 'application/x-mac-compactpro' => 'cpt', + 'application/x-maker' => 'frm', + 'application/x-mathcad' => 'mcd', + 'application/x-midi' => 'mid', + 'application/x-mif' => 'mif', + 'application/x-msaccess' => 'mda', + 'application/x-msdos-program' => 'com', + 'application/x-msdownload' => 'base64', + 'application/x-msexcel' => 'xls', + 'application/x-msword' => 'doc', + 'application/x-netcdf' => 'nc', + 'application/x-ns-proxy-autoconfig' => 'pac', + 'application/x-pagemaker' => 'pm5', + 'application/x-perl' => 'pl', + 'application/x-pn-realmedia' => 'rp', + 'application/x-python' => 'py', + 'application/x-quicktimeplayer' => 'qtl', + 'application/x-rar-compressed' => 'rar', + 'application/x-ruby' => 'rb', + 'application/x-sh' => 'sh', + 'application/x-shar' => 'shar', + 'application/x-shockwave-flash' => 'swf', + 'application/x-sprite' => 'spr', + 'application/x-spss' => 'sav', + 'application/x-spt' => 'spt', + 'application/x-stuffit' => 'sit', + 'application/x-sv4cpio' => 'sv4cpio', + 'application/x-sv4crc' => 'sv4crc', + 'application/x-tar' => 'tar', + 'application/x-tcl' => 'tcl', + 'application/x-tex' => 'tex', + 'application/x-texinfo' => 'texinfo', + 'application/x-troff' => 't', + 'application/x-troff-man' => 'man', + 'application/x-troff-me' => 'me', + 'application/x-troff-ms' => 'ms', + 'application/x-twinvq' => 'vqf', + 'application/x-twinvq-plugin' => 'vqe', + 'application/x-ustar' => 'ustar', + 'application/x-vmsbackup' => 'bck', + 'application/x-wais-source' => 'src', + 'application/x-wingz' => 'wz', + 'application/x-word' => 'base64', + 'application/x-wordperfect6.1' => 'wp6', + 'application/x-x509-ca-cert' => 'crt', + 'application/x-zip-compressed' => 'zip', + 'application/xhtml+xml' => 'xhtml', + 'application/zip' => 'zip', + 'audio/3gpp' => '3gpp', + 'audio/amr' => 'amr', + 'audio/amr-wb' => 'awb', + 'audio/basic' => 'au', + 'audio/evrc' => 'evc', + 'audio/l16' => 'l16', + 'audio/midi' => 'mid', + 'audio/mpeg' => 'mp3', + 'audio/prs.sid' => 'sid', + 'audio/qcelp' => 'qcp', + 'audio/smv' => 'smv', + 'audio/vnd.audiokoz' => 'koz', + 'audio/vnd.digital-winds' => 'eol', + 'audio/vnd.everad.plj' => 'plj', + 'audio/vnd.lucent.voice' => 'lvp', + 'audio/vnd.nokia.mobile-xmf' => 'mxmf', + 'audio/vnd.nortel.vbk' => 'vbk', + 'audio/vnd.nuera.ecelp4800' => 'ecelp4800', + 'audio/vnd.nuera.ecelp7470' => 'ecelp7470', + 'audio/vnd.nuera.ecelp9600' => 'ecelp9600', + 'audio/vnd.sealedmedia.softseal.mpeg' => 'smp3', + 'audio/voxware' => 'vox', + 'audio/x-aiff' => 'aif', + 'audio/x-mid' => 'mid', + 'audio/x-midi' => 'mid', + 'audio/x-mpeg' => 'mp2', + 'audio/x-mpegurl' => 'mpu', + 'audio/x-pn-realaudio' => 'rm', + 'audio/x-pn-realaudio-plugin' => 'rpm', + 'audio/x-realaudio' => 'ra', + 'audio/x-wav' => 'wav', + 'chemical/x-csml' => 'csm', + 'chemical/x-embl-dl-nucleotide' => 'emb', + 'chemical/x-gaussian-cube' => 'cube', + 'chemical/x-gaussian-input' => 'gau', + 'chemical/x-jcamp-dx' => 'jdx', + 'chemical/x-mdl-molfile' => 'mol', + 'chemical/x-mdl-rxnfile' => 'rxn', + 'chemical/x-mdl-tgf' => 'tgf', + 'chemical/x-mopac-input' => 'mop', + 'chemical/x-pdb' => 'pdb', + 'chemical/x-rasmol' => 'scr', + 'chemical/x-xyz' => 'xyz', + 'drawing/dwf' => 'dwf', + 'drawing/x-dwf' => 'dwf', + 'i-world/i-vrml' => 'ivr', + 'image/bmp' => 'bmp', + 'image/cewavelet' => 'wif', + 'image/cis-cod' => 'cod', + 'image/fif' => 'fif', + 'image/gif' => 'gif', + 'image/ief' => 'ief', + 'image/jp2' => 'jp2', + 'image/jpeg' => 'jpg', + 'image/jpm' => 'jpm', + 'image/jpx' => 'jpf', + 'image/pict' => 'pic', + 'image/pjpeg' => 'jpg', + 'image/png' => 'png', + 'image/targa' => 'tga', + 'image/tiff' => 'tif', + 'image/vn-svf' => 'svf', + 'image/vnd.dgn' => 'dgn', + 'image/vnd.djvu' => 'djvu', + 'image/vnd.dwg' => 'dwg', + 'image/vnd.glocalgraphics.pgb' => 'pgb', + 'image/vnd.microsoft.icon' => 'ico', + 'image/vnd.ms-modi' => 'mdi', + 'image/vnd.sealed.png' => 'spng', + 'image/vnd.sealedmedia.softseal.gif' => 'sgif', + 'image/vnd.sealedmedia.softseal.jpg' => 'sjpg', + 'image/vnd.wap.wbmp' => 'wbmp', + 'image/x-bmp' => 'bmp', + 'image/x-cmu-raster' => 'ras', + 'image/x-freehand' => 'fh4', + 'image/x-png' => 'png', + 'image/x-portable-anymap' => 'pnm', + 'image/x-portable-bitmap' => 'pbm', + 'image/x-portable-graymap' => 'pgm', + 'image/x-portable-pixmap' => 'ppm', + 'image/x-rgb' => 'rgb', + 'image/x-xbitmap' => 'xbm', + 'image/x-xpixmap' => 'xpm', + 'image/x-xwindowdump' => 'xwd', + 'message/external-body' => '8bit', + 'message/news' => '8bit', + 'message/partial' => '8bit', + 'message/rfc822' => '8bit', + 'model/iges' => 'igs', + 'model/mesh' => 'msh', + 'model/vnd.parasolid.transmit.binary' => 'x_b', + 'model/vnd.parasolid.transmit.text' => 'x_t', + 'model/vrml' => 'wrl', + 'multipart/alternative' => '8bit', + 'multipart/appledouble' => '8bit', + 'multipart/digest' => '8bit', + 'multipart/mixed' => '8bit', + 'multipart/parallel' => '8bit', + 'text/comma-separated-values' => 'csv', + 'text/css' => 'css', + 'text/html' => 'html', + 'text/plain' => 'txt', + 'text/prs.fallenstein.rst' => 'rst', + 'text/richtext' => 'rtx', + 'text/rtf' => 'rtf', + 'text/sgml' => 'sgml', + 'text/tab-separated-values' => 'tsv', + 'text/vnd.net2phone.commcenter.command' => 'ccc', + 'text/vnd.sun.j2me.app-descriptor' => 'jad', + 'text/vnd.wap.si' => 'si', + 'text/vnd.wap.sl' => 'sl', + 'text/vnd.wap.wml' => 'wml', + 'text/vnd.wap.wmlscript' => 'wmls', + 'text/x-hdml' => 'hdml', + 'text/x-setext' => 'etx', + 'text/x-sgml' => 'sgml', + 'text/x-speech' => 'talk', + 'text/x-vcalendar' => 'vcs', + 'text/x-vcard' => 'vcf', + 'text/xml' => 'xml', + 'ulead/vrml' => 'uvr', + 'video/3gpp' => '3gp', + 'video/dl' => 'dl', + 'video/gl' => 'gl', + 'video/mj2' => 'mj2', + 'video/mpeg' => 'mpeg', + 'video/quicktime' => 'mov', + 'video/vdo' => 'vdo', + 'video/vivo' => 'viv', + 'video/vnd.fvt' => 'fvt', + 'video/vnd.mpegurl' => 'mxu', + 'video/vnd.nokia.interleaved-multimedia' => 'nim', + 'video/vnd.objectvideo' => 'mp4', + 'video/vnd.sealed.mpeg1' => 's11', + 'video/vnd.sealed.mpeg4' => 'smpg', + 'video/vnd.sealed.swf' => 'sswf', + 'video/vnd.sealedmedia.softseal.mov' => 'smov', + 'video/vnd.vivo' => 'vivo', + 'video/x-fli' => 'fli', + 'video/x-ms-asf' => 'asf', + 'video/x-ms-wmv' => 'wmv', + 'video/x-msvideo' => 'avi', + 'video/x-sgi-movie' => 'movie', + 'x-chemical/x-pdb' => 'pdb', + 'x-chemical/x-xyz' => 'xyz', + 'x-conference/x-cooltalk' => 'ice', + 'x-drawing/dwf' => 'dwf', + 'x-world/x-d96' => 'd', + 'x-world/x-svr' => 'svr', + 'x-world/x-vream' => 'vrw', + 'x-world/x-vrml' => 'wrl', + ); + + /** + * Constructs a new file from the given path. + * + * @param string $path The path to the file + * + * @throws FileNotFoundException If the given path is not a file + * + * @api + */ + public function __construct($path) + { + if (!is_file($path)) { + throw new FileNotFoundException($path); + } + + parent::__construct($path); + } + + /** + * Returns the extension based on the mime type. + * + * If the mime type is unknown, returns null. + * + * @return string|null The guessed extension or null if it cannot be guessed + * + * @api + */ + public function guessExtension() + { + $type = $this->getMimeType(); + + return isset(static::$defaultExtensions[$type]) ? static::$defaultExtensions[$type] : null; + } + + /** + * Returns the mime type of the file. + * + * The mime type is guessed using the functions finfo(), mime_content_type() + * and the system binary "file" (in this order), depending on which of those + * is available on the current operating system. + * + * @return string|null The guessed mime type (i.e. "application/pdf") + * + * @api + */ + public function getMimeType() + { + $guesser = MimeTypeGuesser::getInstance(); + + return $guesser->guess($this->getPathname()); + } + + /** + * Returns the extension of the file. + * + * \SplFileInfo::getExtension() is not available before PHP 5.3.6 + * + * @return string The extension + * + * @api + */ + public function getExtension() + { + return pathinfo($this->getBasename(), PATHINFO_EXTENSION); + } + + /** + * Moves the file to a new location. + * + * @param string $directory The destination folder + * @param string $name The new file name + * + * @return File A File object representing the new file + * + * @throws FileException if the target file could not be created + * + * @api + */ + public function move($directory, $name = null) + { + if (!is_dir($directory)) { + if (false === @mkdir($directory, 0777, true)) { + throw new FileException(sprintf('Unable to create the "%s" directory', $directory)); + } + } elseif (!is_writable($directory)) { + throw new FileException(sprintf('Unable to write in the "%s" directory', $directory)); + } + + $target = $directory.DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : basename($name)); + + if (!@rename($this->getPathname(), $target)) { + $error = error_get_last(); + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message']))); + } + + chmod($target, 0666); + + return new File($target); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/MimeType/ContentTypeMimeTypeGuesser.php b/includes/Symfony/Component/HttpFoundation/File/MimeType/ContentTypeMimeTypeGuesser.php new file mode 100644 index 0000000..fb900b2 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/MimeType/ContentTypeMimeTypeGuesser.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\MimeType; + +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; + +/** + * Guesses the mime type using the PHP function mime_content_type(). + * + * @author Bernhard Schussek + */ +class ContentTypeMimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * Returns whether this guesser is supported on the current OS/PHP setup + * + * @return Boolean + */ + static public function isSupported() + { + return function_exists('mime_content_type'); + } + + /** + * Guesses the mime type of the file with the given path + * + * @see MimeTypeGuesserInterface::guess() + */ + public function guess($path) + { + if (!is_file($path)) { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) { + throw new AccessDeniedException($path); + } + + if (!self::isSupported()) { + return null; + } + + $type = mime_content_type($path); + + // remove charset (added as of PHP 5.3) + if (false !== $pos = strpos($type, ';')) { + $type = substr($type, 0, $pos); + } + + return $type; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php b/includes/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php new file mode 100644 index 0000000..1b869f2 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\MimeType; + +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; + +/** + * Guesses the mime type with the binary "file" (only available on *nix) + * + * @author Bernhard Schussek + */ +class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * Returns whether this guesser is supported on the current OS + * + * @return Boolean + */ + static public function isSupported() + { + return !strstr(PHP_OS, 'WIN'); + } + /** + * Guesses the mime type of the file with the given path + * + * @see MimeTypeGuesserInterface::guess() + */ + public function guess($path) + { + if (!is_file($path)) { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) { + throw new AccessDeniedException($path); + } + + if (!self::isSupported()) { + return null; + } + + ob_start(); + + // need to use --mime instead of -i. see #6641 + passthru(sprintf('file -b --mime %s 2>/dev/null', escapeshellarg($path)), $return); + if ($return > 0) { + ob_end_clean(); + + return null; + } + + $type = trim(ob_get_clean()); + + if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-]+)#i', $type, $match)) { + // it's not a type, but an error message + return null; + } + + return $match[1]; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/MimeType/FileinfoMimeTypeGuesser.php b/includes/Symfony/Component/HttpFoundation/File/MimeType/FileinfoMimeTypeGuesser.php new file mode 100644 index 0000000..45d5a08 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/MimeType/FileinfoMimeTypeGuesser.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\MimeType; + +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; + +/** + * Guesses the mime type using the PECL extension FileInfo + * + * @author Bernhard Schussek + */ +class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * Returns whether this guesser is supported on the current OS/PHP setup + * + * @return Boolean + */ + static public function isSupported() + { + return function_exists('finfo_open'); + } + + /** + * Guesses the mime type of the file with the given path + * + * @see MimeTypeGuesserInterface::guess() + */ + public function guess($path) + { + if (!is_file($path)) { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) { + throw new AccessDeniedException($path); + } + + if (!self::isSupported()) { + return null; + } + + if (!$finfo = new \finfo(FILEINFO_MIME_TYPE)) { + return null; + } + + return $finfo->file($path); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php b/includes/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php new file mode 100644 index 0000000..23dd463 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\MimeType; + +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; + +/** + * A singleton mime type guesser. + * + * By default, all mime type guessers provided by the framework are installed + * (if available on the current OS/PHP setup). You can register custom + * guessers by calling the register() method on the singleton instance. + * + * + * $guesser = MimeTypeGuesser::getInstance(); + * $guesser->register(new MyCustomMimeTypeGuesser()); + * + * + * The last registered guesser is preferred over previously registered ones. + * + * @author Bernhard Schussek + */ +class MimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * The singleton instance + * @var MimeTypeGuesser + */ + static private $instance = null; + + /** + * All registered MimeTypeGuesserInterface instances + * @var array + */ + protected $guessers = array(); + + /** + * Returns the singleton instance + * + * @return MimeTypeGuesser + */ + static public function getInstance() + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Registers all natively provided mime type guessers + */ + private function __construct() + { + if (FileBinaryMimeTypeGuesser::isSupported()) { + $this->register(new FileBinaryMimeTypeGuesser()); + } + + if (ContentTypeMimeTypeGuesser::isSupported()) { + $this->register(new ContentTypeMimeTypeGuesser()); + } + + if (FileinfoMimeTypeGuesser::isSupported()) { + $this->register(new FileinfoMimeTypeGuesser()); + } + } + + /** + * Registers a new mime type guesser + * + * When guessing, this guesser is preferred over previously registered ones. + * + * @param MimeTypeGuesserInterface $guesser + */ + public function register(MimeTypeGuesserInterface $guesser) + { + array_unshift($this->guessers, $guesser); + } + + /** + * Tries to guess the mime type of the given file + * + * The file is passed to each registered mime type guesser in reverse order + * of their registration (last registered is queried first). Once a guesser + * returns a value that is not NULL, this method terminates and returns the + * value. + * + * @param string $path The path to the file + * @return string The mime type or NULL, if none could be guessed + * @throws FileException If the file does not exist + */ + public function guess($path) + { + if (!is_file($path)) { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) { + throw new AccessDeniedException($path); + } + + $mimeType = null; + + foreach ($this->guessers as $guesser) { + $mimeType = $guesser->guess($path); + + if (null !== $mimeType) { + break; + } + } + + return $mimeType; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesserInterface.php b/includes/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesserInterface.php new file mode 100644 index 0000000..c111583 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesserInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\MimeType; + +/** + * Guesses the mime type of a file + * + * @author Bernhard Schussek + */ +interface MimeTypeGuesserInterface +{ + /** + * Guesses the mime type of the file with the given path + * + * @param string $path The path to the file + * @return string The mime type or NULL, if none could be guessed + * @throws FileNotFoundException If the file does not exist + * @throws AccessDeniedException If the file could not be read + */ + function guess($path); +} diff --git a/includes/Symfony/Component/HttpFoundation/File/UploadedFile.php b/includes/Symfony/Component/HttpFoundation/File/UploadedFile.php new file mode 100644 index 0000000..936ed70 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/File/UploadedFile.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; + +/** + * A file uploaded through a form. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * @author Fabien Potencier + * + * @api + */ +class UploadedFile extends File +{ + /** + * Whether the test mode is activated. + * + * Local files are used in test mode hence the code should not enforce HTTP uploads. + * + * @var Boolean + */ + private $test = false; + + /** + * The original name of the uploaded file. + * + * @var string + */ + private $originalName; + + /** + * The mime type provided by the uploader. + * + * @var string + */ + private $mimeType; + + /** + * The file size provided by the uploader. + * + * @var string + */ + private $size; + + /** + * The UPLOAD_ERR_XXX constant provided by the uploader. + * + * @var integer + */ + private $error; + + /** + * Accepts the information of the uploaded file as provided by the PHP global $_FILES. + * + * The file object is only created when the uploaded file is valid (i.e. when the + * isValid() method returns true). Otherwise the only methods that could be called + * on an UploadedFile instance are: + * + * * getClientOriginalName, + * * getClientMimeType, + * * isValid, + * * getError. + * + * Calling any other method on an non-valid instance will cause an unpredictable result. + * + * @param string $path The full temporary path to the file + * @param string $originalName The original file name + * @param string $mimeType The type of the file as provided by PHP + * @param integer $size The file size + * @param integer $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants) + * @param Boolean $test Whether the test mode is active + * + * @throws FileException If file_uploads is disabled + * @throws FileNotFoundException If the file does not exist + * + * @api + */ + public function __construct($path, $originalName, $mimeType = null, $size = null, $error = null, $test = false) + { + if (!ini_get('file_uploads')) { + throw new FileException(sprintf('Unable to create UploadedFile because "file_uploads" is disabled in your php.ini file (%s)', get_cfg_var('cfg_file_path'))); + } + + $this->originalName = basename($originalName); + $this->mimeType = $mimeType ?: 'application/octet-stream'; + $this->size = $size; + $this->error = $error ?: UPLOAD_ERR_OK; + $this->test = (Boolean) $test; + + if (UPLOAD_ERR_OK === $this->error) { + parent::__construct($path); + } + } + + /** + * Returns the original file name. + * + * It is extracted from the request from which the file has been uploaded. + * Then is should not be considered as a safe value. + * + * @return string|null The original name + * + * @api + */ + public function getClientOriginalName() + { + return $this->originalName; + } + + /** + * Returns the file mime type. + * + * It is extracted from the request from which the file has been uploaded. + * Then is should not be considered as a safe value. + * + * @return string|null The mime type + * + * @api + */ + public function getClientMimeType() + { + return $this->mimeType; + } + + /** + * Returns the file size. + * + * It is extracted from the request from which the file has been uploaded. + * Then is should not be considered as a safe value. + * + * @return integer|null The file size + * + * @api + */ + public function getClientSize() + { + return $this->size; + } + + /** + * Returns the upload error. + * + * If the upload was successful, the constant UPLOAD_ERR_OK is returned. + * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. + * + * @return integer The upload error + * + * @api + */ + public function getError() + { + return $this->error; + } + + /** + * Returns whether the file was uploaded successfully. + * + * @return Boolean True if no error occurred during uploading + * + * @api + */ + public function isValid() + { + return $this->error === UPLOAD_ERR_OK; + } + + /** + * Moves the file to a new location. + * + * @param string $directory The destination folder + * @param string $name The new file name + * + * @return File A File object representing the new file + * + * @throws FileException if the file has not been uploaded via Http + * + * @api + */ + public function move($directory, $name = null) + { + if ($this->isValid() && ($this->test || is_uploaded_file($this->getPathname()))) { + return parent::move($directory, $name); + } + + throw new FileException(sprintf('The file "%s" has not been uploaded via Http', $this->getPathname())); + } + + /** + * Returns the maximum size of an uploaded file as configured in php.ini + * + * @return type The maximum size of an uploaded file in bytes + */ + static public function getMaxFilesize() + { + $max = trim(ini_get('upload_max_filesize')); + + if ('' === $max) { + return PHP_INT_MAX; + } + + switch (strtolower(substr($max, -1))) { + case 'g': + $max *= 1024; + case 'm': + $max *= 1024; + case 'k': + $max *= 1024; + } + + return (integer) $max; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/FileBag.php b/includes/Symfony/Component/HttpFoundation/FileBag.php new file mode 100644 index 0000000..602cff2 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/FileBag.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\File\UploadedFile; + +/** + * FileBag is a container for HTTP headers. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + * + * @api + */ +class FileBag extends ParameterBag +{ + static private $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type'); + + /** + * Constructor. + * + * @param array $parameters An array of HTTP files + * + * @api + */ + public function __construct(array $parameters = array()) + { + $this->replace($parameters); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\HttpFoundation\ParameterBag::replace() + * + * @api + */ + public function replace(array $files = array()) + { + $this->parameters = array(); + $this->add($files); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\HttpFoundation\ParameterBag::set() + * + * @api + */ + public function set($key, $value) + { + if (is_array($value) || $value instanceof UploadedFile) { + parent::set($key, $this->convertFileInformation($value)); + } else { + throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); + } + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\HttpFoundation\ParameterBag::add() + * + * @api + */ + public function add(array $files = array()) + { + foreach ($files as $key => $file) { + $this->set($key, $file); + } + } + + /** + * Converts uploaded files to UploadedFile instances. + * + * @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information + * + * @return array A (multi-dimensional) array of UploadedFile instances + */ + protected function convertFileInformation($file) + { + if ($file instanceof UploadedFile) { + return $file; + } + + $file = $this->fixPhpFilesArray($file); + if (is_array($file)) { + $keys = array_keys($file); + sort($keys); + + if ($keys == self::$fileKeys) { + if (UPLOAD_ERR_NO_FILE == $file['error']) { + $file = null; + } else { + $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['size'], $file['error']); + } + } else { + $file = array_map(array($this, 'convertFileInformation'), $file); + } + } + + return $file; + } + + /** + * Fixes a malformed PHP $_FILES array. + * + * PHP has a bug that the format of the $_FILES array differs, depending on + * whether the uploaded file fields had normal field names or array-like + * field names ("normal" vs. "parent[child]"). + * + * This method fixes the array to look like the "normal" $_FILES array. + * + * It's safe to pass an already converted array, in which case this method + * just returns the original array unmodified. + * + * @param array $data + * @return array + */ + protected function fixPhpFilesArray($data) + { + if (!is_array($data)) { + return $data; + } + + $keys = array_keys($data); + sort($keys); + + if (self::$fileKeys != $keys || !isset($data['name']) || !is_array($data['name'])) { + return $data; + } + + $files = $data; + foreach (self::$fileKeys as $k) { + unset($files[$k]); + } + + foreach (array_keys($data['name']) as $key) { + $files[$key] = $this->fixPhpFilesArray(array( + 'error' => $data['error'][$key], + 'name' => $data['name'][$key], + 'type' => $data['type'][$key], + 'tmp_name' => $data['tmp_name'][$key], + 'size' => $data['size'][$key] + )); + } + + return $files; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/HeaderBag.php b/includes/Symfony/Component/HttpFoundation/HeaderBag.php new file mode 100644 index 0000000..f614b09 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/HeaderBag.php @@ -0,0 +1,306 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * HeaderBag is a container for HTTP headers. + * + * @author Fabien Potencier + * + * @api + */ +class HeaderBag +{ + protected $headers; + protected $cacheControl; + + /** + * Constructor. + * + * @param array $headers An array of HTTP headers + * + * @api + */ + public function __construct(array $headers = array()) + { + $this->cacheControl = array(); + $this->headers = array(); + foreach ($headers as $key => $values) { + $this->set($key, $values); + } + } + + /** + * Returns the headers as a string. + * + * @return string The headers + */ + public function __toString() + { + if (!$this->headers) { + return ''; + } + + $beautifier = function ($name) { + return preg_replace_callback('/\-(.)/', function ($match) { return '-'.strtoupper($match[1]); }, ucfirst($name)); + }; + + $max = max(array_map('strlen', array_keys($this->headers))) + 1; + $content = ''; + ksort($this->headers); + foreach ($this->headers as $name => $values) { + foreach ($values as $value) { + $content .= sprintf("%-{$max}s %s\r\n", $beautifier($name).':', $value); + } + } + + return $content; + } + + /** + * Returns the headers. + * + * @return array An array of headers + * + * @api + */ + public function all() + { + return $this->headers; + } + + /** + * Returns the parameter keys. + * + * @return array An array of parameter keys + * + * @api + */ + public function keys() + { + return array_keys($this->headers); + } + + /** + * Replaces the current HTTP headers by a new set. + * + * @param array $headers An array of HTTP headers + * + * @api + */ + public function replace(array $headers = array()) + { + $this->headers = array(); + $this->add($headers); + } + + /** + * Adds new headers the current HTTP headers set. + * + * @param array $headers An array of HTTP headers + * + * @api + */ + public function add(array $headers) + { + foreach ($headers as $key => $values) { + $this->set($key, $values); + } + } + + /** + * Returns a header value by name. + * + * @param string $key The header name + * @param mixed $default The default value + * @param Boolean $first Whether to return the first value or all header values + * + * @return string|array The first header value if $first is true, an array of values otherwise + * + * @api + */ + public function get($key, $default = null, $first = true) + { + $key = strtr(strtolower($key), '_', '-'); + + if (!array_key_exists($key, $this->headers)) { + if (null === $default) { + return $first ? null : array(); + } + + return $first ? $default : array($default); + } + + if ($first) { + return count($this->headers[$key]) ? $this->headers[$key][0] : $default; + } + + return $this->headers[$key]; + } + + /** + * Sets a header by name. + * + * @param string $key The key + * @param string|array $values The value or an array of values + * @param Boolean $replace Whether to replace the actual value of not (true by default) + * + * @api + */ + public function set($key, $values, $replace = true) + { + $key = strtr(strtolower($key), '_', '-'); + + $values = (array) $values; + + if (true === $replace || !isset($this->headers[$key])) { + $this->headers[$key] = $values; + } else { + $this->headers[$key] = array_merge($this->headers[$key], $values); + } + + if ('cache-control' === $key) { + $this->cacheControl = $this->parseCacheControl($values[0]); + } + } + + /** + * Returns true if the HTTP header is defined. + * + * @param string $key The HTTP header + * + * @return Boolean true if the parameter exists, false otherwise + * + * @api + */ + public function has($key) + { + return array_key_exists(strtr(strtolower($key), '_', '-'), $this->headers); + } + + /** + * Returns true if the given HTTP header contains the given value. + * + * @param string $key The HTTP header name + * @param string $value The HTTP value + * + * @return Boolean true if the value is contained in the header, false otherwise + * + * @api + */ + public function contains($key, $value) + { + return in_array($value, $this->get($key, null, false)); + } + + /** + * Removes a header. + * + * @param string $key The HTTP header name + * + * @api + */ + public function remove($key) + { + $key = strtr(strtolower($key), '_', '-'); + + unset($this->headers[$key]); + + if ('cache-control' === $key) { + $this->cacheControl = array(); + } + } + + /** + * Returns the HTTP header value converted to a date. + * + * @param string $key The parameter key + * @param \DateTime $default The default value + * + * @return \DateTime The filtered value + * + * @api + */ + public function getDate($key, \DateTime $default = null) + { + if (null === $value = $this->get($key)) { + return $default; + } + + if (false === $date = \DateTime::createFromFormat(DATE_RFC2822, $value)) { + throw new \RuntimeException(sprintf('The %s HTTP header is not parseable (%s).', $key, $value)); + } + + return $date; + } + + public function addCacheControlDirective($key, $value = true) + { + $this->cacheControl[$key] = $value; + + $this->set('Cache-Control', $this->getCacheControlHeader()); + } + + public function hasCacheControlDirective($key) + { + return array_key_exists($key, $this->cacheControl); + } + + public function getCacheControlDirective($key) + { + return array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null; + } + + public function removeCacheControlDirective($key) + { + unset($this->cacheControl[$key]); + + $this->set('Cache-Control', $this->getCacheControlHeader()); + } + + protected function getCacheControlHeader() + { + $parts = array(); + ksort($this->cacheControl); + foreach ($this->cacheControl as $key => $value) { + if (true === $value) { + $parts[] = $key; + } else { + if (preg_match('#[^a-zA-Z0-9._-]#', $value)) { + $value = '"'.$value.'"'; + } + + $parts[] = "$key=$value"; + } + } + + return implode(', ', $parts); + } + + /** + * Parses a Cache-Control HTTP header. + * + * @param string $header The value of the Cache-Control HTTP header + * + * @return array An array representing the attribute values + */ + protected function parseCacheControl($header) + { + $cacheControl = array(); + preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $cacheControl[strtolower($match[1])] = isset($match[2]) && $match[2] ? $match[2] : (isset($match[3]) ? $match[3] : true); + } + + return $cacheControl; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/LICENSE b/includes/Symfony/Component/HttpFoundation/LICENSE new file mode 100644 index 0000000..89df448 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2011 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/Symfony/Component/HttpFoundation/ParameterBag.php b/includes/Symfony/Component/HttpFoundation/ParameterBag.php new file mode 100644 index 0000000..3a38b5f --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/ParameterBag.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ParameterBag is a container for key/value pairs. + * + * @author Fabien Potencier + * + * @api + */ +class ParameterBag +{ + protected $parameters; + + /** + * Constructor. + * + * @param array $parameters An array of parameters + * + * @api + */ + public function __construct(array $parameters = array()) + { + $this->parameters = $parameters; + } + + /** + * Returns the parameters. + * + * @return array An array of parameters + * + * @api + */ + public function all() + { + return $this->parameters; + } + + /** + * Returns the parameter keys. + * + * @return array An array of parameter keys + * + * @api + */ + public function keys() + { + return array_keys($this->parameters); + } + + /** + * Replaces the current parameters by a new set. + * + * @param array $parameters An array of parameters + * + * @api + */ + public function replace(array $parameters = array()) + { + $this->parameters = $parameters; + } + + /** + * Adds parameters. + * + * @param array $parameters An array of parameters + * + * @api + */ + public function add(array $parameters = array()) + { + $this->parameters = array_replace($this->parameters, $parameters); + } + + /** + * Returns a parameter by name. + * + * @param string $path The key + * @param mixed $default The default value + * @param boolean $deep + * + * @api + */ + public function get($path, $default = null, $deep = false) + { + if (!$deep || false === $pos = strpos($path, '[')) { + return array_key_exists($path, $this->parameters) ? $this->parameters[$path] : $default; + } + + $root = substr($path, 0, $pos); + if (!array_key_exists($root, $this->parameters)) { + return $default; + } + + $value = $this->parameters[$root]; + $currentKey = null; + for ($i=$pos,$c=strlen($path); $i<$c; $i++) { + $char = $path[$i]; + + if ('[' === $char) { + if (null !== $currentKey) { + throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "[" at position %d.', $i)); + } + + $currentKey = ''; + } else if (']' === $char) { + if (null === $currentKey) { + throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "]" at position %d.', $i)); + } + + if (!is_array($value) || !array_key_exists($currentKey, $value)) { + return $default; + } + + $value = $value[$currentKey]; + $currentKey = null; + } else { + if (null === $currentKey) { + throw new \InvalidArgumentException(sprintf('Malformed path. Unexpected "%s" at position %d.', $char, $i)); + } + + $currentKey .= $char; + } + } + + if (null !== $currentKey) { + throw new \InvalidArgumentException(sprintf('Malformed path. Path must end with "]".')); + } + + return $value; + } + + /** + * Sets a parameter by name. + * + * @param string $key The key + * @param mixed $value The value + * + * @api + */ + public function set($key, $value) + { + $this->parameters[$key] = $value; + } + + /** + * Returns true if the parameter is defined. + * + * @param string $key The key + * + * @return Boolean true if the parameter exists, false otherwise + * + * @api + */ + public function has($key) + { + return array_key_exists($key, $this->parameters); + } + + /** + * Removes a parameter. + * + * @param string $key The key + * + * @api + */ + public function remove($key) + { + unset($this->parameters[$key]); + } + + /** + * Returns the alphabetic characters of the parameter value. + * + * @param string $key The parameter key + * @param mixed $default The default value + * @param boolean $deep + * + * @return string The filtered value + * + * @api + */ + public function getAlpha($key, $default = '', $deep = false) + { + return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default, $deep)); + } + + /** + * Returns the alphabetic characters and digits of the parameter value. + * + * @param string $key The parameter key + * @param mixed $default The default value + * @param boolean $deep + * + * @return string The filtered value + * + * @api + */ + public function getAlnum($key, $default = '', $deep = false) + { + return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default, $deep)); + } + + /** + * Returns the digits of the parameter value. + * + * @param string $key The parameter key + * @param mixed $default The default value + * @param boolean $deep + * + * @return string The filtered value + * + * @api + */ + public function getDigits($key, $default = '', $deep = false) + { + return preg_replace('/[^[:digit:]]/', '', $this->get($key, $default, $deep)); + } + + /** + * Returns the parameter value converted to integer. + * + * @param string $key The parameter key + * @param mixed $default The default value + * @param boolean $deep + * + * @return string The filtered value + * + * @api + */ + public function getInt($key, $default = 0, $deep = false) + { + return (int) $this->get($key, $default, $deep); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/RedirectResponse.php b/includes/Symfony/Component/HttpFoundation/RedirectResponse.php new file mode 100644 index 0000000..3318b12 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/RedirectResponse.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RedirectResponse represents an HTTP response doing a redirect. + * + * @author Fabien Potencier + * + * @api + */ +class RedirectResponse extends Response +{ + /** + * Creates a redirect response so that it conforms to the rules defined for a redirect status code. + * + * @param string $url The URL to redirect to + * @param integer $status The status code (302 by default) + * + * @see http://tools.ietf.org/html/rfc2616#section-10.3 + * + * @api + */ + public function __construct($url, $status = 302) + { + if (empty($url)) { + throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + } + + parent::__construct( + sprintf(' + + + + + + Redirecting to %1$s + + + Redirecting to %1$s. + +', htmlspecialchars($url, ENT_QUOTES, 'UTF-8')), + $status, + array('Location' => $url) + ); + + if (!$this->isRedirect()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); + } + } +} diff --git a/includes/Symfony/Component/HttpFoundation/Request.php b/includes/Symfony/Component/HttpFoundation/Request.php new file mode 100644 index 0000000..e66abb7 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/Request.php @@ -0,0 +1,1217 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Request represents an HTTP request. + * + * @author Fabien Potencier + * + * @api + */ +class Request +{ + static protected $trustProxy = false; + + /** + * @var \Symfony\Component\HttpFoundation\ParameterBag + * + * @api + */ + public $attributes; + + /** + * @var \Symfony\Component\HttpFoundation\ParameterBag + * + * @api + */ + public $request; + + /** + * @var \Symfony\Component\HttpFoundation\ParameterBag + * + * @api + */ + public $query; + + /** + * @var \Symfony\Component\HttpFoundation\ServerBag + * + * @api + */ + public $server; + + /** + * @var \Symfony\Component\HttpFoundation\FileBag + * + * @api + */ + public $files; + + /** + * @var \Symfony\Component\HttpFoundation\ParameterBag + * + * @api + */ + public $cookies; + + /** + * @var \Symfony\Component\HttpFoundation\HeaderBag + * + * @api + */ + public $headers; + + protected $content; + protected $languages; + protected $charsets; + protected $acceptableContentTypes; + protected $pathInfo; + protected $requestUri; + protected $baseUrl; + protected $basePath; + protected $method; + protected $format; + protected $session; + + static protected $formats; + + /** + * Constructor. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string $content The raw body data + * + * @api + */ + public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + { + $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); + } + + /** + * Sets the parameters for this request. + * + * This method also re-initializes all properties. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string $content The raw body data + * + * @api + */ + public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + { + $this->request = new ParameterBag($request); + $this->query = new ParameterBag($query); + $this->attributes = new ParameterBag($attributes); + $this->cookies = new ParameterBag($cookies); + $this->files = new FileBag($files); + $this->server = new ServerBag($server); + $this->headers = new HeaderBag($this->server->getHeaders()); + + $this->content = $content; + $this->languages = null; + $this->charsets = null; + $this->acceptableContentTypes = null; + $this->pathInfo = null; + $this->requestUri = null; + $this->baseUrl = null; + $this->basePath = null; + $this->method = null; + $this->format = null; + } + + /** + * Creates a new request with values from PHP's super globals. + * + * @return Request A new request + * + * @api + */ + static public function createFromGlobals() + { + $request = new static($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); + + if (0 === strpos($request->server->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') + && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE')) + ) { + parse_str($request->getContent(), $data); + $request->request = new ParameterBag($data); + } + + return $request; + } + + /** + * Creates a Request based on a given URI and configuration. + * + * @param string $uri The URI + * @param string $method The HTTP method + * @param array $parameters The request (GET) or query (POST) parameters + * @param array $cookies The request cookies ($_COOKIE) + * @param array $files The request files ($_FILES) + * @param array $server The server parameters ($_SERVER) + * @param string $content The raw body data + * + * @return Request A Request instance + * + * @api + */ + static public function create($uri, $method = 'GET', $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null) + { + $defaults = array( + 'SERVER_NAME' => 'localhost', + 'SERVER_PORT' => 80, + 'HTTP_HOST' => 'localhost', + 'HTTP_USER_AGENT' => 'Symfony/2.X', + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5', + 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'REMOTE_ADDR' => '127.0.0.1', + 'SCRIPT_NAME' => '', + 'SCRIPT_FILENAME' => '', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_TIME' => time(), + ); + + $components = parse_url($uri); + if (isset($components['host'])) { + $defaults['SERVER_NAME'] = $components['host']; + $defaults['HTTP_HOST'] = $components['host']; + } + + if (isset($components['scheme'])) { + if ('https' === $components['scheme']) { + $defaults['HTTPS'] = 'on'; + $defaults['SERVER_PORT'] = 443; + } + } + + if (isset($components['port'])) { + $defaults['SERVER_PORT'] = $components['port']; + $defaults['HTTP_HOST'] = $defaults['HTTP_HOST'].':'.$components['port']; + } + + if (!isset($components['path'])) { + $components['path'] = ''; + } + + if (in_array(strtoupper($method), array('POST', 'PUT', 'DELETE'))) { + $request = $parameters; + $query = array(); + $defaults['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + } else { + $request = array(); + $query = $parameters; + if (false !== $pos = strpos($uri, '?')) { + $qs = substr($uri, $pos + 1); + parse_str($qs, $params); + + $query = array_merge($params, $query); + } + } + + $queryString = isset($components['query']) ? html_entity_decode($components['query']) : ''; + parse_str($queryString, $qs); + if (is_array($qs)) { + $query = array_replace($qs, $query); + } + + $uri = $components['path'].($queryString ? '?'.$queryString : ''); + + $server = array_replace($defaults, $server, array( + 'REQUEST_METHOD' => strtoupper($method), + 'PATH_INFO' => '', + 'REQUEST_URI' => $uri, + 'QUERY_STRING' => $queryString, + )); + + return new static($query, $request, array(), $cookies, $files, $server, $content); + } + + /** + * Clones a request and overrides some of its parameters. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * + * @api + */ + public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) + { + $dup = clone $this; + if ($query !== null) { + $dup->query = new ParameterBag($query); + } + if ($request !== null) { + $dup->request = new ParameterBag($request); + } + if ($attributes !== null) { + $dup->attributes = new ParameterBag($attributes); + } + if ($cookies !== null) { + $dup->cookies = new ParameterBag($cookies); + } + if ($files !== null) { + $dup->files = new FileBag($files); + } + if ($server !== null) { + $dup->server = new ServerBag($server); + $dup->headers = new HeaderBag($dup->server->getHeaders()); + } + $dup->languages = null; + $dup->charsets = null; + $dup->acceptableContentTypes = null; + $dup->pathInfo = null; + $dup->requestUri = null; + $dup->baseUrl = null; + $dup->basePath = null; + $dup->method = null; + $dup->format = null; + + return $dup; + } + + /** + * Clones the current request. + * + * Note that the session is not cloned as duplicated requests + * are most of the time sub-requests of the main one. + */ + public function __clone() + { + $this->query = clone $this->query; + $this->request = clone $this->request; + $this->attributes = clone $this->attributes; + $this->cookies = clone $this->cookies; + $this->files = clone $this->files; + $this->server = clone $this->server; + $this->headers = clone $this->headers; + } + + /** + * Returns the request as a string. + * + * @return string The request + */ + public function __toString() + { + return + sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". + $this->headers."\r\n". + $this->getContent(); + } + + /** + * Overrides the PHP global variables according to this request instance. + * + * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE, and $_FILES. + * + * @api + */ + public function overrideGlobals() + { + $_GET = $this->query->all(); + $_POST = $this->request->all(); + $_SERVER = $this->server->all(); + $_COOKIE = $this->cookies->all(); + // FIXME: populate $_FILES + + foreach ($this->headers->all() as $key => $value) { + $key = strtoupper(str_replace('-', '_', $key)); + if (in_array($key, array('CONTENT_TYPE', 'CONTENT_LENGTH'))) { + $_SERVER[$key] = implode(', ', $value); + } else { + $_SERVER['HTTP_'.$key] = implode(', ', $value); + } + } + + // FIXME: should read variables_order and request_order + // to know which globals to merge and in which order + $_REQUEST = array_merge($_GET, $_POST); + } + + /** + * Trusts $_SERVER entries coming from proxies. + * + * You should only call this method if your application + * is hosted behind a reverse proxy that you manage. + * + * @api + */ + static public function trustProxyData() + { + self::$trustProxy = true; + } + + /** + * Gets a "parameter" value. + * + * This method is mainly useful for libraries that want to provide some flexibility. + * + * Order of precedence: GET, PATH, POST, COOKIE + * Avoid using this method in controllers: + * * slow + * * prefer to get from a "named" source + * + * @param string $key the key + * @param mixed $default the default value + * @param type $deep is parameter deep in multidimensional array + * + * @return mixed + */ + public function get($key, $default = null, $deep = false) + { + return $this->query->get($key, $this->attributes->get($key, $this->request->get($key, $default, $deep), $deep), $deep); + } + + /** + * Gets the Session. + * + * @return Session|null The session + * + * @api + */ + public function getSession() + { + return $this->session; + } + + /** + * Whether the request contains a Session which was started in one of the + * previous requests. + * + * @return boolean + * + * @api + */ + public function hasPreviousSession() + { + // the check for $this->session avoids malicious users trying to fake a session cookie with proper name + return $this->cookies->has(session_name()) && null !== $this->session; + } + + /** + * Whether the request contains a Session object. + * + * @return boolean + * + * @api + */ + public function hasSession() + { + return null !== $this->session; + } + + /** + * Sets the Session. + * + * @param Session $session The Session + * + * @api + */ + public function setSession(Session $session) + { + $this->session = $session; + } + + /** + * Returns the client IP address. + * + * @param Boolean $proxy Whether the current request has been made behind a proxy or not + * + * @return string The client IP address + * + * @api + */ + public function getClientIp($proxy = false) + { + if ($proxy) { + if ($this->server->has('HTTP_CLIENT_IP')) { + return $this->server->get('HTTP_CLIENT_IP'); + } elseif (self::$trustProxy && $this->server->has('HTTP_X_FORWARDED_FOR')) { + return $this->server->get('HTTP_X_FORWARDED_FOR'); + } + } + + return $this->server->get('REMOTE_ADDR'); + } + + /** + * Returns current script name. + * + * @return string + * + * @api + */ + public function getScriptName() + { + return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME', '')); + } + + /** + * Returns the path being requested relative to the executed script. + * + * The path info always starts with a /. + * + * Suppose this request is instantiated from /mysite on localhost: + * + * * http://localhost/mysite returns an empty string + * * http://localhost/mysite/about returns '/about' + * * http://localhost/mysite/about?var=1 returns '/about' + * + * @return string + * + * @api + */ + public function getPathInfo() + { + if (null === $this->pathInfo) { + $this->pathInfo = $this->preparePathInfo(); + } + + return $this->pathInfo; + } + + /** + * Returns the root path from which this request is executed. + * + * Suppose that an index.php file instantiates this request object: + * + * * http://localhost/index.php returns an empty string + * * http://localhost/index.php/page returns an empty string + * * http://localhost/web/index.php return '/web' + * + * @return string + * + * @api + */ + public function getBasePath() + { + if (null === $this->basePath) { + $this->basePath = $this->prepareBasePath(); + } + + return $this->basePath; + } + + /** + * Returns the root url from which this request is executed. + * + * The base URL never ends with a /. + * + * This is similar to getBasePath(), except that it also includes the + * script filename (e.g. index.php) if one exists. + * + * @return string + * + * @api + */ + public function getBaseUrl() + { + if (null === $this->baseUrl) { + $this->baseUrl = $this->prepareBaseUrl(); + } + + return $this->baseUrl; + } + + /** + * Gets the request's scheme. + * + * @return string + * + * @api + */ + public function getScheme() + { + return $this->isSecure() ? 'https' : 'http'; + } + + /** + * Returns the port on which the request is made. + * + * @return string + * + * @api + */ + public function getPort() + { + return $this->headers->get('X-Forwarded-Port') ?: $this->server->get('SERVER_PORT'); + } + + /** + * Returns the HTTP host being requested. + * + * The port name will be appended to the host if it's non-standard. + * + * @return string + * + * @api + */ + public function getHttpHost() + { + $scheme = $this->getScheme(); + $port = $this->getPort(); + + if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) { + return $this->getHost(); + } + + return $this->getHost().':'.$port; + } + + /** + * Returns the requested URI. + * + * @return string + * + * @api + */ + public function getRequestUri() + { + if (null === $this->requestUri) { + $this->requestUri = $this->prepareRequestUri(); + } + + return $this->requestUri; + } + + /** + * Generates a normalized URI for the Request. + * + * @return string A normalized URI for the Request + * + * @see getQueryString() + * + * @api + */ + public function getUri() + { + $qs = $this->getQueryString(); + if (null !== $qs) { + $qs = '?'.$qs; + } + + return $this->getScheme().'://'.$this->getHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs; + } + + /** + * Generates a normalized URI for the given path. + * + * @param string $path A path to use instead of the current one + * + * @return string The normalized URI for the path + * + * @api + */ + public function getUriForPath($path) + { + return $this->getScheme().'://'.$this->getHttpHost().$this->getBaseUrl().$path; + } + + /** + * Generates the normalized query string for the Request. + * + * It builds a normalized query string, where keys/value pairs are alphabetized + * and have consistent escaping. + * + * @return string A normalized query string for the Request + * + * @api + */ + public function getQueryString() + { + if (!$qs = $this->server->get('QUERY_STRING')) { + return null; + } + + $parts = array(); + $order = array(); + + foreach (explode('&', $qs) as $segment) { + if (false === strpos($segment, '=')) { + $parts[] = $segment; + $order[] = $segment; + } else { + $tmp = explode('=', rawurldecode($segment), 2); + $parts[] = rawurlencode($tmp[0]).'='.rawurlencode($tmp[1]); + $order[] = $tmp[0]; + } + } + array_multisort($order, SORT_ASC, $parts); + + return implode('&', $parts); + } + + /** + * Checks whether the request is secure or not. + * + * @return Boolean + * + * @api + */ + public function isSecure() + { + return ( + (strtolower($this->server->get('HTTPS')) == 'on' || $this->server->get('HTTPS') == 1) + || + (self::$trustProxy && strtolower($this->headers->get('SSL_HTTPS')) == 'on' || $this->headers->get('SSL_HTTPS') == 1) + || + (self::$trustProxy && strtolower($this->headers->get('X_FORWARDED_PROTO')) == 'https') + ); + } + + /** + * Returns the host name. + * + * @return string + * + * @api + */ + public function getHost() + { + if (self::$trustProxy && $host = $this->headers->get('X_FORWARDED_HOST')) { + $elements = explode(',', $host); + + $host = trim($elements[count($elements) - 1]); + } else { + if (!$host = $this->headers->get('HOST')) { + if (!$host = $this->server->get('SERVER_NAME')) { + $host = $this->server->get('SERVER_ADDR', ''); + } + } + } + + // Remove port number from host + $host = preg_replace('/:\d+$/', '', $host); + + return trim($host); + } + + /** + * Sets the request method. + * + * @param string $method + * + * @api + */ + public function setMethod($method) + { + $this->method = null; + $this->server->set('REQUEST_METHOD', $method); + } + + /** + * Gets the request method. + * + * The method is always an uppercased string. + * + * @return string The request method + * + * @api + */ + public function getMethod() + { + if (null === $this->method) { + $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET')); + if ('POST' === $this->method) { + $this->method = strtoupper($this->headers->get('X-HTTP-METHOD-OVERRIDE', $this->request->get('_method', 'POST'))); + } + } + + return $this->method; + } + + /** + * Gets the mime type associated with the format. + * + * @param string $format The format + * + * @return string The associated mime type (null if not found) + * + * @api + */ + public function getMimeType($format) + { + if (null === static::$formats) { + static::initializeFormats(); + } + + return isset(static::$formats[$format]) ? static::$formats[$format][0] : null; + } + + /** + * Gets the format associated with the mime type. + * + * @param string $mimeType The associated mime type + * + * @return string The format (null if not found) + * + * @api + */ + public function getFormat($mimeType) + { + if (false !== $pos = strpos($mimeType, ';')) { + $mimeType = substr($mimeType, 0, $pos); + } + + if (null === static::$formats) { + static::initializeFormats(); + } + + foreach (static::$formats as $format => $mimeTypes) { + if (in_array($mimeType, (array) $mimeTypes)) { + return $format; + } + } + + return null; + } + + /** + * Associates a format with mime types. + * + * @param string $format The format + * @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) + * + * @api + */ + public function setFormat($format, $mimeTypes) + { + if (null === static::$formats) { + static::initializeFormats(); + } + + static::$formats[$format] = is_array($mimeTypes) ? $mimeTypes : array($mimeTypes); + } + + /** + * Gets the request format. + * + * Here is the process to determine the format: + * + * * format defined by the user (with setRequestFormat()) + * * _format request parameter + * * $default + * + * @param string $default The default format + * + * @return string The request format + * + * @api + */ + public function getRequestFormat($default = 'html') + { + if (null === $this->format) { + $this->format = $this->get('_format', $default); + } + + return $this->format; + } + + /** + * Sets the request format. + * + * @param string $format The request format. + * + * @api + */ + public function setRequestFormat($format) + { + $this->format = $format; + } + + /** + * Checks whether the method is safe or not. + * + * @return Boolean + * + * @api + */ + public function isMethodSafe() + { + return in_array($this->getMethod(), array('GET', 'HEAD')); + } + + /** + * Returns the request body content. + * + * @param Boolean $asResource If true, a resource will be returned + * + * @return string|resource The request body content or a resource to read the body stream. + */ + public function getContent($asResource = false) + { + if (false === $this->content || (true === $asResource && null !== $this->content)) { + throw new \LogicException('getContent() can only be called once when using the resource return type.'); + } + + if (true === $asResource) { + $this->content = false; + + return fopen('php://input', 'rb'); + } + + if (null === $this->content) { + $this->content = file_get_contents('php://input'); + } + + return $this->content; + } + + /** + * Gets the Etags. + * + * @return array The entity tags + */ + public function getETags() + { + return preg_split('/\s*,\s*/', $this->headers->get('if_none_match'), null, PREG_SPLIT_NO_EMPTY); + } + + public function isNoCache() + { + return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma'); + } + + /** + * Returns the preferred language. + * + * @param array $locales An array of ordered available locales + * + * @return string The preferred locale + * + * @api + */ + public function getPreferredLanguage(array $locales = null) + { + $preferredLanguages = $this->getLanguages(); + + if (null === $locales) { + return isset($preferredLanguages[0]) ? $preferredLanguages[0] : null; + } + + if (!$preferredLanguages) { + return $locales[0]; + } + + $preferredLanguages = array_values(array_intersect($preferredLanguages, $locales)); + + return isset($preferredLanguages[0]) ? $preferredLanguages[0] : $locales[0]; + } + + /** + * Gets a list of languages acceptable by the client browser. + * + * @return array Languages ordered in the user browser preferences + * + * @api + */ + public function getLanguages() + { + if (null !== $this->languages) { + return $this->languages; + } + + $languages = $this->splitHttpAcceptHeader($this->headers->get('Accept-Language')); + $this->languages = array(); + foreach ($languages as $lang => $q) { + if (strstr($lang, '-')) { + $codes = explode('-', $lang); + if ($codes[0] == 'i') { + // Language not listed in ISO 639 that are not variants + // of any listed language, which can be registered with the + // i-prefix, such as i-cherokee + if (count($codes) > 1) { + $lang = $codes[1]; + } + } else { + for ($i = 0, $max = count($codes); $i < $max; $i++) { + if ($i == 0) { + $lang = strtolower($codes[0]); + } else { + $lang .= '_'.strtoupper($codes[$i]); + } + } + } + } + + $this->languages[] = $lang; + } + + return $this->languages; + } + + /** + * Gets a list of charsets acceptable by the client browser. + * + * @return array List of charsets in preferable order + * + * @api + */ + public function getCharsets() + { + if (null !== $this->charsets) { + return $this->charsets; + } + + return $this->charsets = array_keys($this->splitHttpAcceptHeader($this->headers->get('Accept-Charset'))); + } + + /** + * Gets a list of content types acceptable by the client browser + * + * @return array List of content types in preferable order + * + * @api + */ + public function getAcceptableContentTypes() + { + if (null !== $this->acceptableContentTypes) { + return $this->acceptableContentTypes; + } + + return $this->acceptableContentTypes = array_keys($this->splitHttpAcceptHeader($this->headers->get('Accept'))); + } + + /** + * Returns true if the request is a XMLHttpRequest. + * + * It works if your JavaScript library set an X-Requested-With HTTP header. + * It is known to work with Prototype, Mootools, jQuery. + * + * @return Boolean true if the request is an XMLHttpRequest, false otherwise + * + * @api + */ + public function isXmlHttpRequest() + { + return 'XMLHttpRequest' == $this->headers->get('X-Requested-With'); + } + + /** + * Splits an Accept-* HTTP header. + * + * @param string $header Header to split + */ + public function splitHttpAcceptHeader($header) + { + if (!$header) { + return array(); + } + + $values = array(); + foreach (array_filter(explode(',', $header)) as $value) { + // Cut off any q-value that might come after a semi-colon + if ($pos = strpos($value, ';')) { + $q = (float) trim(substr($value, strpos($value, '=') + 1)); + $value = trim(substr($value, 0, $pos)); + } else { + $q = 1; + } + + if (0 < $q) { + $values[trim($value)] = $q; + } + } + + arsort($values); + reset($values); + + return $values; + } + + /* + * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24) + * + * Code subject to the new BSD license (http://framework.zend.com/license/new-bsd). + * + * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) + */ + + protected function prepareRequestUri() + { + $requestUri = ''; + + if ($this->headers->has('X_REWRITE_URL')) { + // check this first so IIS will catch + $requestUri = $this->headers->get('X_REWRITE_URL'); + } elseif ($this->server->get('IIS_WasUrlRewritten') == '1' && $this->server->get('UNENCODED_URL') != '') { + // IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem) + $requestUri = $this->server->get('UNENCODED_URL'); + } elseif ($this->server->has('REQUEST_URI')) { + $requestUri = $this->server->get('REQUEST_URI'); + // HTTP proxy reqs setup request uri with scheme and host [and port] + the url path, only use url path + $schemeAndHttpHost = $this->getScheme().'://'.$this->getHttpHost(); + if (strpos($requestUri, $schemeAndHttpHost) === 0) { + $requestUri = substr($requestUri, strlen($schemeAndHttpHost)); + } + } elseif ($this->server->has('ORIG_PATH_INFO')) { + // IIS 5.0, PHP as CGI + $requestUri = $this->server->get('ORIG_PATH_INFO'); + if ($this->server->get('QUERY_STRING')) { + $requestUri .= '?'.$this->server->get('QUERY_STRING'); + } + } + + return $requestUri; + } + + protected function prepareBaseUrl() + { + $filename = basename($this->server->get('SCRIPT_FILENAME')); + + if (basename($this->server->get('SCRIPT_NAME')) === $filename) { + $baseUrl = $this->server->get('SCRIPT_NAME'); + } elseif (basename($this->server->get('PHP_SELF')) === $filename) { + $baseUrl = $this->server->get('PHP_SELF'); + } elseif (basename($this->server->get('ORIG_SCRIPT_NAME')) === $filename) { + $baseUrl = $this->server->get('ORIG_SCRIPT_NAME'); // 1and1 shared hosting compatibility + } else { + // Backtrack up the script_filename to find the portion matching + // php_self + $path = $this->server->get('PHP_SELF', ''); + $file = $this->server->get('SCRIPT_FILENAME', ''); + $segs = explode('/', trim($file, '/')); + $segs = array_reverse($segs); + $index = 0; + $last = count($segs); + $baseUrl = ''; + do { + $seg = $segs[$index]; + $baseUrl = '/'.$seg.$baseUrl; + ++$index; + } while (($last > $index) && (false !== ($pos = strpos($path, $baseUrl))) && (0 != $pos)); + } + + // Does the baseUrl have anything in common with the request_uri? + $requestUri = $this->getRequestUri(); + + if ($baseUrl && 0 === strpos($requestUri, $baseUrl)) { + // full $baseUrl matches + return $baseUrl; + } + + if ($baseUrl && 0 === strpos($requestUri, dirname($baseUrl))) { + // directory portion of $baseUrl matches + return rtrim(dirname($baseUrl), '/'); + } + + $truncatedRequestUri = $requestUri; + if (($pos = strpos($requestUri, '?')) !== false) { + $truncatedRequestUri = substr($requestUri, 0, $pos); + } + + $basename = basename($baseUrl); + if (empty($basename) || !strpos($truncatedRequestUri, $basename)) { + // no match whatsoever; set it blank + return ''; + } + + // If using mod_rewrite or ISAPI_Rewrite strip the script filename + // out of baseUrl. $pos !== 0 makes sure it is not matching a value + // from PATH_INFO or QUERY_STRING + if ((strlen($requestUri) >= strlen($baseUrl)) && ((false !== ($pos = strpos($requestUri, $baseUrl))) && ($pos !== 0))) { + $baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl)); + } + + return rtrim($baseUrl, '/'); + } + + /** + * Prepares base path. + * + * @return string base path + */ + protected function prepareBasePath() + { + $filename = basename($this->server->get('SCRIPT_FILENAME')); + $baseUrl = $this->getBaseUrl(); + if (empty($baseUrl)) { + return ''; + } + + if (basename($baseUrl) === $filename) { + $basePath = dirname($baseUrl); + } else { + $basePath = $baseUrl; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + $basePath = str_replace('\\', '/', $basePath); + } + + return rtrim($basePath, '/'); + } + + /** + * Prepares path info. + * + * @return string path info + */ + protected function preparePathInfo() + { + $baseUrl = $this->getBaseUrl(); + + if (null === ($requestUri = $this->getRequestUri())) { + return '/'; + } + + $pathInfo = '/'; + + // Remove the query string from REQUEST_URI + if ($pos = strpos($requestUri, '?')) { + $requestUri = substr($requestUri, 0, $pos); + } + + if ((null !== $baseUrl) && (false === ($pathInfo = substr(urldecode($requestUri), strlen(urldecode($baseUrl)))))) { + // If substr() returns false then PATH_INFO is set to an empty string + return '/'; + } elseif (null === $baseUrl) { + return $requestUri; + } + + return (string) $pathInfo; + } + + /** + * Initializes HTTP request formats. + */ + static protected function initializeFormats() + { + static::$formats = array( + 'html' => array('text/html', 'application/xhtml+xml'), + 'txt' => array('text/plain'), + 'js' => array('application/javascript', 'application/x-javascript', 'text/javascript'), + 'css' => array('text/css'), + 'json' => array('application/json', 'application/x-json'), + 'xml' => array('text/xml', 'application/xml', 'application/x-xml'), + 'rdf' => array('application/rdf+xml'), + 'atom' => array('application/atom+xml'), + ); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/RequestMatcher.php b/includes/Symfony/Component/HttpFoundation/RequestMatcher.php new file mode 100644 index 0000000..52ba078 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/RequestMatcher.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RequestMatcher compares a pre-defined set of checks against a Request instance. + * + * @author Fabien Potencier + * + * @api + */ +class RequestMatcher implements RequestMatcherInterface +{ + private $path; + private $host; + private $methods; + private $ip; + private $attributes; + + public function __construct($path = null, $host = null, $methods = null, $ip = null, array $attributes = array()) + { + $this->path = $path; + $this->host = $host; + $this->methods = $methods; + $this->ip = $ip; + $this->attributes = $attributes; + } + + /** + * Adds a check for the URL host name. + * + * @param string $regexp A Regexp + */ + public function matchHost($regexp) + { + $this->host = $regexp; + } + + /** + * Adds a check for the URL path info. + * + * @param string $regexp A Regexp + */ + public function matchPath($regexp) + { + $this->path = $regexp; + } + + /** + * Adds a check for the client IP. + * + * @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + */ + public function matchIp($ip) + { + $this->ip = $ip; + } + + /** + * Adds a check for the HTTP method. + * + * @param string|array $method An HTTP method or an array of HTTP methods + */ + public function matchMethod($method) + { + $this->methods = array_map('strtoupper', is_array($method) ? $method : array($method)); + } + + /** + * Adds a check for request attribute. + * + * @param string $key The request attribute name + * @param string $regexp A Regexp + */ + public function matchAttribute($key, $regexp) + { + $this->attributes[$key] = $regexp; + } + + /** + * {@inheritdoc} + * + * @api + */ + public function matches(Request $request) + { + if (null !== $this->methods && !in_array($request->getMethod(), $this->methods)) { + return false; + } + + foreach ($this->attributes as $key => $pattern) { + if (!preg_match('#'.str_replace('#', '\\#', $pattern).'#', $request->attributes->get($key))) { + return false; + } + } + + if (null !== $this->path) { + $path = str_replace('#', '\\#', $this->path); + + if (!preg_match('#'.$path.'#', $request->getPathInfo())) { + return false; + } + } + + if (null !== $this->host && !preg_match('#'.str_replace('#', '\\#', $this->host).'#', $request->getHost())) { + return false; + } + + if (null !== $this->ip && !$this->checkIp($request->getClientIp(), $this->ip)) { + return false; + } + + return true; + } + + protected function checkIp($requestIp, $ip) + { + // IPv6 address + if (false !== strpos($requestIp, ':')) { + return $this->checkIp6($requestIp, $ip); + } else { + return $this->checkIp4($requestIp, $ip); + } + } + + protected function checkIp4($requestIp, $ip) + { + if (false !== strpos($ip, '/')) { + list($address, $netmask) = explode('/', $ip); + + if ($netmask < 1 || $netmask > 32) { + return false; + } + } else { + $address = $ip; + $netmask = 32; + } + + return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); + } + + /** + * @author David Soria Parra + * @see https://github.com/dsp/v6tools + */ + protected function checkIp6($requestIp, $ip) + { + if (!defined('AF_INET6')) { + throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); + } + + list($address, $netmask) = explode('/', $ip); + + $bytes_addr = unpack("n*", inet_pton($address)); + $bytes_test = unpack("n*", inet_pton($requestIp)); + + for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; $i++) { + $left = $netmask - 16 * ($i-1); + $left = ($left <= 16) ?: 16; + $mask = ~(0xffff >> $left) & 0xffff; + if (($bytes_addr[$i] & $mask) != ($bytes_test[$i] & $mask)) { + return false; + } + } + + return true; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/RequestMatcherInterface.php b/includes/Symfony/Component/HttpFoundation/RequestMatcherInterface.php new file mode 100644 index 0000000..506ec79 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/RequestMatcherInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RequestMatcherInterface is an interface for strategies to match a Request. + * + * @author Fabien Potencier + * + * @api + */ +interface RequestMatcherInterface +{ + /** + * Decides whether the rule(s) implemented by the strategy matches the supplied request. + * + * @param Request $request The request to check for a match + * + * @return Boolean true if the request matches, false otherwise + * + * @api + */ + function matches(Request $request); +} diff --git a/includes/Symfony/Component/HttpFoundation/Response.php b/includes/Symfony/Component/HttpFoundation/Response.php new file mode 100644 index 0000000..e8a7d56 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/Response.php @@ -0,0 +1,891 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Response represents an HTTP response. + * + * @author Fabien Potencier + * + * @api + */ +class Response +{ + /** + * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag + */ + public $headers; + + protected $content; + protected $version; + protected $statusCode; + protected $statusText; + protected $charset; + + static public $statusTexts = 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', + 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', + 418 => 'I\'m a teapot', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + ); + + /** + * Constructor. + * + * @param string $content The response content + * @param integer $status The response status code + * @param array $headers An array of response headers + * + * @api + */ + public function __construct($content = '', $status = 200, $headers = array()) + { + $this->headers = new ResponseHeaderBag($headers); + $this->setContent($content); + $this->setStatusCode($status); + $this->setProtocolVersion('1.0'); + if (!$this->headers->has('Date')) { + $this->setDate(new \DateTime(null, new \DateTimeZone('UTC'))); + } + } + + /** + * Returns the response content as it will be sent (with the headers). + * + * @return string The response content + */ + public function __toString() + { + $this->prepare(); + + return + sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + $this->headers."\r\n". + $this->getContent(); + } + + /** + * Clones the current Response instance. + */ + public function __clone() + { + $this->headers = clone $this->headers; + } + + /** + * Prepares the Response before it is sent to the client. + * + * This method tweaks the Response to ensure that it is + * compliant with RFC 2616. + */ + public function prepare() + { + if ($this->isInformational() || in_array($this->statusCode, array(204, 304))) { + $this->setContent(''); + } + + // Fix Content-Type + $charset = $this->charset ?: 'UTF-8'; + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', 'text/html; charset='.$charset); + } elseif ('text/' === substr($this->headers->get('Content-Type'), 0, 5) && false === strpos($this->headers->get('Content-Type'), 'charset')) { + // add the charset + $this->headers->set('Content-Type', $this->headers->get('Content-Type').'; charset='.$charset); + } + + // Fix Content-Length + if ($this->headers->has('Transfer-Encoding')) { + $this->headers->remove('Content-Length'); + } + } + + /** + * Sends HTTP headers. + */ + public function sendHeaders() + { + // headers have already been sent by the developer + if (headers_sent()) { + return; + } + + $this->prepare(); + + // status + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)); + + // headers + foreach ($this->headers->all() as $name => $values) { + foreach ($values as $value) { + header($name.': '.$value, false); + } + } + + // cookies + foreach ($this->headers->getCookies() as $cookie) { + setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); + } + } + + /** + * Sends content for the current web response. + */ + public function sendContent() + { + echo $this->content; + } + + /** + * Sends HTTP headers and content. + * + * @api + */ + public function send() + { + $this->sendHeaders(); + $this->sendContent(); + + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } + } + + /** + * Sets the response content + * + * Valid types are strings, numbers, and objects that implement a __toString() method. + * + * @param mixed $content + * + * @api + */ + public function setContent($content) + { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) { + throw new \UnexpectedValueException('The Response content must be a string or object implementing __toString(), "'.gettype($content).'" given.'); + } + + $this->content = (string) $content; + } + + /** + * Gets the current response content + * + * @return string Content + * + * @api + */ + public function getContent() + { + return $this->content; + } + + /** + * Sets the HTTP protocol version (1.0 or 1.1). + * + * @param string $version The HTTP protocol version + * + * @api + */ + public function setProtocolVersion($version) + { + $this->version = $version; + } + + /** + * Gets the HTTP protocol version. + * + * @return string The HTTP protocol version + * + * @api + */ + public function getProtocolVersion() + { + return $this->version; + } + + /** + * Sets response status code. + * + * @param integer $code HTTP status code + * @param string $text HTTP status text + * + * @throws \InvalidArgumentException When the HTTP status code is not valid + * + * @api + */ + public function setStatusCode($code, $text = null) + { + $this->statusCode = (int) $code; + if ($this->isInvalid()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code)); + } + + $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text); + } + + /** + * Retrieves status code for the current web response. + * + * @return string Status code + * + * @api + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * Sets response charset. + * + * @param string $charset Character set + * + * @api + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Retrieves the response charset. + * + * @return string Character set + * + * @api + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Returns true if the response is worth caching under any circumstance. + * + * Responses marked "private" with an explicit Cache-Control directive are + * considered uncacheable. + * + * Responses with neither a freshness lifetime (Expires, max-age) nor cache + * validator (Last-Modified, ETag) are considered uncacheable. + * + * @return Boolean true if the response is worth caching, false otherwise + * + * @api + */ + public function isCacheable() + { + if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) { + return false; + } + + if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) { + return false; + } + + return $this->isValidateable() || $this->isFresh(); + } + + /** + * Returns true if the response is "fresh". + * + * Fresh responses may be served from cache without any interaction with the + * origin. A response is considered fresh when it includes a Cache-Control/max-age + * indicator or Expiration header and the calculated age is less than the freshness lifetime. + * + * @return Boolean true if the response is fresh, false otherwise + * + * @api + */ + public function isFresh() + { + return $this->getTtl() > 0; + } + + /** + * Returns true if the response includes headers that can be used to validate + * the response with the origin server using a conditional GET request. + * + * @return Boolean true if the response is validateable, false otherwise + * + * @api + */ + public function isValidateable() + { + return $this->headers->has('Last-Modified') || $this->headers->has('ETag'); + } + + /** + * Marks the response as "private". + * + * It makes the response ineligible for serving other clients. + * + * @api + */ + public function setPrivate() + { + $this->headers->removeCacheControlDirective('public'); + $this->headers->addCacheControlDirective('private'); + } + + /** + * Marks the response as "public". + * + * It makes the response eligible for serving other clients. + * + * @api + */ + public function setPublic() + { + $this->headers->addCacheControlDirective('public'); + $this->headers->removeCacheControlDirective('private'); + } + + /** + * Returns true if the response must be revalidated by caches. + * + * This method indicates that the response must not be served stale by a + * cache in any circumstance without first revalidating with the origin. + * When present, the TTL of the response should not be overridden to be + * greater than the value provided by the origin. + * + * @return Boolean true if the response must be revalidated by a cache, false otherwise + * + * @api + */ + public function mustRevalidate() + { + return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->has('must-proxy-revalidate'); + } + + /** + * Returns the Date header as a DateTime instance. + * + * @return \DateTime A \DateTime instance + * + * @throws \RuntimeException when the header is not parseable + * + * @api + */ + public function getDate() + { + return $this->headers->getDate('Date'); + } + + /** + * Sets the Date header. + * + * @param \DateTime $date A \DateTime instance + * + * @api + */ + public function setDate(\DateTime $date) + { + $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT'); + } + + /** + * Returns the age of the response. + * + * @return integer The age of the response in seconds + */ + public function getAge() + { + if ($age = $this->headers->get('Age')) { + return $age; + } + + return max(time() - $this->getDate()->format('U'), 0); + } + + /** + * Marks the response stale by setting the Age header to be equal to the maximum age of the response. + * + * @api + */ + public function expire() + { + if ($this->isFresh()) { + $this->headers->set('Age', $this->getMaxAge()); + } + } + + /** + * Returns the value of the Expires header as a DateTime instance. + * + * @return \DateTime A DateTime instance + * + * @api + */ + public function getExpires() + { + return $this->headers->getDate('Expires'); + } + + /** + * Sets the Expires HTTP header with a \DateTime instance. + * + * If passed a null value, it removes the header. + * + * @param \DateTime $date A \DateTime instance + * + * @api + */ + public function setExpires(\DateTime $date = null) + { + if (null === $date) { + $this->headers->remove('Expires'); + } else { + $date = clone $date; + $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT'); + } + } + + /** + * Sets the number of seconds after the time specified in the response's Date + * header when the the response should no longer be considered fresh. + * + * First, it checks for a s-maxage directive, then a max-age directive, and then it falls + * back on an expires header. It returns null when no maximum age can be established. + * + * @return integer|null Number of seconds + * + * @api + */ + public function getMaxAge() + { + if ($age = $this->headers->getCacheControlDirective('s-maxage')) { + return $age; + } + + if ($age = $this->headers->getCacheControlDirective('max-age')) { + return $age; + } + + if (null !== $this->getExpires()) { + return $this->getExpires()->format('U') - $this->getDate()->format('U'); + } + + return null; + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh. + * + * This methods sets the Cache-Control max-age directive. + * + * @param integer $value A number of seconds + * + * @api + */ + public function setMaxAge($value) + { + $this->headers->addCacheControlDirective('max-age', $value); + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. + * + * This methods sets the Cache-Control s-maxage directive. + * + * @param integer $value A number of seconds + * + * @api + */ + public function setSharedMaxAge($value) + { + $this->setPublic(); + $this->headers->addCacheControlDirective('s-maxage', $value); + } + + /** + * Returns the response's time-to-live in seconds. + * + * It returns null when no freshness information is present in the response. + * + * When the responses TTL is <= 0, the response may not be served from cache without first + * revalidating with the origin. + * + * @return integer The TTL in seconds + * + * @api + */ + public function getTtl() + { + if ($maxAge = $this->getMaxAge()) { + return $maxAge - $this->getAge(); + } + + return null; + } + + /** + * Sets the response's time-to-live for shared caches. + * + * This method adjusts the Cache-Control/s-maxage directive. + * + * @param integer $seconds The number of seconds + * + * @api + */ + public function setTtl($seconds) + { + $this->setSharedMaxAge($this->getAge() + $seconds); + } + + /** + * Sets the response's time-to-live for private/client caches. + * + * This method adjusts the Cache-Control/max-age directive. + * + * @param integer $seconds The number of seconds + * + * @api + */ + public function setClientTtl($seconds) + { + $this->setMaxAge($this->getAge() + $seconds); + } + + /** + * Returns the Last-Modified HTTP header as a DateTime instance. + * + * @return \DateTime A DateTime instance + * + * @api + */ + public function getLastModified() + { + return $this->headers->getDate('Last-Modified'); + } + + /** + * Sets the Last-Modified HTTP header with a \DateTime instance. + * + * If passed a null value, it removes the header. + * + * @param \DateTime $date A \DateTime instance + * + * @api + */ + public function setLastModified(\DateTime $date = null) + { + if (null === $date) { + $this->headers->remove('Last-Modified'); + } else { + $date = clone $date; + $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); + } + } + + /** + * Returns the literal value of ETag HTTP header. + * + * @return string The ETag HTTP header + * + * @api + */ + public function getEtag() + { + return $this->headers->get('ETag'); + } + + /** + * Sets the ETag value. + * + * @param string $etag The ETag unique identifier + * @param Boolean $weak Whether you want a weak ETag or not + * + * @api + */ + public function setEtag($etag = null, $weak = false) + { + if (null === $etag) { + $this->headers->remove('Etag'); + } else { + if (0 !== strpos($etag, '"')) { + $etag = '"'.$etag.'"'; + } + + $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag); + } + } + + /** + * Sets Response cache headers (validation and/or expiration). + * + * Available options are: etag, last_modified, max_age, s_maxage, private, and public. + * + * @param array $options An array of cache options + * + * @api + */ + public function setCache(array $options) + { + if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) { + throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_keys($diff)))); + } + + if (isset($options['etag'])) { + $this->setEtag($options['etag']); + } + + if (isset($options['last_modified'])) { + $this->setLastModified($options['last_modified']); + } + + if (isset($options['max_age'])) { + $this->setMaxAge($options['max_age']); + } + + if (isset($options['s_maxage'])) { + $this->setSharedMaxAge($options['s_maxage']); + } + + if (isset($options['public'])) { + if ($options['public']) { + $this->setPublic(); + } else { + $this->setPrivate(); + } + } + + if (isset($options['private'])) { + if ($options['private']) { + $this->setPrivate(); + } else { + $this->setPublic(); + } + } + } + + /** + * Modifies the response so that it conforms to the rules defined for a 304 status code. + * + * This sets the status, removes the body, and discards any headers + * that MUST NOT be included in 304 responses. + * + * @see http://tools.ietf.org/html/rfc2616#section-10.3.5 + * + * @api + */ + public function setNotModified() + { + $this->setStatusCode(304); + $this->setContent(null); + + // remove headers that MUST NOT be included with 304 Not Modified responses + foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) { + $this->headers->remove($header); + } + } + + /** + * Returns true if the response includes a Vary header. + * + * @return true if the response includes a Vary header, false otherwise + * + * @api + */ + public function hasVary() + { + return (Boolean) $this->headers->get('Vary'); + } + + /** + * Returns an array of header names given in the Vary header. + * + * @return array An array of Vary names + * + * @api + */ + public function getVary() + { + if (!$vary = $this->headers->get('Vary')) { + return array(); + } + + return is_array($vary) ? $vary : preg_split('/[\s,]+/', $vary); + } + + /** + * Sets the Vary header. + * + * @param string|array $headers + * @param Boolean $replace Whether to replace the actual value of not (true by default) + * + * @api + */ + public function setVary($headers, $replace = true) + { + $this->headers->set('Vary', $headers, $replace); + } + + /** + * Determines if the Response validators (ETag, Last-Modified) matches + * a conditional value specified in the Request. + * + * If the Response is not modified, it sets the status code to 304 and + * remove the actual content by calling the setNotModified() method. + * + * @param Request $request A Request instance + * + * @return Boolean true if the Response validators matches the Request, false otherwise + * + * @api + */ + public function isNotModified(Request $request) + { + $lastModified = $request->headers->get('If-Modified-Since'); + $notModified = false; + if ($etags = $request->getEtags()) { + $notModified = (in_array($this->getEtag(), $etags) || in_array('*', $etags)) && (!$lastModified || $this->headers->get('Last-Modified') == $lastModified); + } elseif ($lastModified) { + $notModified = $lastModified == $this->headers->get('Last-Modified'); + } + + if ($notModified) { + $this->setNotModified(); + } + + return $notModified; + } + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + /** + * @api + */ + public function isInvalid() + { + return $this->statusCode < 100 || $this->statusCode >= 600; + } + + /** + * @api + */ + public function isInformational() + { + return $this->statusCode >= 100 && $this->statusCode < 200; + } + + /** + * @api + */ + public function isSuccessful() + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } + + /** + * @api + */ + public function isRedirection() + { + return $this->statusCode >= 300 && $this->statusCode < 400; + } + + /** + * @api + */ + public function isClientError() + { + return $this->statusCode >= 400 && $this->statusCode < 500; + } + + /** + * @api + */ + public function isServerError() + { + return $this->statusCode >= 500 && $this->statusCode < 600; + } + + /** + * @api + */ + public function isOk() + { + return 200 === $this->statusCode; + } + + /** + * @api + */ + public function isForbidden() + { + return 403 === $this->statusCode; + } + + /** + * @api + */ + public function isNotFound() + { + return 404 === $this->statusCode; + } + + /** + * @api + */ + public function isRedirect($location = null) + { + return in_array($this->statusCode, array(201, 301, 302, 303, 307)) && (null === $location ?: $location == $this->headers->get('Location')); + } + + /** + * @api + */ + public function isEmpty() + { + return in_array($this->statusCode, array(201, 204, 304)); + } +} diff --git a/includes/Symfony/Component/HttpFoundation/ResponseHeaderBag.php b/includes/Symfony/Component/HttpFoundation/ResponseHeaderBag.php new file mode 100644 index 0000000..f243dcc --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/ResponseHeaderBag.php @@ -0,0 +1,238 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ResponseHeaderBag is a container for Response HTTP headers. + * + * @author Fabien Potencier + * + * @api + */ +class ResponseHeaderBag extends HeaderBag +{ + const COOKIES_FLAT = 'flat'; + const COOKIES_ARRAY = 'array'; + + protected $computedCacheControl = array(); + protected $cookies = array(); + + /** + * Constructor. + * + * @param array $headers An array of HTTP headers + * + * @api + */ + public function __construct(array $headers = array()) + { + parent::__construct($headers); + + if (!isset($this->headers['cache-control'])) { + $this->set('cache-control', ''); + } + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + $cookies = ''; + foreach ($this->getCookies() as $cookie) { + $cookies .= 'Set-Cookie: '.$cookie."\r\n"; + } + + return parent::__toString().$cookies; + } + + /** + * {@inheritdoc} + * + * @api + */ + public function replace(array $headers = array()) + { + parent::replace($headers); + + if (!isset($this->headers['cache-control'])) { + $this->set('cache-control', ''); + } + } + + /** + * {@inheritdoc} + * + * @api + */ + public function set($key, $values, $replace = true) + { + parent::set($key, $values, $replace); + + // ensure the cache-control header has sensible defaults + if (in_array(strtr(strtolower($key), '_', '-'), array('cache-control', 'etag', 'last-modified', 'expires'))) { + $computed = $this->computeCacheControlValue(); + $this->headers['cache-control'] = array($computed); + $this->computedCacheControl = $this->parseCacheControl($computed); + } + } + + /** + * {@inheritdoc} + * + * @api + */ + public function remove($key) + { + parent::remove($key); + + if ('cache-control' === strtr(strtolower($key), '_', '-')) { + $this->computedCacheControl = array(); + } + } + + /** + * {@inheritdoc} + */ + public function hasCacheControlDirective($key) + { + return array_key_exists($key, $this->computedCacheControl); + } + + /** + * {@inheritdoc} + */ + public function getCacheControlDirective($key) + { + return array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null; + } + + /** + * Sets a cookie. + * + * @param Cookie $cookie + * @return void + * + * @api + */ + public function setCookie(Cookie $cookie) + { + $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; + } + + /** + * Removes a cookie from the array, but does not unset it in the browser + * + * @param string $name + * @param string $path + * @param string $domain + * @return void + * + * @api + */ + public function removeCookie($name, $path = '/', $domain = null) + { + if (null === $path) { + $path = '/'; + } + + unset($this->cookies[$domain][$path][$name]); + + if (empty($this->cookies[$domain][$path])) { + unset($this->cookies[$domain][$path]); + + if (empty($this->cookies[$domain])) { + unset($this->cookies[$domain]); + } + } + } + + /** + * Returns an array with all cookies + * + * @param string $format + * + * @throws \InvalidArgumentException When the $format is invalid + * + * @return array + * + * @api + */ + public function getCookies($format = self::COOKIES_FLAT) + { + if (!in_array($format, array(self::COOKIES_FLAT, self::COOKIES_ARRAY))) { + throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', array(self::COOKIES_FLAT, self::COOKIES_ARRAY)))); + } + + if (self::COOKIES_ARRAY === $format) { + return $this->cookies; + } + + $flattenedCookies = array(); + foreach ($this->cookies as $path) { + foreach ($path as $cookies) { + foreach ($cookies as $cookie) { + $flattenedCookies[] = $cookie; + } + } + } + + return $flattenedCookies; + } + + /** + * Clears a cookie in the browser + * + * @param string $name + * @param string $path + * @param string $domain + * @return void + * + * @api + */ + public function clearCookie($name, $path = '/', $domain = null) + { + $this->setCookie(new Cookie($name, null, 1, $path, $domain)); + } + + /** + * Returns the calculated value of the cache-control header. + * + * This considers several other headers and calculates or modifies the + * cache-control header to a sensible, conservative value. + * + * @return string + */ + protected function computeCacheControlValue() + { + if (!$this->cacheControl && !$this->has('ETag') && !$this->has('Last-Modified') && !$this->has('Expires')) { + return 'no-cache'; + } + + if (!$this->cacheControl) { + // conservative by default + return 'private, must-revalidate'; + } + + $header = $this->getCacheControlHeader(); + if (isset($this->cacheControl['public']) || isset($this->cacheControl['private'])) { + return $header; + } + + // public if s-maxage is defined, private otherwise + if (!isset($this->cacheControl['s-maxage'])) { + return $header.', private'; + } + + return $header; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/ServerBag.php b/includes/Symfony/Component/HttpFoundation/ServerBag.php new file mode 100644 index 0000000..02db3b1 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/ServerBag.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ServerBag is a container for HTTP headers from the $_SERVER variable. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + */ +class ServerBag extends ParameterBag +{ + public function getHeaders() + { + $headers = array(); + foreach ($this->parameters as $key => $value) { + if ('HTTP_' === substr($key, 0, 5)) { + $headers[substr($key, 5)] = $value; + } + // CONTENT_* are not prefixed with HTTP_ + elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) { + $headers[$key] = $this->parameters[$key]; + } + } + + // PHP_AUTH_USER/PHP_AUTH_PW + if (isset($this->parameters['PHP_AUTH_USER'])) { + $pass = isset($this->parameters['PHP_AUTH_PW']) ? $this->parameters['PHP_AUTH_PW'] : ''; + $headers['AUTHORIZATION'] = 'Basic '.base64_encode($this->parameters['PHP_AUTH_USER'].':'.$pass); + } + + return $headers; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/Session.php b/includes/Symfony/Component/HttpFoundation/Session.php new file mode 100644 index 0000000..a835ef7 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/Session.php @@ -0,0 +1,405 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\SessionStorage\SessionStorageInterface; + +/** + * Session. + * + * @author Fabien Potencier + * + * @api + */ +class Session implements \Serializable +{ + protected $storage; + protected $started; + protected $attributes; + protected $flashes; + protected $oldFlashes; + protected $locale; + protected $defaultLocale; + protected $closed; + + /** + * Constructor. + * + * @param SessionStorageInterface $storage A SessionStorageInterface instance + * @param string $defaultLocale The default locale + */ + public function __construct(SessionStorageInterface $storage, $defaultLocale = 'en') + { + $this->storage = $storage; + $this->defaultLocale = $defaultLocale; + $this->locale = $defaultLocale; + $this->flashes = array(); + $this->oldFlashes = array(); + $this->attributes = array(); + $this->setPhpDefaultLocale($this->defaultLocale); + $this->started = false; + $this->closed = false; + } + + /** + * Starts the session storage. + * + * @api + */ + public function start() + { + if (true === $this->started) { + return; + } + + $this->storage->start(); + + $attributes = $this->storage->read('_symfony2'); + + if (isset($attributes['attributes'])) { + $this->attributes = $attributes['attributes']; + $this->flashes = $attributes['flashes']; + $this->locale = $attributes['locale']; + $this->setPhpDefaultLocale($this->locale); + + // flag current flash messages to be removed at shutdown + $this->oldFlashes = $this->flashes; + } + + $this->started = true; + } + + /** + * Checks if an attribute is defined. + * + * @param string $name The attribute name + * + * @return Boolean true if the attribute is defined, false otherwise + * + * @api + */ + public function has($name) + { + return array_key_exists($name, $this->attributes); + } + + /** + * Returns an attribute. + * + * @param string $name The attribute name + * @param mixed $default The default value + * + * @return mixed + * + * @api + */ + public function get($name, $default = null) + { + return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + } + + /** + * Sets an attribute. + * + * @param string $name + * @param mixed $value + * + * @api + */ + public function set($name, $value) + { + if (false === $this->started) { + $this->start(); + } + + $this->attributes[$name] = $value; + } + + /** + * Returns attributes. + * + * @return array Attributes + * + * @api + */ + public function all() + { + return $this->attributes; + } + + /** + * Sets attributes. + * + * @param array $attributes Attributes + * + * @api + */ + public function replace(array $attributes) + { + if (false === $this->started) { + $this->start(); + } + + $this->attributes = $attributes; + } + + /** + * Removes an attribute. + * + * @param string $name + * + * @api + */ + public function remove($name) + { + if (false === $this->started) { + $this->start(); + } + + if (array_key_exists($name, $this->attributes)) { + unset($this->attributes[$name]); + } + } + + /** + * Clears all attributes. + * + * @api + */ + public function clear() + { + if (false === $this->started) { + $this->start(); + } + + $this->attributes = array(); + $this->flashes = array(); + $this->setPhpDefaultLocale($this->locale = $this->defaultLocale); + } + + /** + * Invalidates the current session. + * + * @api + */ + public function invalidate() + { + $this->clear(); + $this->storage->regenerate(true); + } + + /** + * Migrates the current session to a new session id while maintaining all + * session attributes. + * + * @api + */ + public function migrate() + { + $this->storage->regenerate(); + } + + /** + * Returns the session ID + * + * @return mixed The session ID + * + * @api + */ + public function getId() + { + if (false === $this->started) { + $this->start(); + } + + return $this->storage->getId(); + } + + /** + * Returns the locale + * + * @return string + */ + public function getLocale() + { + return $this->locale; + } + + /** + * Sets the locale. + * + * @param string $locale + */ + public function setLocale($locale) + { + if (false === $this->started) { + $this->start(); + } + + $this->setPhpDefaultLocale($this->locale = $locale); + } + + /** + * Gets the flash messages. + * + * @return array + */ + public function getFlashes() + { + return $this->flashes; + } + + /** + * Sets the flash messages. + * + * @param array $values + */ + public function setFlashes($values) + { + if (false === $this->started) { + $this->start(); + } + + $this->flashes = $values; + $this->oldFlashes = array(); + } + + /** + * Gets a flash message. + * + * @param string $name + * @param string|null $default + * + * @return string + */ + public function getFlash($name, $default = null) + { + return array_key_exists($name, $this->flashes) ? $this->flashes[$name] : $default; + } + + /** + * Sets a flash message. + * + * @param string $name + * @param string $value + */ + public function setFlash($name, $value) + { + if (false === $this->started) { + $this->start(); + } + + $this->flashes[$name] = $value; + unset($this->oldFlashes[$name]); + } + + /** + * Checks whether a flash message exists. + * + * @param string $name + * + * @return Boolean + */ + public function hasFlash($name) + { + if (false === $this->started) { + $this->start(); + } + + return array_key_exists($name, $this->flashes); + } + + /** + * Removes a flash message. + * + * @param string $name + */ + public function removeFlash($name) + { + if (false === $this->started) { + $this->start(); + } + + unset($this->flashes[$name]); + } + + /** + * Removes the flash messages. + */ + public function clearFlashes() + { + if (false === $this->started) { + $this->start(); + } + + $this->flashes = array(); + $this->oldFlashes = array(); + } + + public function save() + { + if (false === $this->started) { + $this->start(); + } + + $this->flashes = array_diff_key($this->flashes, $this->oldFlashes); + + $this->storage->write('_symfony2', array( + 'attributes' => $this->attributes, + 'flashes' => $this->flashes, + 'locale' => $this->locale, + )); + } + + /** + * This method should be called when you don't want the session to be saved + * when the Session object is garbaged collected (useful for instance when + * you want to simulate the interaction of several users/sessions in a single + * PHP process). + */ + public function close() + { + $this->closed = true; + } + + public function __destruct() + { + if (true === $this->started && !$this->closed) { + $this->save(); + } + } + + public function serialize() + { + return serialize(array($this->storage, $this->defaultLocale)); + } + + public function unserialize($serialized) + { + list($this->storage, $this->defaultLocale) = unserialize($serialized); + $this->attributes = array(); + $this->started = false; + } + + private function setPhpDefaultLocale($locale) + { + // if either the class Locale doesn't exist, or an exception is thrown when + // setting the default locale, the intl module is not installed, and + // the call can be ignored: + try { + if (class_exists('Locale', false)) { + \Locale::setDefault($locale); + } + } catch (\Exception $e) { + } + } +} diff --git a/includes/Symfony/Component/HttpFoundation/SessionStorage/ArraySessionStorage.php b/includes/Symfony/Component/HttpFoundation/SessionStorage/ArraySessionStorage.php new file mode 100644 index 0000000..62aac40 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/SessionStorage/ArraySessionStorage.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\SessionStorage; + +/** + * ArraySessionStorage mocks the session for unit tests. + * + * When doing functional testing, you should use FilesystemSessionStorage instead. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + */ + +class ArraySessionStorage implements SessionStorageInterface +{ + private $data = array(); + + public function read($key, $default = null) + { + return array_key_exists($key, $this->data) ? $this->data[$key] : $default; + } + + public function regenerate($destroy = false) + { + if ($destroy) { + $this->data = array(); + } + + return true; + } + + public function remove($key) + { + unset($this->data[$key]); + } + + public function start() + { + } + + public function getId() + { + } + + public function write($key, $data) + { + $this->data[$key] = $data; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/SessionStorage/FilesystemSessionStorage.php b/includes/Symfony/Component/HttpFoundation/SessionStorage/FilesystemSessionStorage.php new file mode 100644 index 0000000..87abd01 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/SessionStorage/FilesystemSessionStorage.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\SessionStorage; + +/** + * FilesystemSessionStorage simulates sessions for functional tests. + * + * This storage does not start the session (session_start()) + * as it is not "available" when running tests on the command line. + * + * @author Fabien Potencier + * + * @api + */ +class FilesystemSessionStorage extends NativeSessionStorage +{ + private $path; + private $data; + private $started; + + /** + * Constructor. + */ + public function __construct($path, array $options = array()) + { + $this->path = $path; + $this->started = false; + + parent::__construct($options); + } + + /** + * Starts the session. + * + * @api + */ + public function start() + { + if ($this->started) { + return; + } + + session_set_cookie_params( + $this->options['lifetime'], + $this->options['path'], + $this->options['domain'], + $this->options['secure'], + $this->options['httponly'] + ); + + if (!ini_get('session.use_cookies') && isset($this->options['id']) && $this->options['id'] && $this->options['id'] != session_id()) { + session_id($this->options['id']); + } + + if (!session_id()) { + session_id(hash('md5', uniqid(mt_rand(), true))); + } + + $file = $this->path.'/'.session_id().'.session'; + + $this->data = file_exists($file) ? unserialize(file_get_contents($file)) : array(); + $this->started = true; + } + + /** + * Returns the session ID + * + * @return mixed The session ID + * + * @throws \RuntimeException If the session was not started yet + * + * @api + */ + public function getId() + { + if (!$this->started) { + throw new \RuntimeException('The session must be started before reading its ID'); + } + + return session_id(); + } + + /** + * Reads data from this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * + * @return mixed Data associated with the key + * + * @throws \RuntimeException If an error occurs while reading data from this storage + * + * @api + */ + public function read($key, $default = null) + { + return array_key_exists($key, $this->data) ? $this->data[$key] : $default; + } + + /** + * Removes data from this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * + * @return mixed Data associated with the key + * + * @throws \RuntimeException If an error occurs while removing data from this storage + * + * @api + */ + public function remove($key) + { + $retval = $this->data[$key]; + + unset($this->data[$key]); + + return $retval; + } + + /** + * Writes data to this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * @param mixed $data Data associated with your key + * + * @throws \RuntimeException If an error occurs while writing to this storage + * + * @api + */ + public function write($key, $data) + { + $this->data[$key] = $data; + + if (!is_dir($this->path)) { + mkdir($this->path, 0777, true); + } + + file_put_contents($this->path.'/'.session_id().'.session', serialize($this->data)); + } + + /** + * Regenerates id that represents this storage. + * + * @param Boolean $destroy Destroy session when regenerating? + * + * @return Boolean True if session regenerated, false if error + * + * @throws \RuntimeException If an error occurs while regenerating this storage + * + * @api + */ + public function regenerate($destroy = false) + { + if ($destroy) { + $this->data = array(); + } + + return true; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/SessionStorage/NativeSessionStorage.php b/includes/Symfony/Component/HttpFoundation/SessionStorage/NativeSessionStorage.php new file mode 100644 index 0000000..b759f74 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/SessionStorage/NativeSessionStorage.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\SessionStorage; + +/** + * NativeSessionStorage. + * + * @author Fabien Potencier + * + * @api + */ +class NativeSessionStorage implements SessionStorageInterface +{ + static protected $sessionIdRegenerated = false; + static protected $sessionStarted = false; + + protected $options; + + /** + * Available options: + * + * * name: The cookie name (null [omitted] by default) + * * id: The session id (null [omitted] by default) + * * lifetime: Cookie lifetime + * * path: Cookie path + * * domain: Cookie domain + * * secure: Cookie secure + * * httponly: Cookie http only + * + * The default values for most options are those returned by the session_get_cookie_params() function + * + * @param array $options An associative array of session options + */ + public function __construct(array $options = array()) + { + $cookieDefaults = session_get_cookie_params(); + + $this->options = array_merge(array( + 'lifetime' => $cookieDefaults['lifetime'], + 'path' => $cookieDefaults['path'], + 'domain' => $cookieDefaults['domain'], + 'secure' => $cookieDefaults['secure'], + 'httponly' => isset($cookieDefaults['httponly']) ? $cookieDefaults['httponly'] : false, + ), $options); + + // Skip setting new session name if user don't want it + if (isset($this->options['name'])) { + session_name($this->options['name']); + } + } + + /** + * Starts the session. + * + * @api + */ + public function start() + { + if (self::$sessionStarted) { + return; + } + + session_set_cookie_params( + $this->options['lifetime'], + $this->options['path'], + $this->options['domain'], + $this->options['secure'], + $this->options['httponly'] + ); + + // disable native cache limiter as this is managed by HeaderBag directly + session_cache_limiter(false); + + if (!ini_get('session.use_cookies') && isset($this->options['id']) && $this->options['id'] && $this->options['id'] != session_id()) { + session_id($this->options['id']); + } + + session_start(); + + self::$sessionStarted = true; + } + + /** + * {@inheritDoc} + * + * @api + */ + public function getId() + { + if (!self::$sessionStarted) { + throw new \RuntimeException('The session must be started before reading its ID'); + } + + return session_id(); + } + + /** + * Reads data from this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * @param string $default Default value + * + * @return mixed Data associated with the key + * + * @api + */ + public function read($key, $default = null) + { + return array_key_exists($key, $_SESSION) ? $_SESSION[$key] : $default; + } + + /** + * Removes data from this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * + * @return mixed Data associated with the key + * + * @api + */ + public function remove($key) + { + $retval = null; + + if (isset($_SESSION[$key])) { + $retval = $_SESSION[$key]; + unset($_SESSION[$key]); + } + + return $retval; + } + + /** + * Writes data to this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * @param mixed $data Data associated with your key + * + * @api + */ + public function write($key, $data) + { + $_SESSION[$key] = $data; + } + + /** + * Regenerates id that represents this storage. + * + * @param Boolean $destroy Destroy session when regenerating? + * + * @return Boolean True if session regenerated, false if error + * + * @api + */ + public function regenerate($destroy = false) + { + if (self::$sessionIdRegenerated) { + return; + } + + session_regenerate_id($destroy); + + self::$sessionIdRegenerated = true; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/SessionStorage/PdoSessionStorage.php b/includes/Symfony/Component/HttpFoundation/SessionStorage/PdoSessionStorage.php new file mode 100644 index 0000000..78f90b8 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/SessionStorage/PdoSessionStorage.php @@ -0,0 +1,263 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\SessionStorage; + +/** + * PdoSessionStorage. + * + * @author Fabien Potencier + * @author Michael Williams + */ +class PdoSessionStorage extends NativeSessionStorage +{ + private $db; + private $dbOptions; + + /** + * Constructor. + * + * @param \PDO $db A PDO instance + * @param array $options An associative array of session options + * @param array $dbOptions An associative array of DB options + * + * @throws \InvalidArgumentException When "db_table" option is not provided + * + * @see NativeSessionStorage::__construct() + */ + public function __construct(\PDO $db, array $options = array(), array $dbOptions = array()) + { + if (!array_key_exists('db_table', $dbOptions)) { + throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.'); + } + + $this->db = $db; + $this->dbOptions = array_merge(array( + 'db_id_col' => 'sess_id', + 'db_data_col' => 'sess_data', + 'db_time_col' => 'sess_time', + ), $dbOptions); + + parent::__construct($options); + } + + /** + * Starts the session. + */ + public function start() + { + if (self::$sessionStarted) { + return; + } + + // use this object as the session handler + session_set_save_handler( + array($this, 'sessionOpen'), + array($this, 'sessionClose'), + array($this, 'sessionRead'), + array($this, 'sessionWrite'), + array($this, 'sessionDestroy'), + array($this, 'sessionGC') + ); + + parent::start(); + } + + /** + * Opens a session. + * + * @param string $path (ignored) + * @param string $name (ignored) + * + * @return Boolean true, if the session was opened, otherwise an exception is thrown + */ + public function sessionOpen($path = null, $name = null) + { + return true; + } + + /** + * Closes a session. + * + * @return Boolean true, if the session was closed, otherwise false + */ + public function sessionClose() + { + // do nothing + return true; + } + + /** + * Destroys a session. + * + * @param string $id A session ID + * + * @return Boolean true, if the session was destroyed, otherwise an exception is thrown + * + * @throws \RuntimeException If the session cannot be destroyed + */ + public function sessionDestroy($id) + { + // get table/column + $dbTable = $this->dbOptions['db_table']; + $dbIdCol = $this->dbOptions['db_id_col']; + + // delete the record associated with this id + $sql = "DELETE FROM $dbTable WHERE $dbIdCol = :id"; + + try { + $stmt = $this->db->prepare($sql); + $stmt->bindParam(':id', $id, \PDO::PARAM_STR); + $stmt->execute(); + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); + } + + return true; + } + + /** + * Cleans up old sessions. + * + * @param int $lifetime The lifetime of a session + * + * @return Boolean true, if old sessions have been cleaned, otherwise an exception is thrown + * + * @throws \RuntimeException If any old sessions cannot be cleaned + */ + public function sessionGC($lifetime) + { + // get table/column + $dbTable = $this->dbOptions['db_table']; + $dbTimeCol = $this->dbOptions['db_time_col']; + + // delete the record associated with this id + $sql = "DELETE FROM $dbTable WHERE $dbTimeCol < (:time - $lifetime)"; + + try { + $this->db->query($sql); + $stmt = $this->db->prepare($sql); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); + } + + return true; + } + + /** + * Reads a session. + * + * @param string $id A session ID + * + * @return string The session data if the session was read or created, otherwise an exception is thrown + * + * @throws \RuntimeException If the session cannot be read + */ + public function sessionRead($id) + { + // get table/columns + $dbTable = $this->dbOptions['db_table']; + $dbDataCol = $this->dbOptions['db_data_col']; + $dbIdCol = $this->dbOptions['db_id_col']; + + try { + $sql = "SELECT $dbDataCol FROM $dbTable WHERE $dbIdCol = :id"; + + $stmt = $this->db->prepare($sql); + $stmt->bindParam(':id', $id, \PDO::PARAM_STR, 255); + + $stmt->execute(); + // it is recommended to use fetchAll so that PDO can close the DB cursor + // we anyway expect either no rows, or one row with one column. fetchColumn, seems to be buggy #4777 + $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); + + if (count($sessionRows) == 1) { + return $sessionRows[0][0]; + } + + // session does not exist, create it + $this->createNewSession($id); + + return ''; + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); + } + } + + /** + * Writes session data. + * + * @param string $id A session ID + * @param string $data A serialized chunk of session data + * + * @return Boolean true, if the session was written, otherwise an exception is thrown + * + * @throws \RuntimeException If the session data cannot be written + */ + public function sessionWrite($id, $data) + { + // get table/column + $dbTable = $this->dbOptions['db_table']; + $dbDataCol = $this->dbOptions['db_data_col']; + $dbIdCol = $this->dbOptions['db_id_col']; + $dbTimeCol = $this->dbOptions['db_time_col']; + + $sql = ('mysql' === $this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) + ? "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol) VALUES (:id, :data, :time) " + ."ON DUPLICATE KEY UPDATE $dbDataCol = VALUES($dbDataCol), $dbTimeCol = CASE WHEN $dbTimeCol = :time THEN (VALUES($dbTimeCol) + 1) ELSE VALUES($dbTimeCol) END" + : "UPDATE $dbTable SET $dbDataCol = :data, $dbTimeCol = :time WHERE $dbIdCol = :id"; + + try { + $stmt = $this->db->prepare($sql); + $stmt->bindParam(':id', $id, \PDO::PARAM_STR); + $stmt->bindParam(':data', $data, \PDO::PARAM_STR); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); + + if (!$stmt->rowCount()) { + // No session exists in the database to update. This happens when we have called + // session_regenerate_id() + $this->createNewSession($id, $data); + } + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); + } + + return true; + } + + /** + * Creates a new session with the given $id and $data + * + * @param string $id + * @param string $data + */ + private function createNewSession($id, $data = '') + { + // get table/column + $dbTable = $this->dbOptions['db_table']; + $dbDataCol = $this->dbOptions['db_data_col']; + $dbIdCol = $this->dbOptions['db_id_col']; + $dbTimeCol = $this->dbOptions['db_time_col']; + + $sql = "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol) VALUES (:id, :data, :time)"; + + $stmt = $this->db->prepare($sql); + $stmt->bindParam(':id', $id, \PDO::PARAM_STR); + $stmt->bindParam(':data', $data, \PDO::PARAM_STR); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); + + return true; + } +} diff --git a/includes/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.php b/includes/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.php new file mode 100644 index 0000000..b61a255 --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\SessionStorage; + +/** + * SessionStorageInterface. + * + * @author Fabien Potencier + * + * @api + */ +interface SessionStorageInterface +{ + /** + * Starts the session. + * + * @api + */ + function start(); + + /** + * Returns the session ID + * + * @return mixed The session ID + * + * @throws \RuntimeException If the session was not started yet + * + * @api + */ + function getId(); + + /** + * Reads data from this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * + * @return mixed Data associated with the key + * + * @throws \RuntimeException If an error occurs while reading data from this storage + * + * @api + */ + function read($key); + + /** + * Removes data from this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * + * @return mixed Data associated with the key + * + * @throws \RuntimeException If an error occurs while removing data from this storage + * + * @api + */ + function remove($key); + + /** + * Writes data to this storage. + * + * The preferred format for a key is directory style so naming conflicts can be avoided. + * + * @param string $key A unique key identifying your data + * @param mixed $data Data associated with your key + * + * @throws \RuntimeException If an error occurs while writing to this storage + * + * @api + */ + function write($key, $data); + + /** + * Regenerates id that represents this storage. + * + * @param Boolean $destroy Destroy session when regenerating? + * + * @return Boolean True if session regenerated, false if error + * + * @throws \RuntimeException If an error occurs while regenerating this storage + * + * @api + */ + function regenerate($destroy = false); +} diff --git a/includes/Symfony/Component/HttpFoundation/composer.json b/includes/Symfony/Component/HttpFoundation/composer.json new file mode 100644 index 0000000..b3cfbbd --- /dev/null +++ b/includes/Symfony/Component/HttpFoundation/composer.json @@ -0,0 +1,22 @@ +{ + "name": "symfony/http-foundation", + "type": "library", + "description": "Symfony HttpFoundation Component", + "keywords": [], + "homepage": "http://symfony.com", + "version": "2.0.4", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.2" + } +} diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index a7257c7..ebeb4ab 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -2228,6 +2228,38 @@ function _drupal_bootstrap_configuration() { timer_start('page'); // Initialize the configuration, including variables from settings.php. drupal_settings_initialize(); + + // Hook up the Symfony ClassLoader for loading PSR-0-compatible classes. + require_once(DRUPAL_ROOT . '/includes/Symfony/Component/ClassLoader/UniversalClassLoader.php'); + + // By default, use the UniversalClassLoader which is best for development, + // as it does not break when code is moved on the file system. It is slow, + // however, so for production the APC class loader should be used instead. + // @todo Switch to a cleaner way to switch autoloaders than variable_get(). + switch (variable_get('autoloader_mode', 'default')) { + case 'apc': + if (function_exists('apc_store')) { + require_once(DRUPAL_ROOT . '/includes/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php'); + $loader = new \Symfony\Component\ClassLoader\ApcUniversalClassLoader('drupal.' . $GLOBALS['drupal_hash_salt']); + break; + } + // If APC was not loaded, fall through to the default loader so that + // the site does not fail completely. + case 'dev': + case 'default': + default: + $loader = new \Symfony\Component\ClassLoader\UniversalClassLoader(); + break; + } + + // Register classes with namespaces. + $loader->registerNamespaces(array( + // All Symfony-borrowed code lives in /includes/Symfony. + 'Symfony' => DRUPAL_ROOT . '/includes', + )); + + // Activate the autoloader. + $loader->register(); } /** diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info index c86f440..fab7b5e 100644 --- a/modules/simpletest/simpletest.info +++ b/modules/simpletest/simpletest.info @@ -30,6 +30,7 @@ files[] = tests/path.test files[] = tests/registry.test files[] = tests/schema.test files[] = tests/session.test +files[] = tests/symfony.test files[] = tests/tablesort.test files[] = tests/theme.test files[] = tests/unicode.test diff --git a/modules/simpletest/tests/symfony.test b/modules/simpletest/tests/symfony.test new file mode 100644 index 0000000..8a7ecb9 --- /dev/null +++ b/modules/simpletest/tests/symfony.test @@ -0,0 +1,31 @@ + 'Class loader', + 'description' => 'Confirm that the PSR-0 class loader is connected properly', + 'group' => 'Symfony', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Test that we can lazy-load classes from the Symfony framework. + */ + function testClassesLoad() { + $class_name = 'Symfony\\Component\\HttpFoundation\\Request'; + $this->assertTrue(class_exists($class_name), t('Class !class_name exists', array('!class_name' => $class_name))); + } +}