├── Slim ├── Exception │ ├── NotFoundException.php │ ├── ContainerException.php │ ├── ContainerValueNotFoundException.php │ ├── InvalidMethodException.php │ ├── MethodNotAllowedException.php │ └── SlimException.php ├── Interfaces │ ├── Http │ │ ├── EnvironmentInterface.php │ │ ├── HeadersInterface.php │ │ └── CookiesInterface.php │ ├── CallableResolverInterface.php │ ├── CollectionInterface.php │ ├── RouteGroupInterface.php │ ├── InvocationStrategyInterface.php │ ├── RouterInterface.php │ └── RouteInterface.php ├── Http │ ├── Body.php │ ├── RequestBody.php │ ├── Environment.php │ ├── StatusCode.php │ ├── Cookies.php │ ├── Headers.php │ ├── Message.php │ ├── UploadedFile.php │ ├── Stream.php │ └── Response.php ├── DeferredCallable.php ├── RouteGroup.php ├── Handlers │ ├── Strategies │ │ ├── RequestResponseArgs.php │ │ └── RequestResponse.php │ ├── AbstractHandler.php │ ├── AbstractError.php │ ├── NotFound.php │ ├── NotAllowed.php │ ├── PhpError.php │ └── Error.php ├── CallableResolverAwareTrait.php ├── Routable.php ├── CallableResolver.php ├── MiddlewareAwareTrait.php ├── Collection.php ├── Container.php ├── DefaultServicesProvider.php ├── Route.php ├── Router.php └── App.php ├── CODE_OF_CONDUCT.md ├── LICENSE.md └── composer.json /Slim/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | request = $request; 20 | parent::__construct(sprintf('Unsupported HTTP method "%s" provided', $method)); 21 | } 22 | 23 | public function getRequest() 24 | { 25 | return $this->request; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Slim/Interfaces/CollectionInterface.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 31 | $this->container = $container; 32 | } 33 | 34 | public function __invoke() 35 | { 36 | $callable = $this->resolveCallable($this->callable); 37 | if ($callable instanceof Closure) { 38 | $callable = $callable->bindTo($this->container); 39 | } 40 | 41 | $args = func_get_args(); 42 | 43 | return call_user_func_array($callable, $args); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Slim/Exception/MethodNotAllowedException.php: -------------------------------------------------------------------------------- 1 | allowedMethods = $allowedMethods; 34 | } 35 | 36 | /** 37 | * Get allowed methods 38 | * 39 | * @return string[] 40 | */ 41 | public function getAllowedMethods() 42 | { 43 | return $this->allowedMethods; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Slim/RouteGroup.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 30 | $this->callable = $callable; 31 | } 32 | 33 | /** 34 | * Invoke the group to register any Routable objects within it. 35 | * 36 | * @param App $app The App instance to bind/pass to the group callable 37 | */ 38 | public function __invoke(App $app = null) 39 | { 40 | $callable = $this->resolveCallable($this->callable); 41 | if ($callable instanceof Closure && $app !== null) { 42 | $callable = $callable->bindTo($app); 43 | } 44 | 45 | $callable($app); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Slim/Handlers/Strategies/RequestResponseArgs.php: -------------------------------------------------------------------------------- 1 | container instanceof ContainerInterface) { 39 | return $callable; 40 | } 41 | 42 | /** @var CallableResolverInterface $resolver */ 43 | $resolver = $this->container->get('callableResolver'); 44 | 45 | return $resolver->resolve($callable); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Slim/Handlers/Strategies/RequestResponse.php: -------------------------------------------------------------------------------- 1 | $v) { 38 | $request = $request->withAttribute($k, $v); 39 | } 40 | 41 | return call_user_func($callable, $request, $response, $routeArguments); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Slim/Exception/SlimException.php: -------------------------------------------------------------------------------- 1 | request = $request; 47 | $this->response = $response; 48 | } 49 | 50 | /** 51 | * Get request 52 | * 53 | * @return ServerRequestInterface 54 | */ 55 | public function getRequest() 56 | { 57 | return $this->request; 58 | } 59 | 60 | /** 61 | * Get response 62 | * 63 | * @return ResponseInterface 64 | */ 65 | public function getResponse() 66 | { 67 | return $this->response; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slim/slim", 3 | "type": "library", 4 | "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", 5 | "keywords": ["framework","micro","api","router"], 6 | "homepage": "https://slimframework.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Josh Lockhart", 11 | "email": "hello@joshlockhart.com", 12 | "homepage": "https://joshlockhart.com" 13 | }, 14 | { 15 | "name": "Andrew Smith", 16 | "email": "a.smith@silentworks.co.uk", 17 | "homepage": "http://silentworks.co.uk" 18 | }, 19 | { 20 | "name": "Rob Allen", 21 | "email": "rob@akrabat.com", 22 | "homepage": "http://akrabat.com" 23 | }, 24 | { 25 | "name": "Gabriel Manricks", 26 | "email": "gmanricks@me.com", 27 | "homepage": "http://gabrielmanricks.com" 28 | } 29 | ], 30 | "require": { 31 | "php": ">=5.5.0", 32 | "pimple/pimple": "^3.0", 33 | "psr/http-message": "^1.0", 34 | "nikic/fast-route": "^1.0", 35 | "container-interop/container-interop": "^1.2", 36 | "psr/container": "^1.0" 37 | }, 38 | "require-dev": { 39 | "squizlabs/php_codesniffer": "^2.5", 40 | "phpunit/phpunit": "^4.0" 41 | }, 42 | "provide": { 43 | "psr/http-message-implementation": "1.0" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Slim\\": "Slim" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "files": [ 52 | "tests/Assets/HeaderFunctions.php" 53 | ] 54 | }, 55 | "scripts": { 56 | "test": [ 57 | "phpunit", 58 | "phpcs" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Slim/Handlers/AbstractHandler.php: -------------------------------------------------------------------------------- 1 | getHeaderLine('Accept'); 43 | $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); 44 | 45 | if (count($selectedContentTypes)) { 46 | return current($selectedContentTypes); 47 | } 48 | 49 | // handle +json and +xml specially 50 | if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { 51 | $mediaType = 'application/' . $matches[1]; 52 | if (in_array($mediaType, $this->knownContentTypes)) { 53 | return $mediaType; 54 | } 55 | } 56 | 57 | return 'text/html'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Slim/Http/Environment.php: -------------------------------------------------------------------------------- 1 | 'HTTP/1.1', 44 | 'REQUEST_METHOD' => 'GET', 45 | 'REQUEST_SCHEME' => $defscheme, 46 | 'SCRIPT_NAME' => '', 47 | 'REQUEST_URI' => '', 48 | 'QUERY_STRING' => '', 49 | 'SERVER_NAME' => 'localhost', 50 | 'SERVER_PORT' => $defport, 51 | 'HTTP_HOST' => 'localhost', 52 | 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 53 | 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8', 54 | 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', 55 | 'HTTP_USER_AGENT' => 'Slim Framework', 56 | 'REMOTE_ADDR' => '127.0.0.1', 57 | 'REQUEST_TIME' => time(), 58 | 'REQUEST_TIME_FLOAT' => microtime(true), 59 | ], $userData); 60 | 61 | return new static($data); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Slim/Routable.php: -------------------------------------------------------------------------------- 1 | middleware; 59 | } 60 | 61 | /** 62 | * Get the route pattern 63 | * 64 | * @return string 65 | */ 66 | public function getPattern() 67 | { 68 | return $this->pattern; 69 | } 70 | 71 | /** 72 | * Set container for use with resolveCallable 73 | * 74 | * @param ContainerInterface $container 75 | * 76 | * @return self 77 | */ 78 | public function setContainer(ContainerInterface $container) 79 | { 80 | $this->container = $container; 81 | return $this; 82 | } 83 | 84 | /** 85 | * Prepend middleware to the middleware collection 86 | * 87 | * @param callable|string $callable The callback routine 88 | * 89 | * @return static 90 | */ 91 | public function add($callable) 92 | { 93 | $this->middleware[] = new DeferredCallable($callable, $this->container); 94 | return $this; 95 | } 96 | 97 | /** 98 | * Set the route pattern 99 | * 100 | * @param string $newPattern 101 | */ 102 | public function setPattern($newPattern) 103 | { 104 | $this->pattern = $newPattern; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Slim/Handlers/AbstractError.php: -------------------------------------------------------------------------------- 1 | displayErrorDetails = (bool) $displayErrorDetails; 29 | } 30 | 31 | /** 32 | * Write to the error log if displayErrorDetails is false 33 | * 34 | * @param \Exception|\Throwable $throwable 35 | * 36 | * @return void 37 | */ 38 | protected function writeToErrorLog($throwable) 39 | { 40 | if ($this->displayErrorDetails) { 41 | return; 42 | } 43 | 44 | $message = 'Slim Application Error:' . PHP_EOL; 45 | $message .= $this->renderThrowableAsText($throwable); 46 | while ($throwable = $throwable->getPrevious()) { 47 | $message .= PHP_EOL . 'Previous error:' . PHP_EOL; 48 | $message .= $this->renderThrowableAsText($throwable); 49 | } 50 | 51 | $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; 52 | 53 | $this->logError($message); 54 | } 55 | 56 | /** 57 | * Render error as Text. 58 | * 59 | * @param \Exception|\Throwable $throwable 60 | * 61 | * @return string 62 | */ 63 | protected function renderThrowableAsText($throwable) 64 | { 65 | $text = sprintf('Type: %s' . PHP_EOL, get_class($throwable)); 66 | 67 | if ($code = $throwable->getCode()) { 68 | $text .= sprintf('Code: %s' . PHP_EOL, $code); 69 | } 70 | 71 | if ($message = $throwable->getMessage()) { 72 | $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); 73 | } 74 | 75 | if ($file = $throwable->getFile()) { 76 | $text .= sprintf('File: %s' . PHP_EOL, $file); 77 | } 78 | 79 | if ($line = $throwable->getLine()) { 80 | $text .= sprintf('Line: %s' . PHP_EOL, $line); 81 | } 82 | 83 | if ($trace = $throwable->getTraceAsString()) { 84 | $text .= sprintf('Trace: %s', $trace); 85 | } 86 | 87 | return $text; 88 | } 89 | 90 | /** 91 | * Wraps the error_log function so that this can be easily tested 92 | * 93 | * @param $message 94 | */ 95 | protected function logError($message) 96 | { 97 | error_log($message); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Slim/Http/StatusCode.php: -------------------------------------------------------------------------------- 1 | container = $container; 34 | } 35 | 36 | /** 37 | * Resolve toResolve into a closure so that the router can dispatch. 38 | * 39 | * If toResolve is of the format 'class:method', then try to extract 'class' 40 | * from the container otherwise instantiate it and then dispatch 'method'. 41 | * 42 | * @param mixed $toResolve 43 | * 44 | * @return callable 45 | * 46 | * @throws RuntimeException if the callable does not exist 47 | * @throws RuntimeException if the callable is not resolvable 48 | */ 49 | public function resolve($toResolve) 50 | { 51 | if (is_callable($toResolve)) { 52 | return $toResolve; 53 | } 54 | 55 | if (!is_string($toResolve)) { 56 | $this->assertCallable($toResolve); 57 | } 58 | 59 | // check for slim callable as "class:method" 60 | if (preg_match(self::CALLABLE_PATTERN, $toResolve, $matches)) { 61 | $resolved = $this->resolveCallable($matches[1], $matches[2]); 62 | $this->assertCallable($resolved); 63 | 64 | return $resolved; 65 | } 66 | 67 | $resolved = $this->resolveCallable($toResolve); 68 | $this->assertCallable($resolved); 69 | 70 | return $resolved; 71 | } 72 | 73 | /** 74 | * Check if string is something in the DIC 75 | * that's callable or is a class name which has an __invoke() method. 76 | * 77 | * @param string $class 78 | * @param string $method 79 | * @return callable 80 | * 81 | * @throws \RuntimeException if the callable does not exist 82 | */ 83 | protected function resolveCallable($class, $method = '__invoke') 84 | { 85 | if ($this->container->has($class)) { 86 | return [$this->container->get($class), $method]; 87 | } 88 | 89 | if (!class_exists($class)) { 90 | throw new RuntimeException(sprintf('Callable %s does not exist', $class)); 91 | } 92 | 93 | return [new $class($this->container), $method]; 94 | } 95 | 96 | /** 97 | * @param Callable $callable 98 | * 99 | * @throws \RuntimeException if the callable is not resolvable 100 | */ 101 | protected function assertCallable($callable) 102 | { 103 | if (!is_callable($callable)) { 104 | throw new RuntimeException(sprintf( 105 | '%s is not resolvable', 106 | is_array($callable) || is_object($callable) ? json_encode($callable) : $callable 107 | )); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Slim/Interfaces/RouteInterface.php: -------------------------------------------------------------------------------- 1 | middlewareLock) { 56 | throw new RuntimeException('Middleware can’t be added once the stack is dequeuing'); 57 | } 58 | 59 | if (is_null($this->tip)) { 60 | $this->seedMiddlewareStack(); 61 | } 62 | $next = $this->tip; 63 | $this->tip = function ( 64 | ServerRequestInterface $request, 65 | ResponseInterface $response 66 | ) use ( 67 | $callable, 68 | $next 69 | ) { 70 | $result = call_user_func($callable, $request, $response, $next); 71 | if ($result instanceof ResponseInterface === false) { 72 | throw new UnexpectedValueException( 73 | 'Middleware must return instance of \Psr\Http\Message\ResponseInterface' 74 | ); 75 | } 76 | 77 | return $result; 78 | }; 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Seed middleware stack with first callable 85 | * 86 | * @param callable $kernel The last item to run as middleware 87 | * 88 | * @throws RuntimeException if the stack is seeded more than once 89 | */ 90 | protected function seedMiddlewareStack(callable $kernel = null) 91 | { 92 | if (!is_null($this->tip)) { 93 | throw new RuntimeException('MiddlewareStack can only be seeded once.'); 94 | } 95 | if ($kernel === null) { 96 | $kernel = $this; 97 | } 98 | $this->tip = $kernel; 99 | } 100 | 101 | /** 102 | * Call middleware stack 103 | * 104 | * @param ServerRequestInterface $request A request object 105 | * @param ResponseInterface $response A response object 106 | * 107 | * @return ResponseInterface 108 | */ 109 | public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response) 110 | { 111 | if (is_null($this->tip)) { 112 | $this->seedMiddlewareStack(); 113 | } 114 | /** @var callable $start */ 115 | $start = $this->tip; 116 | $this->middlewareLock = true; 117 | $response = $start($request, $response); 118 | $this->middlewareLock = false; 119 | return $response; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Slim/Handlers/NotFound.php: -------------------------------------------------------------------------------- 1 | getMethod() === 'OPTIONS') { 36 | $contentType = 'text/plain'; 37 | $output = $this->renderPlainNotFoundOutput(); 38 | } else { 39 | $contentType = $this->determineContentType($request); 40 | switch ($contentType) { 41 | case 'application/json': 42 | $output = $this->renderJsonNotFoundOutput(); 43 | break; 44 | 45 | case 'text/xml': 46 | case 'application/xml': 47 | $output = $this->renderXmlNotFoundOutput(); 48 | break; 49 | 50 | case 'text/html': 51 | $output = $this->renderHtmlNotFoundOutput($request); 52 | break; 53 | 54 | default: 55 | throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); 56 | } 57 | } 58 | 59 | $body = new Body(fopen('php://temp', 'r+')); 60 | $body->write($output); 61 | 62 | return $response->withStatus(404) 63 | ->withHeader('Content-Type', $contentType) 64 | ->withBody($body); 65 | } 66 | 67 | /** 68 | * Render plain not found message 69 | * 70 | * @return ResponseInterface 71 | */ 72 | protected function renderPlainNotFoundOutput() 73 | { 74 | return 'Not found'; 75 | } 76 | 77 | /** 78 | * Return a response for application/json content not found 79 | * 80 | * @return ResponseInterface 81 | */ 82 | protected function renderJsonNotFoundOutput() 83 | { 84 | return '{"message":"Not found"}'; 85 | } 86 | 87 | /** 88 | * Return a response for xml content not found 89 | * 90 | * @return ResponseInterface 91 | */ 92 | protected function renderXmlNotFoundOutput() 93 | { 94 | return 'Not found'; 95 | } 96 | 97 | /** 98 | * Return a response for text/html content not found 99 | * 100 | * @param ServerRequestInterface $request The most recent Request object 101 | * 102 | * @return ResponseInterface 103 | */ 104 | protected function renderHtmlNotFoundOutput(ServerRequestInterface $request) 105 | { 106 | $homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment('')); 107 | return << 109 | 110 | Page Not Found 111 | 128 | 129 | 130 |

Page Not Found

131 |

132 | The page you are looking for could not be found. Check the address bar 133 | to ensure your URL is spelled correctly. If all else fails, you can 134 | visit our home page at the link below. 135 |

136 | Visit the Home Page 137 | 138 | 139 | END; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Slim/Handlers/NotAllowed.php: -------------------------------------------------------------------------------- 1 | getMethod() === 'OPTIONS') { 37 | $status = 200; 38 | $contentType = 'text/plain'; 39 | $output = $this->renderPlainOptionsMessage($methods); 40 | } else { 41 | $status = 405; 42 | $contentType = $this->determineContentType($request); 43 | switch ($contentType) { 44 | case 'application/json': 45 | $output = $this->renderJsonNotAllowedMessage($methods); 46 | break; 47 | 48 | case 'text/xml': 49 | case 'application/xml': 50 | $output = $this->renderXmlNotAllowedMessage($methods); 51 | break; 52 | 53 | case 'text/html': 54 | $output = $this->renderHtmlNotAllowedMessage($methods); 55 | break; 56 | default: 57 | throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); 58 | } 59 | } 60 | 61 | $body = new Body(fopen('php://temp', 'r+')); 62 | $body->write($output); 63 | $allow = implode(', ', $methods); 64 | 65 | return $response 66 | ->withStatus($status) 67 | ->withHeader('Content-type', $contentType) 68 | ->withHeader('Allow', $allow) 69 | ->withBody($body); 70 | } 71 | 72 | /** 73 | * Render PLAIN message for OPTIONS response 74 | * 75 | * @param array $methods 76 | * @return string 77 | */ 78 | protected function renderPlainOptionsMessage($methods) 79 | { 80 | $allow = implode(', ', $methods); 81 | 82 | return 'Allowed methods: ' . $allow; 83 | } 84 | 85 | /** 86 | * Render JSON not allowed message 87 | * 88 | * @param array $methods 89 | * @return string 90 | */ 91 | protected function renderJsonNotAllowedMessage($methods) 92 | { 93 | $allow = implode(', ', $methods); 94 | 95 | return '{"message":"Method not allowed. Must be one of: ' . $allow . '"}'; 96 | } 97 | 98 | /** 99 | * Render XML not allowed message 100 | * 101 | * @param array $methods 102 | * @return string 103 | */ 104 | protected function renderXmlNotAllowedMessage($methods) 105 | { 106 | $allow = implode(', ', $methods); 107 | 108 | return "Method not allowed. Must be one of: $allow"; 109 | } 110 | 111 | /** 112 | * Render HTML not allowed message 113 | * 114 | * @param array $methods 115 | * @return string 116 | */ 117 | protected function renderHtmlNotAllowedMessage($methods) 118 | { 119 | $allow = implode(', ', $methods); 120 | $output = << 122 | 123 | Method not allowed 124 | 137 | 138 | 139 |

Method not allowed

140 |

Method not allowed. Must be one of: $allow

141 | 142 | 143 | END; 144 | 145 | return $output; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Slim/Collection.php: -------------------------------------------------------------------------------- 1 | replace($items); 38 | } 39 | 40 | /******************************************************************************** 41 | * Collection interface 42 | *******************************************************************************/ 43 | 44 | /** 45 | * Set collection item 46 | * 47 | * @param string $key The data key 48 | * @param mixed $value The data value 49 | */ 50 | public function set($key, $value) 51 | { 52 | $this->data[$key] = $value; 53 | } 54 | 55 | /** 56 | * Get collection item for key 57 | * 58 | * @param string $key The data key 59 | * @param mixed $default The default value to return if data key does not exist 60 | * 61 | * @return mixed The key's value, or the default value 62 | */ 63 | public function get($key, $default = null) 64 | { 65 | return $this->has($key) ? $this->data[$key] : $default; 66 | } 67 | 68 | /** 69 | * Add item to collection, replacing existing items with the same data key 70 | * 71 | * @param array $items Key-value array of data to append to this collection 72 | */ 73 | public function replace(array $items) 74 | { 75 | foreach ($items as $key => $value) { 76 | $this->set($key, $value); 77 | } 78 | } 79 | 80 | /** 81 | * Get all items in collection 82 | * 83 | * @return array The collection's source data 84 | */ 85 | public function all() 86 | { 87 | return $this->data; 88 | } 89 | 90 | /** 91 | * Get collection keys 92 | * 93 | * @return array The collection's source data keys 94 | */ 95 | public function keys() 96 | { 97 | return array_keys($this->data); 98 | } 99 | 100 | /** 101 | * Does this collection have a given key? 102 | * 103 | * @param string $key The data key 104 | * 105 | * @return bool 106 | */ 107 | public function has($key) 108 | { 109 | return array_key_exists($key, $this->data); 110 | } 111 | 112 | /** 113 | * Remove item from collection 114 | * 115 | * @param string $key The data key 116 | */ 117 | public function remove($key) 118 | { 119 | unset($this->data[$key]); 120 | } 121 | 122 | /** 123 | * Remove all items from collection 124 | */ 125 | public function clear() 126 | { 127 | $this->data = []; 128 | } 129 | 130 | /******************************************************************************** 131 | * ArrayAccess interface 132 | *******************************************************************************/ 133 | 134 | /** 135 | * Does this collection have a given key? 136 | * 137 | * @param string $key The data key 138 | * 139 | * @return bool 140 | */ 141 | public function offsetExists($key) 142 | { 143 | return $this->has($key); 144 | } 145 | 146 | /** 147 | * Get collection item for key 148 | * 149 | * @param string $key The data key 150 | * 151 | * @return mixed The key's value, or the default value 152 | */ 153 | public function offsetGet($key) 154 | { 155 | return $this->get($key); 156 | } 157 | 158 | /** 159 | * Set collection item 160 | * 161 | * @param string $key The data key 162 | * @param mixed $value The data value 163 | */ 164 | public function offsetSet($key, $value) 165 | { 166 | $this->set($key, $value); 167 | } 168 | 169 | /** 170 | * Remove item from collection 171 | * 172 | * @param string $key The data key 173 | */ 174 | public function offsetUnset($key) 175 | { 176 | $this->remove($key); 177 | } 178 | 179 | /** 180 | * Get number of items in collection 181 | * 182 | * @return int 183 | */ 184 | public function count() 185 | { 186 | return count($this->data); 187 | } 188 | 189 | /******************************************************************************** 190 | * IteratorAggregate interface 191 | *******************************************************************************/ 192 | 193 | /** 194 | * Get collection iterator 195 | * 196 | * @return \ArrayIterator 197 | */ 198 | public function getIterator() 199 | { 200 | return new ArrayIterator($this->data); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Slim/Http/Cookies.php: -------------------------------------------------------------------------------- 1 | '', 40 | 'domain' => null, 41 | 'hostonly' => null, 42 | 'path' => null, 43 | 'expires' => null, 44 | 'secure' => false, 45 | 'httponly' => false, 46 | 'samesite' => null 47 | ]; 48 | 49 | /** 50 | * Create new cookies helper 51 | * 52 | * @param array $cookies 53 | */ 54 | public function __construct(array $cookies = []) 55 | { 56 | $this->requestCookies = $cookies; 57 | } 58 | 59 | /** 60 | * Set default cookie properties 61 | * 62 | * @param array $settings 63 | */ 64 | public function setDefaults(array $settings) 65 | { 66 | $this->defaults = array_replace($this->defaults, $settings); 67 | } 68 | 69 | /** 70 | * Get request cookie 71 | * 72 | * @param string $name Cookie name 73 | * @param mixed $default Cookie default value 74 | * 75 | * @return mixed Cookie value if present, else default 76 | */ 77 | public function get($name, $default = null) 78 | { 79 | return isset($this->requestCookies[$name]) ? $this->requestCookies[$name] : $default; 80 | } 81 | 82 | /** 83 | * Set response cookie 84 | * 85 | * @param string $name Cookie name 86 | * @param string|array $value Cookie value, or cookie properties 87 | */ 88 | public function set($name, $value) 89 | { 90 | if (!is_array($value)) { 91 | $value = ['value' => (string)$value]; 92 | } 93 | $this->responseCookies[$name] = array_replace($this->defaults, $value); 94 | } 95 | 96 | /** 97 | * Convert to `Set-Cookie` headers 98 | * 99 | * @return string[] 100 | */ 101 | public function toHeaders() 102 | { 103 | $headers = []; 104 | foreach ($this->responseCookies as $name => $properties) { 105 | $headers[] = $this->toHeader($name, $properties); 106 | } 107 | 108 | return $headers; 109 | } 110 | 111 | /** 112 | * Convert to `Set-Cookie` header 113 | * 114 | * @param string $name Cookie name 115 | * @param array $properties Cookie properties 116 | * 117 | * @return string 118 | */ 119 | protected function toHeader($name, array $properties) 120 | { 121 | $result = urlencode($name) . '=' . urlencode($properties['value']); 122 | 123 | if (isset($properties['domain'])) { 124 | $result .= '; domain=' . $properties['domain']; 125 | } 126 | 127 | if (isset($properties['path'])) { 128 | $result .= '; path=' . $properties['path']; 129 | } 130 | 131 | if (isset($properties['expires'])) { 132 | if (is_string($properties['expires'])) { 133 | $timestamp = strtotime($properties['expires']); 134 | } else { 135 | $timestamp = (int)$properties['expires']; 136 | } 137 | if ($timestamp !== 0) { 138 | $result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp); 139 | } 140 | } 141 | 142 | if (isset($properties['secure']) && $properties['secure']) { 143 | $result .= '; secure'; 144 | } 145 | 146 | if (isset($properties['hostonly']) && $properties['hostonly']) { 147 | $result .= '; HostOnly'; 148 | } 149 | 150 | if (isset($properties['httponly']) && $properties['httponly']) { 151 | $result .= '; HttpOnly'; 152 | } 153 | 154 | if (isset($properties['samesite']) && in_array(strtolower($properties['samesite']), ['lax', 'strict'], true)) { 155 | // While strtolower is needed for correct comparison, the RFC doesn't care about case 156 | $result .= '; SameSite=' . $properties['samesite']; 157 | } 158 | 159 | return $result; 160 | } 161 | 162 | /** 163 | * Parse HTTP request `Cookie:` header and extract 164 | * into a PHP associative array. 165 | * 166 | * @param string $header The raw HTTP request `Cookie:` header 167 | * 168 | * @return array Associative array of cookie names and values 169 | * 170 | * @throws InvalidArgumentException if the cookie data cannot be parsed 171 | */ 172 | public static function parseHeader($header) 173 | { 174 | if (is_array($header) === true) { 175 | $header = isset($header[0]) ? $header[0] : ''; 176 | } 177 | 178 | if (is_string($header) === false) { 179 | throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.'); 180 | } 181 | 182 | $header = rtrim($header, "\r\n"); 183 | $pieces = preg_split('@[;]\s*@', $header); 184 | $cookies = []; 185 | 186 | foreach ($pieces as $cookie) { 187 | $cookie = explode('=', $cookie, 2); 188 | 189 | if (count($cookie) === 2) { 190 | $key = urldecode($cookie[0]); 191 | $value = urldecode($cookie[1]); 192 | 193 | if (!isset($cookies[$key])) { 194 | $cookies[$key] = $value; 195 | } 196 | } 197 | } 198 | 199 | return $cookies; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Slim/Container.php: -------------------------------------------------------------------------------- 1 | '1.1', 54 | 'responseChunkSize' => 4096, 55 | 'outputBuffering' => 'append', 56 | 'determineRouteBeforeAppMiddleware' => false, 57 | 'displayErrorDetails' => false, 58 | 'addContentLengthHeader' => true, 59 | 'routerCacheFile' => false, 60 | ]; 61 | 62 | /** 63 | * Create new container 64 | * 65 | * @param array $values The parameters or objects. 66 | */ 67 | public function __construct(array $values = []) 68 | { 69 | parent::__construct($values); 70 | 71 | $userSettings = isset($values['settings']) ? $values['settings'] : []; 72 | $this->registerDefaultServices($userSettings); 73 | } 74 | 75 | /** 76 | * This function registers the default services that Slim needs to work. 77 | * 78 | * All services are shared - that is, they are registered such that the 79 | * same instance is returned on subsequent calls. 80 | * 81 | * @param array $userSettings Associative array of application settings 82 | * 83 | * @return void 84 | */ 85 | private function registerDefaultServices($userSettings) 86 | { 87 | $defaultSettings = $this->defaultSettings; 88 | 89 | /** 90 | * This service MUST return an array or an 91 | * instance of \ArrayAccess. 92 | * 93 | * @return array|\ArrayAccess 94 | */ 95 | $this['settings'] = function () use ($userSettings, $defaultSettings) { 96 | return new Collection(array_merge($defaultSettings, $userSettings)); 97 | }; 98 | 99 | $defaultProvider = new DefaultServicesProvider(); 100 | $defaultProvider->register($this); 101 | } 102 | 103 | /******************************************************************************** 104 | * Methods to satisfy Psr\Container\ContainerInterface 105 | *******************************************************************************/ 106 | 107 | /** 108 | * Finds an entry of the container by its identifier and returns it. 109 | * 110 | * @param string $id Identifier of the entry to look for. 111 | * 112 | * @throws ContainerValueNotFoundException No entry was found for this identifier. 113 | * @throws ContainerException Error while retrieving the entry. 114 | * 115 | * @return mixed Entry. 116 | */ 117 | public function get($id) 118 | { 119 | if (!$this->offsetExists($id)) { 120 | throw new ContainerValueNotFoundException(sprintf('Identifier "%s" is not defined.', $id)); 121 | } 122 | try { 123 | return $this->offsetGet($id); 124 | } catch (\InvalidArgumentException $exception) { 125 | if ($this->exceptionThrownByContainer($exception)) { 126 | throw new SlimContainerException( 127 | sprintf('Container error while retrieving "%s"', $id), 128 | null, 129 | $exception 130 | ); 131 | } else { 132 | throw $exception; 133 | } 134 | } 135 | } 136 | 137 | /** 138 | * Tests whether an exception needs to be recast for compliance with Container-Interop. This will be if the 139 | * exception was thrown by Pimple. 140 | * 141 | * @param \InvalidArgumentException $exception 142 | * 143 | * @return bool 144 | */ 145 | private function exceptionThrownByContainer(\InvalidArgumentException $exception) 146 | { 147 | $trace = $exception->getTrace()[0]; 148 | 149 | return $trace['class'] === PimpleContainer::class && $trace['function'] === 'offsetGet'; 150 | } 151 | 152 | /** 153 | * Returns true if the container can return an entry for the given identifier. 154 | * Returns false otherwise. 155 | * 156 | * @param string $id Identifier of the entry to look for. 157 | * 158 | * @return boolean 159 | */ 160 | public function has($id) 161 | { 162 | return $this->offsetExists($id); 163 | } 164 | 165 | 166 | /******************************************************************************** 167 | * Magic methods for convenience 168 | *******************************************************************************/ 169 | 170 | public function __get($name) 171 | { 172 | return $this->get($name); 173 | } 174 | 175 | public function __isset($name) 176 | { 177 | return $this->has($name); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Slim/Http/Headers.php: -------------------------------------------------------------------------------- 1 | 1, 36 | 'CONTENT_LENGTH' => 1, 37 | 'PHP_AUTH_USER' => 1, 38 | 'PHP_AUTH_PW' => 1, 39 | 'PHP_AUTH_DIGEST' => 1, 40 | 'AUTH_TYPE' => 1, 41 | ]; 42 | 43 | /** 44 | * Create new headers collection with data extracted from 45 | * the application Environment object 46 | * 47 | * @param Environment $environment The Slim application Environment 48 | * 49 | * @return self 50 | */ 51 | public static function createFromEnvironment(Environment $environment) 52 | { 53 | $data = []; 54 | $environment = self::determineAuthorization($environment); 55 | foreach ($environment as $key => $value) { 56 | $key = strtoupper($key); 57 | if (isset(static::$special[$key]) || strpos($key, 'HTTP_') === 0) { 58 | if ($key !== 'HTTP_CONTENT_LENGTH') { 59 | $data[$key] = $value; 60 | } 61 | } 62 | } 63 | 64 | return new static($data); 65 | } 66 | 67 | /** 68 | * If HTTP_AUTHORIZATION does not exist tries to get it from 69 | * getallheaders() when available. 70 | * 71 | * @param Environment $environment The Slim application Environment 72 | * 73 | * @return Environment 74 | */ 75 | 76 | public static function determineAuthorization(Environment $environment) 77 | { 78 | $authorization = $environment->get('HTTP_AUTHORIZATION'); 79 | 80 | if (empty($authorization) && is_callable('getallheaders')) { 81 | $headers = getallheaders(); 82 | $headers = array_change_key_case($headers, CASE_LOWER); 83 | if (isset($headers['authorization'])) { 84 | $environment->set('HTTP_AUTHORIZATION', $headers['authorization']); 85 | } 86 | } 87 | 88 | return $environment; 89 | } 90 | 91 | /** 92 | * Return array of HTTP header names and values. 93 | * This method returns the _original_ header name 94 | * as specified by the end user. 95 | * 96 | * @return array 97 | */ 98 | public function all() 99 | { 100 | $all = parent::all(); 101 | $out = []; 102 | foreach ($all as $key => $props) { 103 | $out[$props['originalKey']] = $props['value']; 104 | } 105 | 106 | return $out; 107 | } 108 | 109 | /** 110 | * Set HTTP header value 111 | * 112 | * This method sets a header value. It replaces 113 | * any values that may already exist for the header name. 114 | * 115 | * @param string $key The case-insensitive header name 116 | * @param string $value The header value 117 | */ 118 | public function set($key, $value) 119 | { 120 | if (!is_array($value)) { 121 | $value = [$value]; 122 | } 123 | parent::set($this->normalizeKey($key), [ 124 | 'value' => $value, 125 | 'originalKey' => $key 126 | ]); 127 | } 128 | 129 | /** 130 | * Get HTTP header value 131 | * 132 | * @param string $key The case-insensitive header name 133 | * @param mixed $default The default value if key does not exist 134 | * 135 | * @return string[] 136 | */ 137 | public function get($key, $default = null) 138 | { 139 | if ($this->has($key)) { 140 | return parent::get($this->normalizeKey($key))['value']; 141 | } 142 | 143 | return $default; 144 | } 145 | 146 | /** 147 | * Get HTTP header key as originally specified 148 | * 149 | * @param string $key The case-insensitive header name 150 | * @param mixed $default The default value if key does not exist 151 | * 152 | * @return string 153 | */ 154 | public function getOriginalKey($key, $default = null) 155 | { 156 | if ($this->has($key)) { 157 | return parent::get($this->normalizeKey($key))['originalKey']; 158 | } 159 | 160 | return $default; 161 | } 162 | 163 | /** 164 | * Add HTTP header value 165 | * 166 | * This method appends a header value. Unlike the set() method, 167 | * this method _appends_ this new value to any values 168 | * that already exist for this header name. 169 | * 170 | * @param string $key The case-insensitive header name 171 | * @param array|string $value The new header value(s) 172 | */ 173 | public function add($key, $value) 174 | { 175 | $oldValues = $this->get($key, []); 176 | $newValues = is_array($value) ? $value : [$value]; 177 | $this->set($key, array_merge($oldValues, array_values($newValues))); 178 | } 179 | 180 | /** 181 | * Does this collection have a given header? 182 | * 183 | * @param string $key The case-insensitive header name 184 | * 185 | * @return bool 186 | */ 187 | public function has($key) 188 | { 189 | return parent::has($this->normalizeKey($key)); 190 | } 191 | 192 | /** 193 | * Remove header from collection 194 | * 195 | * @param string $key The case-insensitive header name 196 | */ 197 | public function remove($key) 198 | { 199 | parent::remove($this->normalizeKey($key)); 200 | } 201 | 202 | /** 203 | * Normalize header name 204 | * 205 | * This method transforms header names into a 206 | * normalized form. This is how we enable case-insensitive 207 | * header names in the other methods in this class. 208 | * 209 | * @param string $key The case-insensitive header name 210 | * 211 | * @return string Normalized header name 212 | */ 213 | public function normalizeKey($key) 214 | { 215 | $key = strtr(strtolower($key), '_', '-'); 216 | if (strpos($key, 'http-') === 0) { 217 | $key = substr($key, 5); 218 | } 219 | 220 | return $key; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Slim/Handlers/PhpError.php: -------------------------------------------------------------------------------- 1 | determineContentType($request); 37 | switch ($contentType) { 38 | case 'application/json': 39 | $output = $this->renderJsonErrorMessage($error); 40 | break; 41 | 42 | case 'text/xml': 43 | case 'application/xml': 44 | $output = $this->renderXmlErrorMessage($error); 45 | break; 46 | 47 | case 'text/html': 48 | $output = $this->renderHtmlErrorMessage($error); 49 | break; 50 | default: 51 | throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); 52 | } 53 | 54 | $this->writeToErrorLog($error); 55 | 56 | $body = new Body(fopen('php://temp', 'r+')); 57 | $body->write($output); 58 | 59 | return $response 60 | ->withStatus(500) 61 | ->withHeader('Content-type', $contentType) 62 | ->withBody($body); 63 | } 64 | 65 | /** 66 | * Render HTML error page 67 | * 68 | * @param \Throwable $error 69 | * 70 | * @return string 71 | */ 72 | protected function renderHtmlErrorMessage(\Throwable $error) 73 | { 74 | $title = 'Slim Application Error'; 75 | 76 | if ($this->displayErrorDetails) { 77 | $html = '

The application could not run because of the following error:

'; 78 | $html .= '

Details

'; 79 | $html .= $this->renderHtmlError($error); 80 | 81 | while ($error = $error->getPrevious()) { 82 | $html .= '

Previous error

'; 83 | $html .= $this->renderHtmlError($error); 84 | } 85 | } else { 86 | $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; 87 | } 88 | 89 | $output = sprintf( 90 | "" . 91 | "%s

%s

%s", 94 | $title, 95 | $title, 96 | $html 97 | ); 98 | 99 | return $output; 100 | } 101 | 102 | /** 103 | * Render error as HTML. 104 | * 105 | * @param \Throwable $error 106 | * 107 | * @return string 108 | */ 109 | protected function renderHtmlError(\Throwable $error) 110 | { 111 | $html = sprintf('
Type: %s
', get_class($error)); 112 | 113 | if (($code = $error->getCode())) { 114 | $html .= sprintf('
Code: %s
', $code); 115 | } 116 | 117 | if (($message = $error->getMessage())) { 118 | $html .= sprintf('
Message: %s
', htmlentities($message)); 119 | } 120 | 121 | if (($file = $error->getFile())) { 122 | $html .= sprintf('
File: %s
', $file); 123 | } 124 | 125 | if (($line = $error->getLine())) { 126 | $html .= sprintf('
Line: %s
', $line); 127 | } 128 | 129 | if (($trace = $error->getTraceAsString())) { 130 | $html .= '

Trace

'; 131 | $html .= sprintf('
%s
', htmlentities($trace)); 132 | } 133 | 134 | return $html; 135 | } 136 | 137 | /** 138 | * Render JSON error 139 | * 140 | * @param \Throwable $error 141 | * 142 | * @return string 143 | */ 144 | protected function renderJsonErrorMessage(\Throwable $error) 145 | { 146 | $json = [ 147 | 'message' => 'Slim Application Error', 148 | ]; 149 | 150 | if ($this->displayErrorDetails) { 151 | $json['error'] = []; 152 | 153 | do { 154 | $json['error'][] = [ 155 | 'type' => get_class($error), 156 | 'code' => $error->getCode(), 157 | 'message' => $error->getMessage(), 158 | 'file' => $error->getFile(), 159 | 'line' => $error->getLine(), 160 | 'trace' => explode("\n", $error->getTraceAsString()), 161 | ]; 162 | } while ($error = $error->getPrevious()); 163 | } 164 | 165 | return json_encode($json, JSON_PRETTY_PRINT); 166 | } 167 | 168 | /** 169 | * Render XML error 170 | * 171 | * @param \Throwable $error 172 | * 173 | * @return string 174 | */ 175 | protected function renderXmlErrorMessage(\Throwable $error) 176 | { 177 | $xml = "\n Slim Application Error\n"; 178 | if ($this->displayErrorDetails) { 179 | do { 180 | $xml .= " \n"; 181 | $xml .= " " . get_class($error) . "\n"; 182 | $xml .= " " . $error->getCode() . "\n"; 183 | $xml .= " " . $this->createCdataSection($error->getMessage()) . "\n"; 184 | $xml .= " " . $error->getFile() . "\n"; 185 | $xml .= " " . $error->getLine() . "\n"; 186 | $xml .= " " . $this->createCdataSection($error->getTraceAsString()) . "\n"; 187 | $xml .= " \n"; 188 | } while ($error = $error->getPrevious()); 189 | } 190 | $xml .= ""; 191 | 192 | return $xml; 193 | } 194 | 195 | /** 196 | * Returns a CDATA section with the given content. 197 | * 198 | * @param string $content 199 | * @return string 200 | */ 201 | private function createCdataSection($content) 202 | { 203 | return sprintf('', str_replace(']]>', ']]]]>', $content)); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Slim/DefaultServicesProvider.php: -------------------------------------------------------------------------------- 1 | get('environment')); 61 | }; 62 | } 63 | 64 | if (!isset($container['response'])) { 65 | /** 66 | * PSR-7 Response object 67 | * 68 | * @param Container $container 69 | * 70 | * @return ResponseInterface 71 | */ 72 | $container['response'] = function ($container) { 73 | $headers = new Headers(['Content-Type' => 'text/html; charset=UTF-8']); 74 | $response = new Response(200, $headers); 75 | 76 | return $response->withProtocolVersion($container->get('settings')['httpVersion']); 77 | }; 78 | } 79 | 80 | if (!isset($container['router'])) { 81 | /** 82 | * This service MUST return a SHARED instance 83 | * of \Slim\Interfaces\RouterInterface. 84 | * 85 | * @param Container $container 86 | * 87 | * @return RouterInterface 88 | */ 89 | $container['router'] = function ($container) { 90 | $routerCacheFile = false; 91 | if (isset($container->get('settings')['routerCacheFile'])) { 92 | $routerCacheFile = $container->get('settings')['routerCacheFile']; 93 | } 94 | 95 | 96 | $router = (new Router)->setCacheFile($routerCacheFile); 97 | if (method_exists($router, 'setContainer')) { 98 | $router->setContainer($container); 99 | } 100 | 101 | return $router; 102 | }; 103 | } 104 | 105 | if (!isset($container['foundHandler'])) { 106 | /** 107 | * This service MUST return a SHARED instance 108 | * of \Slim\Interfaces\InvocationStrategyInterface. 109 | * 110 | * @return InvocationStrategyInterface 111 | */ 112 | $container['foundHandler'] = function () { 113 | return new RequestResponse; 114 | }; 115 | } 116 | 117 | if (!isset($container['phpErrorHandler'])) { 118 | /** 119 | * This service MUST return a callable 120 | * that accepts three arguments: 121 | * 122 | * 1. Instance of \Psr\Http\Message\ServerRequestInterface 123 | * 2. Instance of \Psr\Http\Message\ResponseInterface 124 | * 3. Instance of \Error 125 | * 126 | * The callable MUST return an instance of 127 | * \Psr\Http\Message\ResponseInterface. 128 | * 129 | * @param Container $container 130 | * 131 | * @return callable 132 | */ 133 | $container['phpErrorHandler'] = function ($container) { 134 | return new PhpError($container->get('settings')['displayErrorDetails']); 135 | }; 136 | } 137 | 138 | if (!isset($container['errorHandler'])) { 139 | /** 140 | * This service MUST return a callable 141 | * that accepts three arguments: 142 | * 143 | * 1. Instance of \Psr\Http\Message\ServerRequestInterface 144 | * 2. Instance of \Psr\Http\Message\ResponseInterface 145 | * 3. Instance of \Exception 146 | * 147 | * The callable MUST return an instance of 148 | * \Psr\Http\Message\ResponseInterface. 149 | * 150 | * @param Container $container 151 | * 152 | * @return callable 153 | */ 154 | $container['errorHandler'] = function ($container) { 155 | return new Error( 156 | $container->get('settings')['displayErrorDetails'] 157 | ); 158 | }; 159 | } 160 | 161 | if (!isset($container['notFoundHandler'])) { 162 | /** 163 | * This service MUST return a callable 164 | * that accepts two arguments: 165 | * 166 | * 1. Instance of \Psr\Http\Message\ServerRequestInterface 167 | * 2. Instance of \Psr\Http\Message\ResponseInterface 168 | * 169 | * The callable MUST return an instance of 170 | * \Psr\Http\Message\ResponseInterface. 171 | * 172 | * @return callable 173 | */ 174 | $container['notFoundHandler'] = function () { 175 | return new NotFound; 176 | }; 177 | } 178 | 179 | if (!isset($container['notAllowedHandler'])) { 180 | /** 181 | * This service MUST return a callable 182 | * that accepts three arguments: 183 | * 184 | * 1. Instance of \Psr\Http\Message\ServerRequestInterface 185 | * 2. Instance of \Psr\Http\Message\ResponseInterface 186 | * 3. Array of allowed HTTP methods 187 | * 188 | * The callable MUST return an instance of 189 | * \Psr\Http\Message\ResponseInterface. 190 | * 191 | * @return callable 192 | */ 193 | $container['notAllowedHandler'] = function () { 194 | return new NotAllowed; 195 | }; 196 | } 197 | 198 | if (!isset($container['callableResolver'])) { 199 | /** 200 | * Instance of \Slim\Interfaces\CallableResolverInterface 201 | * 202 | * @param Container $container 203 | * 204 | * @return CallableResolverInterface 205 | */ 206 | $container['callableResolver'] = function ($container) { 207 | return new CallableResolver($container); 208 | }; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Slim/Handlers/Error.php: -------------------------------------------------------------------------------- 1 | determineContentType($request); 37 | switch ($contentType) { 38 | case 'application/json': 39 | $output = $this->renderJsonErrorMessage($exception); 40 | break; 41 | 42 | case 'text/xml': 43 | case 'application/xml': 44 | $output = $this->renderXmlErrorMessage($exception); 45 | break; 46 | 47 | case 'text/html': 48 | $output = $this->renderHtmlErrorMessage($exception); 49 | break; 50 | 51 | default: 52 | throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); 53 | } 54 | 55 | $this->writeToErrorLog($exception); 56 | 57 | $body = new Body(fopen('php://temp', 'r+')); 58 | $body->write($output); 59 | 60 | return $response 61 | ->withStatus(500) 62 | ->withHeader('Content-type', $contentType) 63 | ->withBody($body); 64 | } 65 | 66 | /** 67 | * Render HTML error page 68 | * 69 | * @param \Exception $exception 70 | * 71 | * @return string 72 | */ 73 | protected function renderHtmlErrorMessage(\Exception $exception) 74 | { 75 | $title = 'Slim Application Error'; 76 | 77 | if ($this->displayErrorDetails) { 78 | $html = '

The application could not run because of the following error:

'; 79 | $html .= '

Details

'; 80 | $html .= $this->renderHtmlException($exception); 81 | 82 | while ($exception = $exception->getPrevious()) { 83 | $html .= '

Previous exception

'; 84 | $html .= $this->renderHtmlExceptionOrError($exception); 85 | } 86 | } else { 87 | $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; 88 | } 89 | 90 | $output = sprintf( 91 | "" . 92 | "%s

%s

%s", 95 | $title, 96 | $title, 97 | $html 98 | ); 99 | 100 | return $output; 101 | } 102 | 103 | /** 104 | * Render exception as HTML. 105 | * 106 | * Provided for backwards compatibility; use renderHtmlExceptionOrError(). 107 | * 108 | * @param \Exception $exception 109 | * 110 | * @return string 111 | */ 112 | protected function renderHtmlException(\Exception $exception) 113 | { 114 | return $this->renderHtmlExceptionOrError($exception); 115 | } 116 | 117 | /** 118 | * Render exception or error as HTML. 119 | * 120 | * @param \Exception|\Error $exception 121 | * 122 | * @return string 123 | */ 124 | protected function renderHtmlExceptionOrError($exception) 125 | { 126 | if (!$exception instanceof \Exception && !$exception instanceof \Error) { 127 | throw new \RuntimeException("Unexpected type. Expected Exception or Error."); 128 | } 129 | 130 | $html = sprintf('
Type: %s
', get_class($exception)); 131 | 132 | if (($code = $exception->getCode())) { 133 | $html .= sprintf('
Code: %s
', $code); 134 | } 135 | 136 | if (($message = $exception->getMessage())) { 137 | $html .= sprintf('
Message: %s
', htmlentities($message)); 138 | } 139 | 140 | if (($file = $exception->getFile())) { 141 | $html .= sprintf('
File: %s
', $file); 142 | } 143 | 144 | if (($line = $exception->getLine())) { 145 | $html .= sprintf('
Line: %s
', $line); 146 | } 147 | 148 | if (($trace = $exception->getTraceAsString())) { 149 | $html .= '

Trace

'; 150 | $html .= sprintf('
%s
', htmlentities($trace)); 151 | } 152 | 153 | return $html; 154 | } 155 | 156 | /** 157 | * Render JSON error 158 | * 159 | * @param \Exception $exception 160 | * 161 | * @return string 162 | */ 163 | protected function renderJsonErrorMessage(\Exception $exception) 164 | { 165 | $error = [ 166 | 'message' => 'Slim Application Error', 167 | ]; 168 | 169 | if ($this->displayErrorDetails) { 170 | $error['exception'] = []; 171 | 172 | do { 173 | $error['exception'][] = [ 174 | 'type' => get_class($exception), 175 | 'code' => $exception->getCode(), 176 | 'message' => $exception->getMessage(), 177 | 'file' => $exception->getFile(), 178 | 'line' => $exception->getLine(), 179 | 'trace' => explode("\n", $exception->getTraceAsString()), 180 | ]; 181 | } while ($exception = $exception->getPrevious()); 182 | } 183 | 184 | return json_encode($error, JSON_PRETTY_PRINT); 185 | } 186 | 187 | /** 188 | * Render XML error 189 | * 190 | * @param \Exception $exception 191 | * 192 | * @return string 193 | */ 194 | protected function renderXmlErrorMessage(\Exception $exception) 195 | { 196 | $xml = "\n Slim Application Error\n"; 197 | if ($this->displayErrorDetails) { 198 | do { 199 | $xml .= " \n"; 200 | $xml .= " " . get_class($exception) . "\n"; 201 | $xml .= " " . $exception->getCode() . "\n"; 202 | $xml .= " " . $this->createCdataSection($exception->getMessage()) . "\n"; 203 | $xml .= " " . $exception->getFile() . "\n"; 204 | $xml .= " " . $exception->getLine() . "\n"; 205 | $xml .= " " . $this->createCdataSection($exception->getTraceAsString()) . "\n"; 206 | $xml .= " \n"; 207 | } while ($exception = $exception->getPrevious()); 208 | } 209 | $xml .= ""; 210 | 211 | return $xml; 212 | } 213 | 214 | /** 215 | * Returns a CDATA section with the given content. 216 | * 217 | * @param string $content 218 | * @return string 219 | */ 220 | private function createCdataSection($content) 221 | { 222 | return sprintf('', str_replace(']]>', ']]]]>', $content)); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Slim/Route.php: -------------------------------------------------------------------------------- 1 | methods = is_string($methods) ? [$methods] : $methods; 97 | $this->pattern = $pattern; 98 | $this->callable = $callable; 99 | $this->groups = $groups; 100 | $this->identifier = 'route' . $identifier; 101 | } 102 | 103 | /** 104 | * Finalize the route in preparation for dispatching 105 | */ 106 | public function finalize() 107 | { 108 | if ($this->finalized) { 109 | return; 110 | } 111 | 112 | $groupMiddleware = []; 113 | foreach ($this->getGroups() as $group) { 114 | $groupMiddleware = array_merge($group->getMiddleware(), $groupMiddleware); 115 | } 116 | 117 | $this->middleware = array_merge($this->middleware, $groupMiddleware); 118 | 119 | foreach ($this->getMiddleware() as $middleware) { 120 | $this->addMiddleware($middleware); 121 | } 122 | 123 | $this->finalized = true; 124 | } 125 | 126 | /** 127 | * Get route callable 128 | * 129 | * @return callable 130 | */ 131 | public function getCallable() 132 | { 133 | return $this->callable; 134 | } 135 | 136 | /** 137 | * This method enables you to override the Route's callable 138 | * 139 | * @param string|\Closure $callable 140 | */ 141 | public function setCallable($callable) 142 | { 143 | $this->callable = $callable; 144 | } 145 | 146 | /** 147 | * Get route methods 148 | * 149 | * @return string[] 150 | */ 151 | public function getMethods() 152 | { 153 | return $this->methods; 154 | } 155 | 156 | /** 157 | * Get parent route groups 158 | * 159 | * @return RouteGroup[] 160 | */ 161 | public function getGroups() 162 | { 163 | return $this->groups; 164 | } 165 | 166 | /** 167 | * Get route name 168 | * 169 | * @return null|string 170 | */ 171 | public function getName() 172 | { 173 | return $this->name; 174 | } 175 | 176 | /** 177 | * Get route identifier 178 | * 179 | * @return string 180 | */ 181 | public function getIdentifier() 182 | { 183 | return $this->identifier; 184 | } 185 | 186 | /** 187 | * Get output buffering mode 188 | * 189 | * @return boolean|string 190 | */ 191 | public function getOutputBuffering() 192 | { 193 | return $this->outputBuffering; 194 | } 195 | 196 | /** 197 | * Set output buffering mode 198 | * 199 | * One of: false, 'prepend' or 'append' 200 | * 201 | * @param boolean|string $mode 202 | * 203 | * @return self 204 | * 205 | * @throws InvalidArgumentException If an unknown buffering mode is specified 206 | */ 207 | public function setOutputBuffering($mode) 208 | { 209 | if (!in_array($mode, [false, 'prepend', 'append'], true)) { 210 | throw new InvalidArgumentException('Unknown output buffering mode'); 211 | } 212 | $this->outputBuffering = $mode; 213 | return $this; 214 | } 215 | 216 | /** 217 | * Set route name 218 | * 219 | * @param string $name 220 | * 221 | * @return self 222 | * 223 | * @throws InvalidArgumentException if the route name is not a string 224 | */ 225 | public function setName($name) 226 | { 227 | if (!is_string($name)) { 228 | throw new InvalidArgumentException('Route name must be a string'); 229 | } 230 | $this->name = $name; 231 | return $this; 232 | } 233 | 234 | /** 235 | * Set a route argument 236 | * 237 | * @param string $name 238 | * @param string $value 239 | * @param bool $includeInSavedArguments 240 | * 241 | * @return self 242 | */ 243 | public function setArgument($name, $value, $includeInSavedArguments = true) 244 | { 245 | if ($includeInSavedArguments) { 246 | $this->savedArguments[$name] = $value; 247 | } 248 | $this->arguments[$name] = $value; 249 | return $this; 250 | } 251 | 252 | /** 253 | * Replace route arguments 254 | * 255 | * @param array $arguments 256 | * @param bool $includeInSavedArguments 257 | * 258 | * @return self 259 | */ 260 | public function setArguments(array $arguments, $includeInSavedArguments = true) 261 | { 262 | if ($includeInSavedArguments) { 263 | $this->savedArguments = $arguments; 264 | } 265 | $this->arguments = $arguments; 266 | return $this; 267 | } 268 | 269 | /** 270 | * Retrieve route arguments 271 | * 272 | * @return array 273 | */ 274 | public function getArguments() 275 | { 276 | return $this->arguments; 277 | } 278 | 279 | /** 280 | * Retrieve a specific route argument 281 | * 282 | * @param string $name 283 | * @param string|null $default 284 | * 285 | * @return mixed 286 | */ 287 | public function getArgument($name, $default = null) 288 | { 289 | if (array_key_exists($name, $this->arguments)) { 290 | return $this->arguments[$name]; 291 | } 292 | return $default; 293 | } 294 | 295 | /******************************************************************************** 296 | * Route Runner 297 | *******************************************************************************/ 298 | 299 | /** 300 | * Prepare the route for use 301 | * 302 | * @param ServerRequestInterface $request 303 | * @param array $arguments 304 | */ 305 | public function prepare(ServerRequestInterface $request, array $arguments) 306 | { 307 | // Remove temp arguments 308 | $this->setArguments($this->savedArguments); 309 | 310 | // Add the route arguments 311 | foreach ($arguments as $k => $v) { 312 | $this->setArgument($k, $v, false); 313 | } 314 | } 315 | 316 | /** 317 | * Run route 318 | * 319 | * This method traverses the middleware stack, including the route's callable 320 | * and captures the resultant HTTP response object. It then sends the response 321 | * back to the Application. 322 | * 323 | * @param ServerRequestInterface $request 324 | * @param ResponseInterface $response 325 | * 326 | * @return ResponseInterface 327 | */ 328 | public function run(ServerRequestInterface $request, ResponseInterface $response) 329 | { 330 | // Finalise route now that we are about to run it 331 | $this->finalize(); 332 | 333 | // Traverse middleware stack and fetch updated response 334 | return $this->callMiddlewareStack($request, $response); 335 | } 336 | 337 | /** 338 | * Dispatch route callable against current Request and Response objects 339 | * 340 | * This method invokes the route object's callable. If middleware is 341 | * registered for the route, each callable middleware is invoked in 342 | * the order specified. 343 | * 344 | * @param ServerRequestInterface $request The current Request object 345 | * @param ResponseInterface $response The current Response object 346 | * @return \Psr\Http\Message\ResponseInterface 347 | * @throws \Exception if the route callable throws an exception 348 | */ 349 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response) 350 | { 351 | $this->callable = $this->resolveCallable($this->callable); 352 | 353 | /** @var InvocationStrategyInterface $handler */ 354 | $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse(); 355 | 356 | $newResponse = $handler($this->callable, $request, $response, $this->arguments); 357 | 358 | if ($newResponse instanceof ResponseInterface) { 359 | // if route callback returns a ResponseInterface, then use it 360 | $response = $newResponse; 361 | } elseif (is_string($newResponse)) { 362 | // if route callback returns a string, then append it to the response 363 | if ($response->getBody()->isWritable()) { 364 | $response->getBody()->write($newResponse); 365 | } 366 | } 367 | 368 | return $response; 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /Slim/Http/Message.php: -------------------------------------------------------------------------------- 1 | true, 41 | '1.1' => true, 42 | '2.0' => true, 43 | '2' => true, 44 | ]; 45 | 46 | /** 47 | * Headers 48 | * 49 | * @var \Slim\Interfaces\Http\HeadersInterface 50 | */ 51 | protected $headers; 52 | 53 | /** 54 | * Body object 55 | * 56 | * @var \Psr\Http\Message\StreamInterface 57 | */ 58 | protected $body; 59 | 60 | 61 | /** 62 | * Disable magic setter to ensure immutability 63 | */ 64 | public function __set($name, $value) 65 | { 66 | // Do nothing 67 | } 68 | 69 | /******************************************************************************* 70 | * Protocol 71 | ******************************************************************************/ 72 | 73 | /** 74 | * Retrieves the HTTP protocol version as a string. 75 | * 76 | * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). 77 | * 78 | * @return string HTTP protocol version. 79 | */ 80 | public function getProtocolVersion() 81 | { 82 | return $this->protocolVersion; 83 | } 84 | 85 | /** 86 | * Return an instance with the specified HTTP protocol version. 87 | * 88 | * The version string MUST contain only the HTTP version number (e.g., 89 | * "1.1", "1.0"). 90 | * 91 | * This method MUST be implemented in such a way as to retain the 92 | * immutability of the message, and MUST return an instance that has the 93 | * new protocol version. 94 | * 95 | * @param string $version HTTP protocol version 96 | * @return static 97 | * @throws InvalidArgumentException if the http version is an invalid number 98 | */ 99 | public function withProtocolVersion($version) 100 | { 101 | if (!isset(self::$validProtocolVersions[$version])) { 102 | throw new InvalidArgumentException( 103 | 'Invalid HTTP version. Must be one of: ' 104 | . implode(', ', array_keys(self::$validProtocolVersions)) 105 | ); 106 | } 107 | $clone = clone $this; 108 | $clone->protocolVersion = $version; 109 | 110 | return $clone; 111 | } 112 | 113 | /******************************************************************************* 114 | * Headers 115 | ******************************************************************************/ 116 | 117 | /** 118 | * Retrieves all message header values. 119 | * 120 | * The keys represent the header name as it will be sent over the wire, and 121 | * each value is an array of strings associated with the header. 122 | * 123 | * // Represent the headers as a string 124 | * foreach ($message->getHeaders() as $name => $values) { 125 | * echo $name . ": " . implode(", ", $values); 126 | * } 127 | * 128 | * // Emit headers iteratively: 129 | * foreach ($message->getHeaders() as $name => $values) { 130 | * foreach ($values as $value) { 131 | * header(sprintf('%s: %s', $name, $value), false); 132 | * } 133 | * } 134 | * 135 | * While header names are not case-sensitive, getHeaders() will preserve the 136 | * exact case in which headers were originally specified. 137 | * 138 | * @return array Returns an associative array of the message's headers. Each 139 | * key MUST be a header name, and each value MUST be an array of strings 140 | * for that header. 141 | */ 142 | public function getHeaders() 143 | { 144 | return $this->headers->all(); 145 | } 146 | 147 | /** 148 | * Checks if a header exists by the given case-insensitive name. 149 | * 150 | * @param string $name Case-insensitive header field name. 151 | * @return bool Returns true if any header names match the given header 152 | * name using a case-insensitive string comparison. Returns false if 153 | * no matching header name is found in the message. 154 | */ 155 | public function hasHeader($name) 156 | { 157 | return $this->headers->has($name); 158 | } 159 | 160 | /** 161 | * Retrieves a message header value by the given case-insensitive name. 162 | * 163 | * This method returns an array of all the header values of the given 164 | * case-insensitive header name. 165 | * 166 | * If the header does not appear in the message, this method MUST return an 167 | * empty array. 168 | * 169 | * @param string $name Case-insensitive header field name. 170 | * @return string[] An array of string values as provided for the given 171 | * header. If the header does not appear in the message, this method MUST 172 | * return an empty array. 173 | */ 174 | public function getHeader($name) 175 | { 176 | return $this->headers->get($name, []); 177 | } 178 | 179 | /** 180 | * Retrieves a comma-separated string of the values for a single header. 181 | * 182 | * This method returns all of the header values of the given 183 | * case-insensitive header name as a string concatenated together using 184 | * a comma. 185 | * 186 | * NOTE: Not all header values may be appropriately represented using 187 | * comma concatenation. For such headers, use getHeader() instead 188 | * and supply your own delimiter when concatenating. 189 | * 190 | * If the header does not appear in the message, this method MUST return 191 | * an empty string. 192 | * 193 | * @param string $name Case-insensitive header field name. 194 | * @return string A string of values as provided for the given header 195 | * concatenated together using a comma. If the header does not appear in 196 | * the message, this method MUST return an empty string. 197 | */ 198 | public function getHeaderLine($name) 199 | { 200 | return implode(',', $this->headers->get($name, [])); 201 | } 202 | 203 | /** 204 | * Return an instance with the provided value replacing the specified header. 205 | * 206 | * While header names are case-insensitive, the casing of the header will 207 | * be preserved by this function, and returned from getHeaders(). 208 | * 209 | * This method MUST be implemented in such a way as to retain the 210 | * immutability of the message, and MUST return an instance that has the 211 | * new and/or updated header and value. 212 | * 213 | * @param string $name Case-insensitive header field name. 214 | * @param string|string[] $value Header value(s). 215 | * @return static 216 | * @throws \InvalidArgumentException for invalid header names or values. 217 | */ 218 | public function withHeader($name, $value) 219 | { 220 | $clone = clone $this; 221 | $clone->headers->set($name, $value); 222 | 223 | return $clone; 224 | } 225 | 226 | /** 227 | * Return an instance with the specified header appended with the given value. 228 | * 229 | * Existing values for the specified header will be maintained. The new 230 | * value(s) will be appended to the existing list. If the header did not 231 | * exist previously, it will be added. 232 | * 233 | * This method MUST be implemented in such a way as to retain the 234 | * immutability of the message, and MUST return an instance that has the 235 | * new header and/or value. 236 | * 237 | * @param string $name Case-insensitive header field name to add. 238 | * @param string|string[] $value Header value(s). 239 | * @return static 240 | * @throws \InvalidArgumentException for invalid header names or values. 241 | */ 242 | public function withAddedHeader($name, $value) 243 | { 244 | $clone = clone $this; 245 | $clone->headers->add($name, $value); 246 | 247 | return $clone; 248 | } 249 | 250 | /** 251 | * Return an instance without the specified header. 252 | * 253 | * Header resolution MUST be done without case-sensitivity. 254 | * 255 | * This method MUST be implemented in such a way as to retain the 256 | * immutability of the message, and MUST return an instance that removes 257 | * the named header. 258 | * 259 | * @param string $name Case-insensitive header field name to remove. 260 | * @return static 261 | */ 262 | public function withoutHeader($name) 263 | { 264 | $clone = clone $this; 265 | $clone->headers->remove($name); 266 | 267 | return $clone; 268 | } 269 | 270 | /******************************************************************************* 271 | * Body 272 | ******************************************************************************/ 273 | 274 | /** 275 | * Gets the body of the message. 276 | * 277 | * @return StreamInterface Returns the body as a stream. 278 | */ 279 | public function getBody() 280 | { 281 | return $this->body; 282 | } 283 | 284 | /** 285 | * Return an instance with the specified message body. 286 | * 287 | * The body MUST be a StreamInterface object. 288 | * 289 | * This method MUST be implemented in such a way as to retain the 290 | * immutability of the message, and MUST return a new instance that has the 291 | * new body stream. 292 | * 293 | * @param StreamInterface $body Body. 294 | * @return static 295 | * @throws \InvalidArgumentException When the body is not valid. 296 | */ 297 | public function withBody(StreamInterface $body) 298 | { 299 | // TODO: Test for invalid body? 300 | $clone = clone $this; 301 | $clone->body = $body; 302 | 303 | return $clone; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /Slim/Http/UploadedFile.php: -------------------------------------------------------------------------------- 1 | has('slim.files')) { 87 | return $env['slim.files']; 88 | } elseif (isset($_FILES)) { 89 | return static::parseUploadedFiles($_FILES); 90 | } 91 | 92 | return []; 93 | } 94 | 95 | /** 96 | * Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data. 97 | * 98 | * @param array $uploadedFiles The non-normalized tree of uploaded file data. 99 | * 100 | * @return array A normalized tree of UploadedFile instances. 101 | */ 102 | private static function parseUploadedFiles(array $uploadedFiles) 103 | { 104 | $parsed = []; 105 | foreach ($uploadedFiles as $field => $uploadedFile) { 106 | if (!isset($uploadedFile['error'])) { 107 | if (is_array($uploadedFile)) { 108 | $parsed[$field] = static::parseUploadedFiles($uploadedFile); 109 | } 110 | continue; 111 | } 112 | 113 | $parsed[$field] = []; 114 | if (!is_array($uploadedFile['error'])) { 115 | $parsed[$field] = new static( 116 | $uploadedFile['tmp_name'], 117 | isset($uploadedFile['name']) ? $uploadedFile['name'] : null, 118 | isset($uploadedFile['type']) ? $uploadedFile['type'] : null, 119 | isset($uploadedFile['size']) ? $uploadedFile['size'] : null, 120 | $uploadedFile['error'], 121 | true 122 | ); 123 | } else { 124 | $subArray = []; 125 | foreach ($uploadedFile['error'] as $fileIdx => $error) { 126 | // normalise subarray and re-parse to move the input's keyname up a level 127 | $subArray[$fileIdx]['name'] = $uploadedFile['name'][$fileIdx]; 128 | $subArray[$fileIdx]['type'] = $uploadedFile['type'][$fileIdx]; 129 | $subArray[$fileIdx]['tmp_name'] = $uploadedFile['tmp_name'][$fileIdx]; 130 | $subArray[$fileIdx]['error'] = $uploadedFile['error'][$fileIdx]; 131 | $subArray[$fileIdx]['size'] = $uploadedFile['size'][$fileIdx]; 132 | 133 | $parsed[$field] = static::parseUploadedFiles($subArray); 134 | } 135 | } 136 | } 137 | 138 | return $parsed; 139 | } 140 | 141 | /** 142 | * Construct a new UploadedFile instance. 143 | * 144 | * @param string $file The full path to the uploaded file provided by the client. 145 | * @param string|null $name The file name. 146 | * @param string|null $type The file media type. 147 | * @param int|null $size The file size in bytes. 148 | * @param int $error The UPLOAD_ERR_XXX code representing the status of the upload. 149 | * @param bool $sapi Indicates if the upload is in a SAPI environment. 150 | */ 151 | public function __construct($file, $name = null, $type = null, $size = null, $error = UPLOAD_ERR_OK, $sapi = false) 152 | { 153 | $this->file = $file; 154 | $this->name = $name; 155 | $this->type = $type; 156 | $this->size = $size; 157 | $this->error = $error; 158 | $this->sapi = $sapi; 159 | } 160 | 161 | /** 162 | * Retrieve a stream representing the uploaded file. 163 | * 164 | * This method MUST return a StreamInterface instance, representing the 165 | * uploaded file. The purpose of this method is to allow utilizing native PHP 166 | * stream functionality to manipulate the file upload, such as 167 | * stream_copy_to_stream() (though the result will need to be decorated in a 168 | * native PHP stream wrapper to work with such functions). 169 | * 170 | * If the moveTo() method has been called previously, this method MUST raise 171 | * an exception. 172 | * 173 | * @return StreamInterface Stream representation of the uploaded file. 174 | * @throws \RuntimeException in cases when no stream is available or can be 175 | * created. 176 | */ 177 | public function getStream() 178 | { 179 | if ($this->moved) { 180 | throw new \RuntimeException(sprintf('Uploaded file %s has already been moved', $this->name)); 181 | } 182 | if ($this->stream === null) { 183 | $this->stream = new Stream(fopen($this->file, 'r')); 184 | } 185 | 186 | return $this->stream; 187 | } 188 | 189 | /** 190 | * Move the uploaded file to a new location. 191 | * 192 | * Use this method as an alternative to move_uploaded_file(). This method is 193 | * guaranteed to work in both SAPI and non-SAPI environments. 194 | * Implementations must determine which environment they are in, and use the 195 | * appropriate method (move_uploaded_file(), rename(), or a stream 196 | * operation) to perform the operation. 197 | * 198 | * $targetPath may be an absolute path, or a relative path. If it is a 199 | * relative path, resolution should be the same as used by PHP's rename() 200 | * function. 201 | * 202 | * The original file or stream MUST be removed on completion. 203 | * 204 | * If this method is called more than once, any subsequent calls MUST raise 205 | * an exception. 206 | * 207 | * When used in an SAPI environment where $_FILES is populated, when writing 208 | * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be 209 | * used to ensure permissions and upload status are verified correctly. 210 | * 211 | * If you wish to move to a stream, use getStream(), as SAPI operations 212 | * cannot guarantee writing to stream destinations. 213 | * 214 | * @see http://php.net/is_uploaded_file 215 | * @see http://php.net/move_uploaded_file 216 | * 217 | * @param string $targetPath Path to which to move the uploaded file. 218 | * 219 | * @throws InvalidArgumentException if the $path specified is invalid. 220 | * @throws RuntimeException on any error during the move operation, or on 221 | * the second or subsequent call to the method. 222 | */ 223 | public function moveTo($targetPath) 224 | { 225 | if ($this->moved) { 226 | throw new RuntimeException('Uploaded file already moved'); 227 | } 228 | 229 | $targetIsStream = strpos($targetPath, '://') > 0; 230 | if (!$targetIsStream && !is_writable(dirname($targetPath))) { 231 | throw new InvalidArgumentException('Upload target path is not writable'); 232 | } 233 | 234 | if ($targetIsStream) { 235 | if (!copy($this->file, $targetPath)) { 236 | throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); 237 | } 238 | if (!unlink($this->file)) { 239 | throw new RuntimeException(sprintf('Error removing uploaded file %s', $this->name)); 240 | } 241 | } elseif ($this->sapi) { 242 | if (!is_uploaded_file($this->file)) { 243 | throw new RuntimeException(sprintf('%s is not a valid uploaded file', $this->file)); 244 | } 245 | 246 | if (!move_uploaded_file($this->file, $targetPath)) { 247 | throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); 248 | } 249 | } else { 250 | if (!rename($this->file, $targetPath)) { 251 | throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); 252 | } 253 | } 254 | 255 | $this->moved = true; 256 | } 257 | 258 | /** 259 | * Retrieve the error associated with the uploaded file. 260 | * 261 | * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. 262 | * 263 | * If the file was uploaded successfully, this method MUST return 264 | * UPLOAD_ERR_OK. 265 | * 266 | * Implementations SHOULD return the value stored in the "error" key of 267 | * the file in the $_FILES array. 268 | * 269 | * @see http://php.net/manual/en/features.file-upload.errors.php 270 | * 271 | * @return int One of PHP's UPLOAD_ERR_XXX constants. 272 | */ 273 | public function getError() 274 | { 275 | return $this->error; 276 | } 277 | 278 | /** 279 | * Retrieve the filename sent by the client. 280 | * 281 | * Do not trust the value returned by this method. A client could send 282 | * a malicious filename with the intention to corrupt or hack your 283 | * application. 284 | * 285 | * Implementations SHOULD return the value stored in the "name" key of 286 | * the file in the $_FILES array. 287 | * 288 | * @return string|null The filename sent by the client or null if none 289 | * was provided. 290 | */ 291 | public function getClientFilename() 292 | { 293 | return $this->name; 294 | } 295 | 296 | /** 297 | * Retrieve the media type sent by the client. 298 | * 299 | * Do not trust the value returned by this method. A client could send 300 | * a malicious media type with the intention to corrupt or hack your 301 | * application. 302 | * 303 | * Implementations SHOULD return the value stored in the "type" key of 304 | * the file in the $_FILES array. 305 | * 306 | * @return string|null The media type sent by the client or null if none 307 | * was provided. 308 | */ 309 | public function getClientMediaType() 310 | { 311 | return $this->type; 312 | } 313 | 314 | /** 315 | * Retrieve the file size. 316 | * 317 | * Implementations SHOULD return the value stored in the "size" key of 318 | * the file in the $_FILES array if available, as PHP calculates this based 319 | * on the actual size transmitted. 320 | * 321 | * @return int|null The file size in bytes or null if unknown. 322 | */ 323 | public function getSize() 324 | { 325 | return $this->size; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /Slim/Http/Stream.php: -------------------------------------------------------------------------------- 1 | ['r', 'r+', 'w+', 'a+', 'x+', 'c+'], 37 | 'writable' => ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'], 38 | ]; 39 | 40 | /** 41 | * The underlying stream resource 42 | * 43 | * @var resource 44 | */ 45 | protected $stream; 46 | 47 | /** 48 | * Stream metadata 49 | * 50 | * @var array 51 | */ 52 | protected $meta; 53 | 54 | /** 55 | * Is this stream readable? 56 | * 57 | * @var bool 58 | */ 59 | protected $readable; 60 | 61 | /** 62 | * Is this stream writable? 63 | * 64 | * @var bool 65 | */ 66 | protected $writable; 67 | 68 | /** 69 | * Is this stream seekable? 70 | * 71 | * @var bool 72 | */ 73 | protected $seekable; 74 | 75 | /** 76 | * The size of the stream if known 77 | * 78 | * @var null|int 79 | */ 80 | protected $size; 81 | 82 | /** 83 | * Is this stream a pipe? 84 | * 85 | * @var bool 86 | */ 87 | protected $isPipe; 88 | 89 | /** 90 | * Create a new Stream. 91 | * 92 | * @param resource $stream A PHP resource handle. 93 | * 94 | * @throws InvalidArgumentException If argument is not a resource. 95 | */ 96 | public function __construct($stream) 97 | { 98 | $this->attach($stream); 99 | } 100 | 101 | /** 102 | * Get stream metadata as an associative array or retrieve a specific key. 103 | * 104 | * The keys returned are identical to the keys returned from PHP's 105 | * stream_get_meta_data() function. 106 | * 107 | * @link http://php.net/manual/en/function.stream-get-meta-data.php 108 | * 109 | * @param string $key Specific metadata to retrieve. 110 | * 111 | * @return array|mixed|null Returns an associative array if no key is 112 | * provided. Returns a specific key value if a key is provided and the 113 | * value is found, or null if the key is not found. 114 | */ 115 | public function getMetadata($key = null) 116 | { 117 | $this->meta = stream_get_meta_data($this->stream); 118 | if (is_null($key) === true) { 119 | return $this->meta; 120 | } 121 | 122 | return isset($this->meta[$key]) ? $this->meta[$key] : null; 123 | } 124 | 125 | /** 126 | * Is a resource attached to this stream? 127 | * 128 | * Note: This method is not part of the PSR-7 standard. 129 | * 130 | * @return bool 131 | */ 132 | protected function isAttached() 133 | { 134 | return is_resource($this->stream); 135 | } 136 | 137 | /** 138 | * Attach new resource to this object. 139 | * 140 | * Note: This method is not part of the PSR-7 standard. 141 | * 142 | * @param resource $newStream A PHP resource handle. 143 | * 144 | * @throws InvalidArgumentException If argument is not a valid PHP resource. 145 | */ 146 | protected function attach($newStream) 147 | { 148 | if (is_resource($newStream) === false) { 149 | throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource'); 150 | } 151 | 152 | if ($this->isAttached() === true) { 153 | $this->detach(); 154 | } 155 | 156 | $this->stream = $newStream; 157 | } 158 | 159 | /** 160 | * Separates any underlying resources from the stream. 161 | * 162 | * After the stream has been detached, the stream is in an unusable state. 163 | * 164 | * @return resource|null Underlying PHP stream, if any 165 | */ 166 | public function detach() 167 | { 168 | $oldResource = $this->stream; 169 | $this->stream = null; 170 | $this->meta = null; 171 | $this->readable = null; 172 | $this->writable = null; 173 | $this->seekable = null; 174 | $this->size = null; 175 | $this->isPipe = null; 176 | 177 | return $oldResource; 178 | } 179 | 180 | /** 181 | * Reads all data from the stream into a string, from the beginning to end. 182 | * 183 | * This method MUST attempt to seek to the beginning of the stream before 184 | * reading data and read the stream until the end is reached. 185 | * 186 | * Warning: This could attempt to load a large amount of data into memory. 187 | * 188 | * This method MUST NOT raise an exception in order to conform with PHP's 189 | * string casting operations. 190 | * 191 | * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring 192 | * @return string 193 | */ 194 | public function __toString() 195 | { 196 | if (!$this->isAttached()) { 197 | return ''; 198 | } 199 | 200 | try { 201 | $this->rewind(); 202 | return $this->getContents(); 203 | } catch (RuntimeException $e) { 204 | return ''; 205 | } 206 | } 207 | 208 | /** 209 | * Closes the stream and any underlying resources. 210 | */ 211 | public function close() 212 | { 213 | if ($this->isAttached() === true) { 214 | if ($this->isPipe()) { 215 | pclose($this->stream); 216 | } else { 217 | fclose($this->stream); 218 | } 219 | } 220 | 221 | $this->detach(); 222 | } 223 | 224 | /** 225 | * Get the size of the stream if known. 226 | * 227 | * @return int|null Returns the size in bytes if known, or null if unknown. 228 | */ 229 | public function getSize() 230 | { 231 | if (!$this->size && $this->isAttached() === true) { 232 | $stats = fstat($this->stream); 233 | $this->size = isset($stats['size']) && !$this->isPipe() ? $stats['size'] : null; 234 | } 235 | 236 | return $this->size; 237 | } 238 | 239 | /** 240 | * Returns the current position of the file read/write pointer 241 | * 242 | * @return int Position of the file pointer 243 | * 244 | * @throws RuntimeException on error. 245 | */ 246 | public function tell() 247 | { 248 | if (!$this->isAttached() || ($position = ftell($this->stream)) === false || $this->isPipe()) { 249 | throw new RuntimeException('Could not get the position of the pointer in stream'); 250 | } 251 | 252 | return $position; 253 | } 254 | 255 | /** 256 | * Returns true if the stream is at the end of the stream. 257 | * 258 | * @return bool 259 | */ 260 | public function eof() 261 | { 262 | return $this->isAttached() ? feof($this->stream) : true; 263 | } 264 | 265 | /** 266 | * Returns whether or not the stream is readable. 267 | * 268 | * @return bool 269 | */ 270 | public function isReadable() 271 | { 272 | if ($this->readable === null) { 273 | if ($this->isPipe()) { 274 | $this->readable = true; 275 | } else { 276 | $this->readable = false; 277 | if ($this->isAttached()) { 278 | $meta = $this->getMetadata(); 279 | foreach (self::$modes['readable'] as $mode) { 280 | if (strpos($meta['mode'], $mode) === 0) { 281 | $this->readable = true; 282 | break; 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | return $this->readable; 290 | } 291 | 292 | /** 293 | * Returns whether or not the stream is writable. 294 | * 295 | * @return bool 296 | */ 297 | public function isWritable() 298 | { 299 | if ($this->writable === null) { 300 | $this->writable = false; 301 | if ($this->isAttached()) { 302 | $meta = $this->getMetadata(); 303 | foreach (self::$modes['writable'] as $mode) { 304 | if (strpos($meta['mode'], $mode) === 0) { 305 | $this->writable = true; 306 | break; 307 | } 308 | } 309 | } 310 | } 311 | 312 | return $this->writable; 313 | } 314 | 315 | /** 316 | * Returns whether or not the stream is seekable. 317 | * 318 | * @return bool 319 | */ 320 | public function isSeekable() 321 | { 322 | if ($this->seekable === null) { 323 | $this->seekable = false; 324 | if ($this->isAttached()) { 325 | $meta = $this->getMetadata(); 326 | $this->seekable = !$this->isPipe() && $meta['seekable']; 327 | } 328 | } 329 | 330 | return $this->seekable; 331 | } 332 | 333 | /** 334 | * Seek to a position in the stream. 335 | * 336 | * @link http://www.php.net/manual/en/function.fseek.php 337 | * 338 | * @param int $offset Stream offset 339 | * @param int $whence Specifies how the cursor position will be calculated 340 | * based on the seek offset. Valid values are identical to the built-in 341 | * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to 342 | * offset bytes SEEK_CUR: Set position to current location plus offset 343 | * SEEK_END: Set position to end-of-stream plus offset. 344 | * 345 | * @throws RuntimeException on failure. 346 | */ 347 | public function seek($offset, $whence = SEEK_SET) 348 | { 349 | // Note that fseek returns 0 on success! 350 | if (!$this->isSeekable() || fseek($this->stream, $offset, $whence) === -1) { 351 | throw new RuntimeException('Could not seek in stream'); 352 | } 353 | } 354 | 355 | /** 356 | * Seek to the beginning of the stream. 357 | * 358 | * If the stream is not seekable, this method will raise an exception; 359 | * otherwise, it will perform a seek(0). 360 | * 361 | * @see seek() 362 | * 363 | * @link http://www.php.net/manual/en/function.fseek.php 364 | * 365 | * @throws RuntimeException on failure. 366 | */ 367 | public function rewind() 368 | { 369 | if (!$this->isSeekable() || rewind($this->stream) === false) { 370 | throw new RuntimeException('Could not rewind stream'); 371 | } 372 | } 373 | 374 | /** 375 | * Read data from the stream. 376 | * 377 | * @param int $length Read up to $length bytes from the object and return 378 | * them. Fewer than $length bytes may be returned if underlying stream 379 | * call returns fewer bytes. 380 | * 381 | * @return string Returns the data read from the stream, or an empty string 382 | * if no bytes are available. 383 | * 384 | * @throws RuntimeException if an error occurs. 385 | */ 386 | public function read($length) 387 | { 388 | if (!$this->isReadable() || ($data = fread($this->stream, $length)) === false) { 389 | throw new RuntimeException('Could not read from stream'); 390 | } 391 | 392 | return $data; 393 | } 394 | 395 | /** 396 | * Write data to the stream. 397 | * 398 | * @param string $string The string that is to be written. 399 | * 400 | * @return int Returns the number of bytes written to the stream. 401 | * 402 | * @throws RuntimeException on failure. 403 | */ 404 | public function write($string) 405 | { 406 | if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) { 407 | throw new RuntimeException('Could not write to stream'); 408 | } 409 | 410 | // reset size so that it will be recalculated on next call to getSize() 411 | $this->size = null; 412 | 413 | return $written; 414 | } 415 | 416 | /** 417 | * Returns the remaining contents in a string 418 | * 419 | * @return string 420 | * 421 | * @throws RuntimeException if unable to read or an error occurs while 422 | * reading. 423 | */ 424 | public function getContents() 425 | { 426 | if (!$this->isReadable() || ($contents = stream_get_contents($this->stream)) === false) { 427 | throw new RuntimeException('Could not get contents of stream'); 428 | } 429 | 430 | return $contents; 431 | } 432 | 433 | /** 434 | * Returns whether or not the stream is a pipe. 435 | * 436 | * @return bool 437 | */ 438 | public function isPipe() 439 | { 440 | if ($this->isPipe === null) { 441 | $this->isPipe = false; 442 | if ($this->isAttached()) { 443 | $mode = fstat($this->stream)['mode']; 444 | $this->isPipe = ($mode & self::FSTAT_MODE_S_IFIFO) !== 0; 445 | } 446 | } 447 | 448 | return $this->isPipe; 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /Slim/Router.php: -------------------------------------------------------------------------------- 1 | routeParser = $parser ?: new StdParser; 94 | } 95 | 96 | /** 97 | * Set the base path used in pathFor() 98 | * 99 | * @param string $basePath 100 | * 101 | * @return self 102 | */ 103 | public function setBasePath($basePath) 104 | { 105 | if (!is_string($basePath)) { 106 | throw new InvalidArgumentException('Router basePath must be a string'); 107 | } 108 | 109 | $this->basePath = $basePath; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set path to fast route cache file. If this is false then route caching is disabled. 116 | * 117 | * @param string|false $cacheFile 118 | * 119 | * @return self 120 | */ 121 | public function setCacheFile($cacheFile) 122 | { 123 | if (!is_string($cacheFile) && $cacheFile !== false) { 124 | throw new InvalidArgumentException('Router cacheFile must be a string or false'); 125 | } 126 | 127 | $this->cacheFile = $cacheFile; 128 | 129 | if ($cacheFile !== false && !is_writable(dirname($cacheFile))) { 130 | throw new RuntimeException('Router cacheFile directory must be writable'); 131 | } 132 | 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * @param ContainerInterface $container 139 | */ 140 | public function setContainer(ContainerInterface $container) 141 | { 142 | $this->container = $container; 143 | } 144 | 145 | /** 146 | * Add route 147 | * 148 | * @param string[] $methods Array of HTTP methods 149 | * @param string $pattern The route pattern 150 | * @param callable $handler The route callable 151 | * 152 | * @return RouteInterface 153 | * 154 | * @throws InvalidArgumentException if the route pattern isn't a string 155 | */ 156 | public function map($methods, $pattern, $handler) 157 | { 158 | if (!is_string($pattern)) { 159 | throw new InvalidArgumentException('Route pattern must be a string'); 160 | } 161 | 162 | // Prepend parent group pattern(s) 163 | if ($this->routeGroups) { 164 | $pattern = $this->processGroups() . $pattern; 165 | } 166 | 167 | // According to RFC methods are defined in uppercase (See RFC 7231) 168 | $methods = array_map("strtoupper", $methods); 169 | 170 | // Add route 171 | $route = $this->createRoute($methods, $pattern, $handler); 172 | $this->routes[$route->getIdentifier()] = $route; 173 | $this->routeCounter++; 174 | 175 | return $route; 176 | } 177 | 178 | /** 179 | * Dispatch router for HTTP request 180 | * 181 | * @param ServerRequestInterface $request The current HTTP request object 182 | * 183 | * @return array 184 | * 185 | * @link https://github.com/nikic/FastRoute/blob/master/src/Dispatcher.php 186 | */ 187 | public function dispatch(ServerRequestInterface $request) 188 | { 189 | $uri = '/' . ltrim($request->getUri()->getPath(), '/'); 190 | 191 | return $this->createDispatcher()->dispatch( 192 | $request->getMethod(), 193 | $uri 194 | ); 195 | } 196 | 197 | /** 198 | * Create a new Route object 199 | * 200 | * @param string[] $methods Array of HTTP methods 201 | * @param string $pattern The route pattern 202 | * @param callable $callable The route callable 203 | * 204 | * @return \Slim\Interfaces\RouteInterface 205 | */ 206 | protected function createRoute($methods, $pattern, $callable) 207 | { 208 | $route = new Route($methods, $pattern, $callable, $this->routeGroups, $this->routeCounter); 209 | if (!empty($this->container)) { 210 | $route->setContainer($this->container); 211 | } 212 | 213 | return $route; 214 | } 215 | 216 | /** 217 | * @return \FastRoute\Dispatcher 218 | */ 219 | protected function createDispatcher() 220 | { 221 | if ($this->dispatcher) { 222 | return $this->dispatcher; 223 | } 224 | 225 | $routeDefinitionCallback = function (RouteCollector $r) { 226 | foreach ($this->getRoutes() as $route) { 227 | $r->addRoute($route->getMethods(), $route->getPattern(), $route->getIdentifier()); 228 | } 229 | }; 230 | 231 | if ($this->cacheFile) { 232 | $this->dispatcher = \FastRoute\cachedDispatcher($routeDefinitionCallback, [ 233 | 'routeParser' => $this->routeParser, 234 | 'cacheFile' => $this->cacheFile, 235 | ]); 236 | } else { 237 | $this->dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback, [ 238 | 'routeParser' => $this->routeParser, 239 | ]); 240 | } 241 | 242 | return $this->dispatcher; 243 | } 244 | 245 | /** 246 | * @param \FastRoute\Dispatcher $dispatcher 247 | */ 248 | public function setDispatcher(Dispatcher $dispatcher) 249 | { 250 | $this->dispatcher = $dispatcher; 251 | } 252 | 253 | /** 254 | * Get route objects 255 | * 256 | * @return Route[] 257 | */ 258 | public function getRoutes() 259 | { 260 | return $this->routes; 261 | } 262 | 263 | /** 264 | * Get named route object 265 | * 266 | * @param string $name Route name 267 | * 268 | * @return Route 269 | * 270 | * @throws RuntimeException If named route does not exist 271 | */ 272 | public function getNamedRoute($name) 273 | { 274 | foreach ($this->routes as $route) { 275 | if ($name == $route->getName()) { 276 | return $route; 277 | } 278 | } 279 | throw new RuntimeException('Named route does not exist for name: ' . $name); 280 | } 281 | 282 | /** 283 | * Remove named route 284 | * 285 | * @param string $name Route name 286 | * 287 | * @throws RuntimeException If named route does not exist 288 | */ 289 | public function removeNamedRoute($name) 290 | { 291 | $route = $this->getNamedRoute($name); 292 | 293 | // no exception, route exists, now remove by id 294 | unset($this->routes[$route->getIdentifier()]); 295 | } 296 | 297 | /** 298 | * Process route groups 299 | * 300 | * @return string A group pattern to prefix routes with 301 | */ 302 | protected function processGroups() 303 | { 304 | $pattern = ""; 305 | foreach ($this->routeGroups as $group) { 306 | $pattern .= $group->getPattern(); 307 | } 308 | return $pattern; 309 | } 310 | 311 | /** 312 | * Add a route group to the array 313 | * 314 | * @param string $pattern 315 | * @param callable $callable 316 | * 317 | * @return RouteGroupInterface 318 | */ 319 | public function pushGroup($pattern, $callable) 320 | { 321 | $group = new RouteGroup($pattern, $callable); 322 | array_push($this->routeGroups, $group); 323 | return $group; 324 | } 325 | 326 | /** 327 | * Removes the last route group from the array 328 | * 329 | * @return RouteGroup|bool The RouteGroup if successful, else False 330 | */ 331 | public function popGroup() 332 | { 333 | $group = array_pop($this->routeGroups); 334 | return $group instanceof RouteGroup ? $group : false; 335 | } 336 | 337 | /** 338 | * @param $identifier 339 | * @return \Slim\Interfaces\RouteInterface 340 | */ 341 | public function lookupRoute($identifier) 342 | { 343 | if (!isset($this->routes[$identifier])) { 344 | throw new RuntimeException('Route not found, looks like your route cache is stale.'); 345 | } 346 | return $this->routes[$identifier]; 347 | } 348 | 349 | /** 350 | * Build the path for a named route excluding the base path 351 | * 352 | * @param string $name Route name 353 | * @param array $data Named argument replacement data 354 | * @param array $queryParams Optional query string parameters 355 | * 356 | * @return string 357 | * 358 | * @throws RuntimeException If named route does not exist 359 | * @throws InvalidArgumentException If required data not provided 360 | */ 361 | public function relativePathFor($name, array $data = [], array $queryParams = []) 362 | { 363 | $route = $this->getNamedRoute($name); 364 | $pattern = $route->getPattern(); 365 | 366 | $routeDatas = $this->routeParser->parse($pattern); 367 | // $routeDatas is an array of all possible routes that can be made. There is 368 | // one routedata for each optional parameter plus one for no optional parameters. 369 | // 370 | // The most specific is last, so we look for that first. 371 | $routeDatas = array_reverse($routeDatas); 372 | 373 | $segments = []; 374 | foreach ($routeDatas as $routeData) { 375 | foreach ($routeData as $item) { 376 | if (is_string($item)) { 377 | // this segment is a static string 378 | $segments[] = $item; 379 | continue; 380 | } 381 | 382 | // This segment has a parameter: first element is the name 383 | if (!array_key_exists($item[0], $data)) { 384 | // we don't have a data element for this segment: cancel 385 | // testing this routeData item, so that we can try a less 386 | // specific routeData item. 387 | $segments = []; 388 | $segmentName = $item[0]; 389 | break; 390 | } 391 | $segments[] = $data[$item[0]]; 392 | } 393 | if (!empty($segments)) { 394 | // we found all the parameters for this route data, no need to check 395 | // less specific ones 396 | break; 397 | } 398 | } 399 | 400 | if (empty($segments)) { 401 | throw new InvalidArgumentException('Missing data for URL segment: ' . $segmentName); 402 | } 403 | $url = implode('', $segments); 404 | 405 | if ($queryParams) { 406 | $url .= '?' . http_build_query($queryParams); 407 | } 408 | 409 | return $url; 410 | } 411 | 412 | 413 | /** 414 | * Build the path for a named route including the base path 415 | * 416 | * @param string $name Route name 417 | * @param array $data Named argument replacement data 418 | * @param array $queryParams Optional query string parameters 419 | * 420 | * @return string 421 | * 422 | * @throws RuntimeException If named route does not exist 423 | * @throws InvalidArgumentException If required data not provided 424 | */ 425 | public function pathFor($name, array $data = [], array $queryParams = []) 426 | { 427 | $url = $this->relativePathFor($name, $data, $queryParams); 428 | 429 | if ($this->basePath) { 430 | $url = $this->basePath . $url; 431 | } 432 | 433 | return $url; 434 | } 435 | 436 | /** 437 | * Build the path for a named route. 438 | * 439 | * This method is deprecated. Use pathFor() from now on. 440 | * 441 | * @param string $name Route name 442 | * @param array $data Named argument replacement data 443 | * @param array $queryParams Optional query string parameters 444 | * 445 | * @return string 446 | * 447 | * @throws RuntimeException If named route does not exist 448 | * @throws InvalidArgumentException If required data not provided 449 | */ 450 | public function urlFor($name, array $data = [], array $queryParams = []) 451 | { 452 | trigger_error('urlFor() is deprecated. Use pathFor() instead.', E_USER_DEPRECATED); 453 | return $this->pathFor($name, $data, $queryParams); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /Slim/Http/Response.php: -------------------------------------------------------------------------------- 1 | 'Continue', 51 | StatusCode::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols', 52 | StatusCode::HTTP_PROCESSING => 'Processing', 53 | //Successful 2xx 54 | StatusCode::HTTP_OK => 'OK', 55 | StatusCode::HTTP_CREATED => 'Created', 56 | StatusCode::HTTP_ACCEPTED => 'Accepted', 57 | StatusCode::HTTP_NONAUTHORITATIVE_INFORMATION => 'Non-Authoritative Information', 58 | StatusCode::HTTP_NO_CONTENT => 'No Content', 59 | StatusCode::HTTP_RESET_CONTENT => 'Reset Content', 60 | StatusCode::HTTP_PARTIAL_CONTENT => 'Partial Content', 61 | StatusCode::HTTP_MULTI_STATUS => 'Multi-Status', 62 | StatusCode::HTTP_ALREADY_REPORTED => 'Already Reported', 63 | StatusCode::HTTP_IM_USED => 'IM Used', 64 | //Redirection 3xx 65 | StatusCode::HTTP_MULTIPLE_CHOICES => 'Multiple Choices', 66 | StatusCode::HTTP_MOVED_PERMANENTLY => 'Moved Permanently', 67 | StatusCode::HTTP_FOUND => 'Found', 68 | StatusCode::HTTP_SEE_OTHER => 'See Other', 69 | StatusCode::HTTP_NOT_MODIFIED => 'Not Modified', 70 | StatusCode::HTTP_USE_PROXY => 'Use Proxy', 71 | StatusCode::HTTP_UNUSED => '(Unused)', 72 | StatusCode::HTTP_TEMPORARY_REDIRECT => 'Temporary Redirect', 73 | StatusCode::HTTP_PERMANENT_REDIRECT => 'Permanent Redirect', 74 | //Client Error 4xx 75 | StatusCode::HTTP_BAD_REQUEST => 'Bad Request', 76 | StatusCode::HTTP_UNAUTHORIZED => 'Unauthorized', 77 | StatusCode::HTTP_PAYMENT_REQUIRED => 'Payment Required', 78 | StatusCode::HTTP_FORBIDDEN => 'Forbidden', 79 | StatusCode::HTTP_NOT_FOUND => 'Not Found', 80 | StatusCode::HTTP_METHOD_NOT_ALLOWED => 'Method Not Allowed', 81 | StatusCode::HTTP_NOT_ACCEPTABLE => 'Not Acceptable', 82 | StatusCode::HTTP_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', 83 | StatusCode::HTTP_REQUEST_TIMEOUT => 'Request Timeout', 84 | StatusCode::HTTP_CONFLICT => 'Conflict', 85 | StatusCode::HTTP_GONE => 'Gone', 86 | StatusCode::HTTP_LENGTH_REQUIRED => 'Length Required', 87 | StatusCode::HTTP_PRECONDITION_FAILED => 'Precondition Failed', 88 | StatusCode::HTTP_REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large', 89 | StatusCode::HTTP_REQUEST_URI_TOO_LONG => 'Request-URI Too Long', 90 | StatusCode::HTTP_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', 91 | StatusCode::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable', 92 | StatusCode::HTTP_EXPECTATION_FAILED => 'Expectation Failed', 93 | StatusCode::HTTP_IM_A_TEAPOT => 'I\'m a teapot', 94 | StatusCode::HTTP_MISDIRECTED_REQUEST => 'Misdirected Request', 95 | StatusCode::HTTP_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', 96 | StatusCode::HTTP_LOCKED => 'Locked', 97 | StatusCode::HTTP_FAILED_DEPENDENCY => 'Failed Dependency', 98 | StatusCode::HTTP_UPGRADE_REQUIRED => 'Upgrade Required', 99 | StatusCode::HTTP_PRECONDITION_REQUIRED => 'Precondition Required', 100 | StatusCode::HTTP_TOO_MANY_REQUESTS => 'Too Many Requests', 101 | StatusCode::HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', 102 | StatusCode::HTTP_CONNECTION_CLOSED_WITHOUT_RESPONSE => 'Connection Closed Without Response', 103 | StatusCode::HTTP_UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons', 104 | StatusCode::HTTP_CLIENT_CLOSED_REQUEST => 'Client Closed Request', 105 | //Server Error 5xx 106 | StatusCode::HTTP_INTERNAL_SERVER_ERROR => 'Internal Server Error', 107 | StatusCode::HTTP_NOT_IMPLEMENTED => 'Not Implemented', 108 | StatusCode::HTTP_BAD_GATEWAY => 'Bad Gateway', 109 | StatusCode::HTTP_SERVICE_UNAVAILABLE => 'Service Unavailable', 110 | StatusCode::HTTP_GATEWAY_TIMEOUT => 'Gateway Timeout', 111 | StatusCode::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported', 112 | StatusCode::HTTP_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates', 113 | StatusCode::HTTP_INSUFFICIENT_STORAGE => 'Insufficient Storage', 114 | StatusCode::HTTP_LOOP_DETECTED => 'Loop Detected', 115 | StatusCode::HTTP_NOT_EXTENDED => 'Not Extended', 116 | StatusCode::HTTP_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', 117 | StatusCode::HTTP_NETWORK_CONNECTION_TIMEOUT_ERROR => 'Network Connect Timeout Error', 118 | ]; 119 | 120 | /** 121 | * EOL characters used for HTTP response. 122 | * 123 | * @var string 124 | */ 125 | const EOL = "\r\n"; 126 | 127 | /** 128 | * Create new HTTP response. 129 | * 130 | * @param int $status The response status code. 131 | * @param HeadersInterface|null $headers The response headers. 132 | * @param StreamInterface|null $body The response body. 133 | */ 134 | public function __construct( 135 | $status = StatusCode::HTTP_OK, 136 | HeadersInterface $headers = null, 137 | StreamInterface $body = null 138 | ) { 139 | $this->status = $this->filterStatus($status); 140 | $this->headers = $headers ? $headers : new Headers(); 141 | $this->body = $body ? $body : new Body(fopen('php://temp', 'r+')); 142 | } 143 | 144 | /** 145 | * This method is applied to the cloned object 146 | * after PHP performs an initial shallow-copy. This 147 | * method completes a deep-copy by creating new objects 148 | * for the cloned object's internal reference pointers. 149 | */ 150 | public function __clone() 151 | { 152 | $this->headers = clone $this->headers; 153 | } 154 | 155 | /******************************************************************************* 156 | * Status 157 | ******************************************************************************/ 158 | 159 | /** 160 | * Gets the response status code. 161 | * 162 | * The status code is a 3-digit integer result code of the server's attempt 163 | * to understand and satisfy the request. 164 | * 165 | * @return int Status code. 166 | */ 167 | public function getStatusCode() 168 | { 169 | return $this->status; 170 | } 171 | 172 | /** 173 | * Return an instance with the specified status code and, optionally, reason phrase. 174 | * 175 | * If no reason phrase is specified, implementations MAY choose to default 176 | * to the RFC 7231 or IANA recommended reason phrase for the response's 177 | * status code. 178 | * 179 | * This method MUST be implemented in such a way as to retain the 180 | * immutability of the message, and MUST return an instance that has the 181 | * updated status and reason phrase. 182 | * 183 | * @link http://tools.ietf.org/html/rfc7231#section-6 184 | * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 185 | * @param int $code The 3-digit integer result code to set. 186 | * @param string $reasonPhrase The reason phrase to use with the 187 | * provided status code; if none is provided, implementations MAY 188 | * use the defaults as suggested in the HTTP specification. 189 | * @return static 190 | * @throws \InvalidArgumentException For invalid status code arguments. 191 | */ 192 | public function withStatus($code, $reasonPhrase = '') 193 | { 194 | $code = $this->filterStatus($code); 195 | 196 | if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) { 197 | throw new InvalidArgumentException('ReasonPhrase must be a string'); 198 | } 199 | 200 | $clone = clone $this; 201 | $clone->status = $code; 202 | if ($reasonPhrase === '' && isset(static::$messages[$code])) { 203 | $reasonPhrase = static::$messages[$code]; 204 | } 205 | 206 | if ($reasonPhrase === '') { 207 | throw new InvalidArgumentException('ReasonPhrase must be supplied for this code'); 208 | } 209 | 210 | $clone->reasonPhrase = $reasonPhrase; 211 | 212 | return $clone; 213 | } 214 | 215 | /** 216 | * Filter HTTP status code. 217 | * 218 | * @param int $status HTTP status code. 219 | * @return int 220 | * @throws \InvalidArgumentException If an invalid HTTP status code is provided. 221 | */ 222 | protected function filterStatus($status) 223 | { 224 | if (!is_integer($status) || 225 | $statusStatusCode::HTTP_NETWORK_CONNECTION_TIMEOUT_ERROR 227 | ) { 228 | throw new InvalidArgumentException('Invalid HTTP status code'); 229 | } 230 | 231 | return $status; 232 | } 233 | 234 | /** 235 | * Gets the response reason phrase associated with the status code. 236 | * 237 | * Because a reason phrase is not a required element in a response 238 | * status line, the reason phrase value MAY be null. Implementations MAY 239 | * choose to return the default RFC 7231 recommended reason phrase (or those 240 | * listed in the IANA HTTP Status Code Registry) for the response's 241 | * status code. 242 | * 243 | * @link http://tools.ietf.org/html/rfc7231#section-6 244 | * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 245 | * @return string Reason phrase; must return an empty string if none present. 246 | */ 247 | public function getReasonPhrase() 248 | { 249 | if ($this->reasonPhrase) { 250 | return $this->reasonPhrase; 251 | } 252 | if (isset(static::$messages[$this->status])) { 253 | return static::$messages[$this->status]; 254 | } 255 | return ''; 256 | } 257 | 258 | /******************************************************************************* 259 | * Headers 260 | ******************************************************************************/ 261 | 262 | /** 263 | * Return an instance with the provided value replacing the specified header. 264 | * 265 | * If a Location header is set and the status code is 200, then set the status 266 | * code to 302 to mimic what PHP does. See https://github.com/slimphp/Slim/issues/1730 267 | * 268 | * @param string $name Case-insensitive header field name. 269 | * @param string|string[] $value Header value(s). 270 | * @return static 271 | * @throws \InvalidArgumentException for invalid header names or values. 272 | */ 273 | public function withHeader($name, $value) 274 | { 275 | $clone = clone $this; 276 | $clone->headers->set($name, $value); 277 | 278 | if ($clone->getStatusCode() === StatusCode::HTTP_OK && strtolower($name) === 'location') { 279 | $clone = $clone->withStatus(StatusCode::HTTP_FOUND); 280 | } 281 | 282 | return $clone; 283 | } 284 | 285 | 286 | /******************************************************************************* 287 | * Body 288 | ******************************************************************************/ 289 | 290 | /** 291 | * Write data to the response body. 292 | * 293 | * Note: This method is not part of the PSR-7 standard. 294 | * 295 | * Proxies to the underlying stream and writes the provided data to it. 296 | * 297 | * @param string $data 298 | * @return $this 299 | */ 300 | public function write($data) 301 | { 302 | $this->getBody()->write($data); 303 | 304 | return $this; 305 | } 306 | 307 | /******************************************************************************* 308 | * Response Helpers 309 | ******************************************************************************/ 310 | 311 | /** 312 | * Redirect. 313 | * 314 | * Note: This method is not part of the PSR-7 standard. 315 | * 316 | * This method prepares the response object to return an HTTP Redirect 317 | * response to the client. 318 | * 319 | * @param string|UriInterface $url The redirect destination. 320 | * @param int|null $status The redirect HTTP status code. 321 | * @return static 322 | */ 323 | public function withRedirect($url, $status = null) 324 | { 325 | $responseWithRedirect = $this->withHeader('Location', (string)$url); 326 | 327 | if (is_null($status) && $this->getStatusCode() === StatusCode::HTTP_OK) { 328 | $status = StatusCode::HTTP_FOUND; 329 | } 330 | 331 | if (!is_null($status)) { 332 | return $responseWithRedirect->withStatus($status); 333 | } 334 | 335 | return $responseWithRedirect; 336 | } 337 | 338 | /** 339 | * Json. 340 | * 341 | * Note: This method is not part of the PSR-7 standard. 342 | * 343 | * This method prepares the response object to return an HTTP Json 344 | * response to the client. 345 | * 346 | * @param mixed $data The data 347 | * @param int $status The HTTP status code. 348 | * @param int $encodingOptions Json encoding options 349 | * @throws \RuntimeException 350 | * @return static 351 | */ 352 | public function withJson($data, $status = null, $encodingOptions = 0) 353 | { 354 | $response = $this->withBody(new Body(fopen('php://temp', 'r+'))); 355 | $response->body->write($json = json_encode($data, $encodingOptions)); 356 | 357 | // Ensure that the json encoding passed successfully 358 | if ($json === false) { 359 | throw new \RuntimeException(json_last_error_msg(), json_last_error()); 360 | } 361 | 362 | $responseWithJson = $response->withHeader('Content-Type', 'application/json;charset=utf-8'); 363 | if (isset($status)) { 364 | return $responseWithJson->withStatus($status); 365 | } 366 | return $responseWithJson; 367 | } 368 | 369 | /** 370 | * Is this response empty? 371 | * 372 | * Note: This method is not part of the PSR-7 standard. 373 | * 374 | * @return bool 375 | */ 376 | public function isEmpty() 377 | { 378 | return in_array( 379 | $this->getStatusCode(), 380 | [StatusCode::HTTP_NO_CONTENT, StatusCode::HTTP_RESET_CONTENT, StatusCode::HTTP_NOT_MODIFIED] 381 | ); 382 | } 383 | 384 | /** 385 | * Is this response informational? 386 | * 387 | * Note: This method is not part of the PSR-7 standard. 388 | * 389 | * @return bool 390 | */ 391 | public function isInformational() 392 | { 393 | return $this->getStatusCode() >= StatusCode::HTTP_CONTINUE && $this->getStatusCode() < StatusCode::HTTP_OK; 394 | } 395 | 396 | /** 397 | * Is this response OK? 398 | * 399 | * Note: This method is not part of the PSR-7 standard. 400 | * 401 | * @return bool 402 | */ 403 | public function isOk() 404 | { 405 | return $this->getStatusCode() === StatusCode::HTTP_OK; 406 | } 407 | 408 | /** 409 | * Is this response successful? 410 | * 411 | * Note: This method is not part of the PSR-7 standard. 412 | * 413 | * @return bool 414 | */ 415 | public function isSuccessful() 416 | { 417 | return $this->getStatusCode() >= StatusCode::HTTP_OK && 418 | $this->getStatusCode() < StatusCode::HTTP_MULTIPLE_CHOICES; 419 | } 420 | 421 | /** 422 | * Is this response a redirect? 423 | * 424 | * Note: This method is not part of the PSR-7 standard. 425 | * 426 | * @return bool 427 | */ 428 | public function isRedirect() 429 | { 430 | return in_array( 431 | $this->getStatusCode(), 432 | [ 433 | StatusCode::HTTP_MOVED_PERMANENTLY, 434 | StatusCode::HTTP_FOUND, 435 | StatusCode::HTTP_SEE_OTHER, 436 | StatusCode::HTTP_TEMPORARY_REDIRECT, 437 | StatusCode::HTTP_PERMANENT_REDIRECT 438 | ] 439 | ); 440 | } 441 | 442 | /** 443 | * Is this response a redirection? 444 | * 445 | * Note: This method is not part of the PSR-7 standard. 446 | * 447 | * @return bool 448 | */ 449 | public function isRedirection() 450 | { 451 | return $this->getStatusCode() >= StatusCode::HTTP_MULTIPLE_CHOICES && 452 | $this->getStatusCode() < StatusCode::HTTP_BAD_REQUEST; 453 | } 454 | 455 | /** 456 | * Is this response forbidden? 457 | * 458 | * Note: This method is not part of the PSR-7 standard. 459 | * 460 | * @return bool 461 | * @api 462 | */ 463 | public function isForbidden() 464 | { 465 | return $this->getStatusCode() === StatusCode::HTTP_FORBIDDEN; 466 | } 467 | 468 | /** 469 | * Is this response not Found? 470 | * 471 | * Note: This method is not part of the PSR-7 standard. 472 | * 473 | * @return bool 474 | */ 475 | public function isNotFound() 476 | { 477 | return $this->getStatusCode() === StatusCode::HTTP_NOT_FOUND; 478 | } 479 | 480 | /** 481 | * Is this response a client error? 482 | * 483 | * Note: This method is not part of the PSR-7 standard. 484 | * 485 | * @return bool 486 | */ 487 | public function isClientError() 488 | { 489 | return $this->getStatusCode() >= StatusCode::HTTP_BAD_REQUEST && 490 | $this->getStatusCode() < StatusCode::HTTP_INTERNAL_SERVER_ERROR; 491 | } 492 | 493 | /** 494 | * Is this response a server error? 495 | * 496 | * Note: This method is not part of the PSR-7 standard. 497 | * 498 | * @return bool 499 | */ 500 | public function isServerError() 501 | { 502 | return $this->getStatusCode() >= StatusCode::HTTP_INTERNAL_SERVER_ERROR && $this->getStatusCode() < 600; 503 | } 504 | 505 | /** 506 | * Convert response to string. 507 | * 508 | * Note: This method is not part of the PSR-7 standard. 509 | * 510 | * @return string 511 | */ 512 | public function __toString() 513 | { 514 | $output = sprintf( 515 | 'HTTP/%s %s %s', 516 | $this->getProtocolVersion(), 517 | $this->getStatusCode(), 518 | $this->getReasonPhrase() 519 | ); 520 | $output .= Response::EOL; 521 | foreach ($this->getHeaders() as $name => $values) { 522 | $output .= sprintf('%s: %s', $name, $this->getHeaderLine($name)) . Response::EOL; 523 | } 524 | $output .= Response::EOL; 525 | $output .= (string)$this->getBody(); 526 | 527 | return $output; 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /Slim/App.php: -------------------------------------------------------------------------------- 1 | container = $container; 83 | } 84 | 85 | /** 86 | * Enable access to the DI container by consumers of $app 87 | * 88 | * @return ContainerInterface 89 | */ 90 | public function getContainer() 91 | { 92 | return $this->container; 93 | } 94 | 95 | /** 96 | * Add middleware 97 | * 98 | * This method prepends new middleware to the app's middleware stack. 99 | * 100 | * @param callable|string $callable The callback routine 101 | * 102 | * @return static 103 | */ 104 | public function add($callable) 105 | { 106 | return $this->addMiddleware(new DeferredCallable($callable, $this->container)); 107 | } 108 | 109 | /** 110 | * Calling a non-existant method on App checks to see if there's an item 111 | * in the container that is callable and if so, calls it. 112 | * 113 | * @param string $method 114 | * @param array $args 115 | * @return mixed 116 | */ 117 | public function __call($method, $args) 118 | { 119 | if ($this->container->has($method)) { 120 | $obj = $this->container->get($method); 121 | if (is_callable($obj)) { 122 | return call_user_func_array($obj, $args); 123 | } 124 | } 125 | 126 | throw new \BadMethodCallException("Method $method is not a valid method"); 127 | } 128 | 129 | /******************************************************************************** 130 | * Router proxy methods 131 | *******************************************************************************/ 132 | 133 | /** 134 | * Add GET route 135 | * 136 | * @param string $pattern The route URI pattern 137 | * @param callable|string $callable The route callback routine 138 | * 139 | * @return \Slim\Interfaces\RouteInterface 140 | */ 141 | public function get($pattern, $callable) 142 | { 143 | return $this->map(['GET'], $pattern, $callable); 144 | } 145 | 146 | /** 147 | * Add POST route 148 | * 149 | * @param string $pattern The route URI pattern 150 | * @param callable|string $callable The route callback routine 151 | * 152 | * @return \Slim\Interfaces\RouteInterface 153 | */ 154 | public function post($pattern, $callable) 155 | { 156 | return $this->map(['POST'], $pattern, $callable); 157 | } 158 | 159 | /** 160 | * Add PUT route 161 | * 162 | * @param string $pattern The route URI pattern 163 | * @param callable|string $callable The route callback routine 164 | * 165 | * @return \Slim\Interfaces\RouteInterface 166 | */ 167 | public function put($pattern, $callable) 168 | { 169 | return $this->map(['PUT'], $pattern, $callable); 170 | } 171 | 172 | /** 173 | * Add PATCH route 174 | * 175 | * @param string $pattern The route URI pattern 176 | * @param callable|string $callable The route callback routine 177 | * 178 | * @return \Slim\Interfaces\RouteInterface 179 | */ 180 | public function patch($pattern, $callable) 181 | { 182 | return $this->map(['PATCH'], $pattern, $callable); 183 | } 184 | 185 | /** 186 | * Add DELETE route 187 | * 188 | * @param string $pattern The route URI pattern 189 | * @param callable|string $callable The route callback routine 190 | * 191 | * @return \Slim\Interfaces\RouteInterface 192 | */ 193 | public function delete($pattern, $callable) 194 | { 195 | return $this->map(['DELETE'], $pattern, $callable); 196 | } 197 | 198 | /** 199 | * Add OPTIONS route 200 | * 201 | * @param string $pattern The route URI pattern 202 | * @param callable|string $callable The route callback routine 203 | * 204 | * @return \Slim\Interfaces\RouteInterface 205 | */ 206 | public function options($pattern, $callable) 207 | { 208 | return $this->map(['OPTIONS'], $pattern, $callable); 209 | } 210 | 211 | /** 212 | * Add route for any HTTP method 213 | * 214 | * @param string $pattern The route URI pattern 215 | * @param callable|string $callable The route callback routine 216 | * 217 | * @return \Slim\Interfaces\RouteInterface 218 | */ 219 | public function any($pattern, $callable) 220 | { 221 | return $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, $callable); 222 | } 223 | 224 | /** 225 | * Add route with multiple methods 226 | * 227 | * @param string[] $methods Numeric array of HTTP method names 228 | * @param string $pattern The route URI pattern 229 | * @param callable|string $callable The route callback routine 230 | * 231 | * @return RouteInterface 232 | */ 233 | public function map(array $methods, $pattern, $callable) 234 | { 235 | if ($callable instanceof Closure) { 236 | $callable = $callable->bindTo($this->container); 237 | } 238 | 239 | $route = $this->container->get('router')->map($methods, $pattern, $callable); 240 | if (is_callable([$route, 'setContainer'])) { 241 | $route->setContainer($this->container); 242 | } 243 | 244 | if (is_callable([$route, 'setOutputBuffering'])) { 245 | $route->setOutputBuffering($this->container->get('settings')['outputBuffering']); 246 | } 247 | 248 | return $route; 249 | } 250 | 251 | /** 252 | * Add a route that sends an HTTP redirect 253 | * 254 | * @param string $from 255 | * @param string|UriInterface $to 256 | * @param int $status 257 | * 258 | * @return RouteInterface 259 | */ 260 | public function redirect($from, $to, $status = 302) 261 | { 262 | $handler = function ($request, ResponseInterface $response) use ($to, $status) { 263 | return $response->withHeader('Location', (string)$to)->withStatus($status); 264 | }; 265 | 266 | return $this->get($from, $handler); 267 | } 268 | 269 | /** 270 | * Route Groups 271 | * 272 | * This method accepts a route pattern and a callback. All route 273 | * declarations in the callback will be prepended by the group(s) 274 | * that it is in. 275 | * 276 | * @param string $pattern 277 | * @param callable $callable 278 | * 279 | * @return RouteGroupInterface 280 | */ 281 | public function group($pattern, $callable) 282 | { 283 | /** @var RouteGroup $group */ 284 | $group = $this->container->get('router')->pushGroup($pattern, $callable); 285 | $group->setContainer($this->container); 286 | $group($this); 287 | $this->container->get('router')->popGroup(); 288 | return $group; 289 | } 290 | 291 | /******************************************************************************** 292 | * Runner 293 | *******************************************************************************/ 294 | 295 | /** 296 | * Run application 297 | * 298 | * This method traverses the application middleware stack and then sends the 299 | * resultant Response object to the HTTP client. 300 | * 301 | * @param bool|false $silent 302 | * @return ResponseInterface 303 | * 304 | * @throws Exception 305 | * @throws MethodNotAllowedException 306 | * @throws NotFoundException 307 | */ 308 | public function run($silent = false) 309 | { 310 | $response = $this->container->get('response'); 311 | 312 | try { 313 | ob_start(); 314 | $response = $this->process($this->container->get('request'), $response); 315 | } catch (InvalidMethodException $e) { 316 | $response = $this->processInvalidMethod($e->getRequest(), $response); 317 | } finally { 318 | $output = ob_get_clean(); 319 | } 320 | 321 | if (!empty($output) && $response->getBody()->isWritable()) { 322 | $outputBuffering = $this->container->get('settings')['outputBuffering']; 323 | if ($outputBuffering === 'prepend') { 324 | // prepend output buffer content 325 | $body = new Http\Body(fopen('php://temp', 'r+')); 326 | $body->write($output . $response->getBody()); 327 | $response = $response->withBody($body); 328 | } elseif ($outputBuffering === 'append') { 329 | // append output buffer content 330 | $response->getBody()->write($output); 331 | } 332 | } 333 | 334 | $response = $this->finalize($response); 335 | 336 | if (!$silent) { 337 | $this->respond($response); 338 | } 339 | 340 | return $response; 341 | } 342 | 343 | /** 344 | * Pull route info for a request with a bad method to decide whether to 345 | * return a not-found error (default) or a bad-method error, then run 346 | * the handler for that error, returning the resulting response. 347 | * 348 | * Used for cases where an incoming request has an unrecognized method, 349 | * rather than throwing an exception and not catching it all the way up. 350 | * 351 | * @param ServerRequestInterface $request 352 | * @param ResponseInterface $response 353 | * @return ResponseInterface 354 | */ 355 | protected function processInvalidMethod(ServerRequestInterface $request, ResponseInterface $response) 356 | { 357 | $router = $this->container->get('router'); 358 | if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) { 359 | $router->setBasePath($request->getUri()->getBasePath()); 360 | } 361 | 362 | $request = $this->dispatchRouterAndPrepareRoute($request, $router); 363 | $routeInfo = $request->getAttribute('routeInfo', [RouterInterface::DISPATCH_STATUS => Dispatcher::NOT_FOUND]); 364 | 365 | if ($routeInfo[RouterInterface::DISPATCH_STATUS] === Dispatcher::METHOD_NOT_ALLOWED) { 366 | return $this->handleException( 367 | new MethodNotAllowedException($request, $response, $routeInfo[RouterInterface::ALLOWED_METHODS]), 368 | $request, 369 | $response 370 | ); 371 | } 372 | 373 | return $this->handleException(new NotFoundException($request, $response), $request, $response); 374 | } 375 | 376 | /** 377 | * Process a request 378 | * 379 | * This method traverses the application middleware stack and then returns the 380 | * resultant Response object. 381 | * 382 | * @param ServerRequestInterface $request 383 | * @param ResponseInterface $response 384 | * @return ResponseInterface 385 | * 386 | * @throws Exception 387 | * @throws MethodNotAllowedException 388 | * @throws NotFoundException 389 | */ 390 | public function process(ServerRequestInterface $request, ResponseInterface $response) 391 | { 392 | // Ensure basePath is set 393 | $router = $this->container->get('router'); 394 | if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) { 395 | $router->setBasePath($request->getUri()->getBasePath()); 396 | } 397 | 398 | // Dispatch the Router first if the setting for this is on 399 | if ($this->container->get('settings')['determineRouteBeforeAppMiddleware'] === true) { 400 | // Dispatch router (note: you won't be able to alter routes after this) 401 | $request = $this->dispatchRouterAndPrepareRoute($request, $router); 402 | } 403 | 404 | // Traverse middleware stack 405 | try { 406 | $response = $this->callMiddlewareStack($request, $response); 407 | } catch (Exception $e) { 408 | $response = $this->handleException($e, $request, $response); 409 | } catch (Throwable $e) { 410 | $response = $this->handlePhpError($e, $request, $response); 411 | } 412 | 413 | return $response; 414 | } 415 | 416 | /** 417 | * Send the response to the client 418 | * 419 | * @param ResponseInterface $response 420 | */ 421 | public function respond(ResponseInterface $response) 422 | { 423 | // Send response 424 | if (!headers_sent()) { 425 | // Headers 426 | foreach ($response->getHeaders() as $name => $values) { 427 | $first = stripos($name, 'Set-Cookie') === 0 ? false : true; 428 | foreach ($values as $value) { 429 | header(sprintf('%s: %s', $name, $value), $first); 430 | $first = false; 431 | } 432 | } 433 | 434 | // Set the status _after_ the headers, because of PHP's "helpful" behavior with location headers. 435 | // See https://github.com/slimphp/Slim/issues/1730 436 | 437 | // Status 438 | header(sprintf( 439 | 'HTTP/%s %s %s', 440 | $response->getProtocolVersion(), 441 | $response->getStatusCode(), 442 | $response->getReasonPhrase() 443 | ), true, $response->getStatusCode()); 444 | } 445 | 446 | // Body 447 | if (!$this->isEmptyResponse($response)) { 448 | $body = $response->getBody(); 449 | if ($body->isSeekable()) { 450 | $body->rewind(); 451 | } 452 | $settings = $this->container->get('settings'); 453 | $chunkSize = $settings['responseChunkSize']; 454 | 455 | $contentLength = $response->getHeaderLine('Content-Length'); 456 | if (!$contentLength) { 457 | $contentLength = $body->getSize(); 458 | } 459 | 460 | 461 | if (isset($contentLength)) { 462 | $amountToRead = $contentLength; 463 | while ($amountToRead > 0 && !$body->eof()) { 464 | $data = $body->read(min($chunkSize, $amountToRead)); 465 | echo $data; 466 | 467 | $amountToRead -= strlen($data); 468 | 469 | if (connection_status() != CONNECTION_NORMAL) { 470 | break; 471 | } 472 | } 473 | } else { 474 | while (!$body->eof()) { 475 | echo $body->read($chunkSize); 476 | if (connection_status() != CONNECTION_NORMAL) { 477 | break; 478 | } 479 | } 480 | } 481 | } 482 | } 483 | 484 | /** 485 | * Invoke application 486 | * 487 | * This method implements the middleware interface. It receives 488 | * Request and Response objects, and it returns a Response object 489 | * after compiling the routes registered in the Router and dispatching 490 | * the Request object to the appropriate Route callback routine. 491 | * 492 | * @param ServerRequestInterface $request The most recent Request object 493 | * @param ResponseInterface $response The most recent Response object 494 | * 495 | * @return ResponseInterface 496 | * @throws MethodNotAllowedException 497 | * @throws NotFoundException 498 | */ 499 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response) 500 | { 501 | // Get the route info 502 | $routeInfo = $request->getAttribute('routeInfo'); 503 | 504 | /** @var \Slim\Interfaces\RouterInterface $router */ 505 | $router = $this->container->get('router'); 506 | 507 | // If router hasn't been dispatched or the URI changed then dispatch 508 | if (null === $routeInfo || ($routeInfo['request'] !== [$request->getMethod(), (string) $request->getUri()])) { 509 | $request = $this->dispatchRouterAndPrepareRoute($request, $router); 510 | $routeInfo = $request->getAttribute('routeInfo'); 511 | } 512 | 513 | if ($routeInfo[0] === Dispatcher::FOUND) { 514 | $route = $router->lookupRoute($routeInfo[1]); 515 | return $route->run($request, $response); 516 | } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { 517 | if (!$this->container->has('notAllowedHandler')) { 518 | throw new MethodNotAllowedException($request, $response, $routeInfo[1]); 519 | } 520 | /** @var callable $notAllowedHandler */ 521 | $notAllowedHandler = $this->container->get('notAllowedHandler'); 522 | return $notAllowedHandler($request, $response, $routeInfo[1]); 523 | } 524 | 525 | if (!$this->container->has('notFoundHandler')) { 526 | throw new NotFoundException($request, $response); 527 | } 528 | /** @var callable $notFoundHandler */ 529 | $notFoundHandler = $this->container->get('notFoundHandler'); 530 | return $notFoundHandler($request, $response); 531 | } 532 | 533 | /** 534 | * Perform a sub-request from within an application route 535 | * 536 | * This method allows you to prepare and initiate a sub-request, run within 537 | * the context of the current request. This WILL NOT issue a remote HTTP 538 | * request. Instead, it will route the provided URL, method, headers, 539 | * cookies, body, and server variables against the set of registered 540 | * application routes. The result response object is returned. 541 | * 542 | * @param string $method The request method (e.g., GET, POST, PUT, etc.) 543 | * @param string $path The request URI path 544 | * @param string $query The request URI query string 545 | * @param array $headers The request headers (key-value array) 546 | * @param array $cookies The request cookies (key-value array) 547 | * @param string $bodyContent The request body 548 | * @param ResponseInterface $response The response object (optional) 549 | * @return ResponseInterface 550 | */ 551 | public function subRequest( 552 | $method, 553 | $path, 554 | $query = '', 555 | array $headers = [], 556 | array $cookies = [], 557 | $bodyContent = '', 558 | ResponseInterface $response = null 559 | ) { 560 | $env = $this->container->get('environment'); 561 | $uri = Uri::createFromEnvironment($env)->withPath($path)->withQuery($query); 562 | $headers = new Headers($headers); 563 | $serverParams = $env->all(); 564 | $body = new Body(fopen('php://temp', 'r+')); 565 | $body->write($bodyContent); 566 | $body->rewind(); 567 | $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); 568 | 569 | if (!$response) { 570 | $response = $this->container->get('response'); 571 | } 572 | 573 | return $this($request, $response); 574 | } 575 | 576 | /** 577 | * Dispatch the router to find the route. Prepare the route for use. 578 | * 579 | * @param ServerRequestInterface $request 580 | * @param RouterInterface $router 581 | * @return ServerRequestInterface 582 | */ 583 | protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request, RouterInterface $router) 584 | { 585 | $routeInfo = $router->dispatch($request); 586 | 587 | if ($routeInfo[0] === Dispatcher::FOUND) { 588 | $routeArguments = []; 589 | foreach ($routeInfo[2] as $k => $v) { 590 | $routeArguments[$k] = urldecode($v); 591 | } 592 | 593 | $route = $router->lookupRoute($routeInfo[1]); 594 | $route->prepare($request, $routeArguments); 595 | 596 | // add route to the request's attributes in case a middleware or handler needs access to the route 597 | $request = $request->withAttribute('route', $route); 598 | } 599 | 600 | $routeInfo['request'] = [$request->getMethod(), (string) $request->getUri()]; 601 | 602 | return $request->withAttribute('routeInfo', $routeInfo); 603 | } 604 | 605 | /** 606 | * Finalize response 607 | * 608 | * @param ResponseInterface $response 609 | * @return ResponseInterface 610 | */ 611 | protected function finalize(ResponseInterface $response) 612 | { 613 | // stop PHP sending a Content-Type automatically 614 | ini_set('default_mimetype', ''); 615 | 616 | if ($this->isEmptyResponse($response)) { 617 | return $response->withoutHeader('Content-Type')->withoutHeader('Content-Length'); 618 | } 619 | 620 | // Add Content-Length header if `addContentLengthHeader` setting is set 621 | if (isset($this->container->get('settings')['addContentLengthHeader']) && 622 | $this->container->get('settings')['addContentLengthHeader'] == true) { 623 | if (ob_get_length() > 0) { 624 | throw new \RuntimeException("Unexpected data in output buffer. " . 625 | "Maybe you have characters before an opening getBody()->getSize(); 628 | if ($size !== null && !$response->hasHeader('Content-Length')) { 629 | $response = $response->withHeader('Content-Length', (string) $size); 630 | } 631 | } 632 | 633 | return $response; 634 | } 635 | 636 | /** 637 | * Helper method, which returns true if the provided response must not output a body and false 638 | * if the response could have a body. 639 | * 640 | * @see https://tools.ietf.org/html/rfc7231 641 | * 642 | * @param ResponseInterface $response 643 | * @return bool 644 | */ 645 | protected function isEmptyResponse(ResponseInterface $response) 646 | { 647 | if (method_exists($response, 'isEmpty')) { 648 | return $response->isEmpty(); 649 | } 650 | 651 | return in_array($response->getStatusCode(), [204, 205, 304]); 652 | } 653 | 654 | /** 655 | * Call relevant handler from the Container if needed. If it doesn't exist, 656 | * then just re-throw. 657 | * 658 | * @param Exception $e 659 | * @param ServerRequestInterface $request 660 | * @param ResponseInterface $response 661 | * 662 | * @return ResponseInterface 663 | * @throws Exception if a handler is needed and not found 664 | */ 665 | protected function handleException(Exception $e, ServerRequestInterface $request, ResponseInterface $response) 666 | { 667 | if ($e instanceof MethodNotAllowedException) { 668 | $handler = 'notAllowedHandler'; 669 | $params = [$e->getRequest(), $e->getResponse(), $e->getAllowedMethods()]; 670 | } elseif ($e instanceof NotFoundException) { 671 | $handler = 'notFoundHandler'; 672 | $params = [$e->getRequest(), $e->getResponse(), $e]; 673 | } elseif ($e instanceof SlimException) { 674 | // This is a Stop exception and contains the response 675 | return $e->getResponse(); 676 | } else { 677 | // Other exception, use $request and $response params 678 | $handler = 'errorHandler'; 679 | $params = [$request, $response, $e]; 680 | } 681 | 682 | if ($this->container->has($handler)) { 683 | $callable = $this->container->get($handler); 684 | // Call the registered handler 685 | return call_user_func_array($callable, $params); 686 | } 687 | 688 | // No handlers found, so just throw the exception 689 | throw $e; 690 | } 691 | 692 | /** 693 | * Call relevant handler from the Container if needed. If it doesn't exist, 694 | * then just re-throw. 695 | * 696 | * @param Throwable $e 697 | * @param ServerRequestInterface $request 698 | * @param ResponseInterface $response 699 | * @return ResponseInterface 700 | * @throws Throwable 701 | */ 702 | protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response) 703 | { 704 | $handler = 'phpErrorHandler'; 705 | $params = [$request, $response, $e]; 706 | 707 | if ($this->container->has($handler)) { 708 | $callable = $this->container->get($handler); 709 | // Call the registered handler 710 | return call_user_func_array($callable, $params); 711 | } 712 | 713 | // No handlers found, so just throw the exception 714 | throw $e; 715 | } 716 | } 717 | --------------------------------------------------------------------------------