├── Slim ├── Exception │ ├── NotFoundException.php │ ├── InvalidMethodException.php │ ├── ContainerException.php │ ├── ContainerValueNotFoundException.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 │ ├── 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 ├── LICENSE.md └── composer.json /Slim/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | request = $request; 14 | parent::__construct(sprintf('Unsupported HTTP method "%s" provided', $method)); 15 | } 16 | 17 | public function getRequest() 18 | { 19 | return $this->request; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Slim/Exception/ContainerException.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 to bind the callable to. 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(); 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 | "scripts": { 51 | "test": [ 52 | "@phpunit", 53 | "@phpcs" 54 | ], 55 | "phpunit": "php vendor/bin/phpunit", 56 | "phpcs": "php vendor/bin/phpcs" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Slim/Http/Environment.php: -------------------------------------------------------------------------------- 1 | 'HTTP/1.1', 34 | 'REQUEST_METHOD' => 'GET', 35 | 'SCRIPT_NAME' => '', 36 | 'REQUEST_URI' => '', 37 | 'QUERY_STRING' => '', 38 | 'SERVER_NAME' => 'localhost', 39 | 'SERVER_PORT' => 80, 40 | 'HTTP_HOST' => 'localhost', 41 | 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 42 | 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8', 43 | 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', 44 | 'HTTP_USER_AGENT' => 'Slim Framework', 45 | 'REMOTE_ADDR' => '127.0.0.1', 46 | 'REQUEST_TIME' => time(), 47 | 'REQUEST_TIME_FLOAT' => microtime(true), 48 | ], $userData); 49 | 50 | return new static($data); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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/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/Interfaces/RouterInterface.php: -------------------------------------------------------------------------------- 1 | container = $container; 34 | } 35 | 36 | /** 37 | * Resolve toResolve into a closure that 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 | determineContentType($request); 36 | switch ($contentType) { 37 | case 'application/json': 38 | $output = $this->renderJsonNotFoundOutput(); 39 | break; 40 | 41 | case 'text/xml': 42 | case 'application/xml': 43 | $output = $this->renderXmlNotFoundOutput(); 44 | break; 45 | 46 | case 'text/html': 47 | $output = $this->renderHtmlNotFoundOutput($request); 48 | break; 49 | 50 | default: 51 | throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); 52 | } 53 | 54 | $body = new Body(fopen('php://temp', 'r+')); 55 | $body->write($output); 56 | 57 | return $response->withStatus(404) 58 | ->withHeader('Content-Type', $contentType) 59 | ->withBody($body); 60 | } 61 | 62 | /** 63 | * Return a response for application/json content not found 64 | * 65 | * @return ResponseInterface 66 | */ 67 | protected function renderJsonNotFoundOutput() 68 | { 69 | return '{"message":"Not found"}'; 70 | } 71 | 72 | /** 73 | * Return a response for xml content not found 74 | * 75 | * @return ResponseInterface 76 | */ 77 | protected function renderXmlNotFoundOutput() 78 | { 79 | return 'Not found'; 80 | } 81 | 82 | /** 83 | * Return a response for text/html content not found 84 | * 85 | * @param ServerRequestInterface $request The most recent Request object 86 | * 87 | * @return ResponseInterface 88 | */ 89 | protected function renderHtmlNotFoundOutput(ServerRequestInterface $request) 90 | { 91 | $homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment('')); 92 | return << 94 | 95 | Page Not Found 96 | 113 | 114 | 115 |

Page Not Found

116 |

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

121 | Visit the Home Page 122 | 123 | 124 | END; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Slim/MiddlewareAwareTrait.php: -------------------------------------------------------------------------------- 1 | middlewareLock) { 59 | throw new RuntimeException('Middleware can’t be added once the stack is dequeuing'); 60 | } 61 | 62 | if (is_null($this->stack)) { 63 | $this->seedMiddlewareStack(); 64 | } 65 | $next = $this->stack->top(); 66 | $this->stack[] = function ( 67 | ServerRequestInterface $request, 68 | ResponseInterface $response 69 | ) use ( 70 | $callable, 71 | $next 72 | ) { 73 | $result = call_user_func($callable, $request, $response, $next); 74 | if ($result instanceof ResponseInterface === false) { 75 | throw new UnexpectedValueException( 76 | 'Middleware must return instance of \Psr\Http\Message\ResponseInterface' 77 | ); 78 | } 79 | 80 | return $result; 81 | }; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Seed middleware stack with first callable 88 | * 89 | * @param callable $kernel The last item to run as middleware 90 | * 91 | * @throws RuntimeException if the stack is seeded more than once 92 | */ 93 | protected function seedMiddlewareStack(callable $kernel = null) 94 | { 95 | if (!is_null($this->stack)) { 96 | throw new RuntimeException('MiddlewareStack can only be seeded once.'); 97 | } 98 | if ($kernel === null) { 99 | $kernel = $this; 100 | } 101 | $this->stack = new SplStack; 102 | $this->stack->setIteratorMode(SplDoublyLinkedList::IT_MODE_LIFO | SplDoublyLinkedList::IT_MODE_KEEP); 103 | $this->stack[] = $kernel; 104 | } 105 | 106 | /** 107 | * Call middleware stack 108 | * 109 | * @param ServerRequestInterface $request A request object 110 | * @param ResponseInterface $response A response object 111 | * 112 | * @return ResponseInterface 113 | */ 114 | public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response) 115 | { 116 | if (is_null($this->stack)) { 117 | $this->seedMiddlewareStack(); 118 | } 119 | /** @var callable $start */ 120 | $start = $this->stack->top(); 121 | $this->middlewareLock = true; 122 | $response = $start($request, $response); 123 | $this->middlewareLock = false; 124 | return $response; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Slim/Handlers/NotAllowed.php: -------------------------------------------------------------------------------- 1 | getMethod() === 'OPTIONS') { 37 | $status = 200; 38 | $contentType = 'text/plain'; 39 | $output = $this->renderPlainNotAllowedMessage($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 not allowed message 74 | * 75 | * @param array $methods 76 | * @return string 77 | */ 78 | protected function renderPlainNotAllowedMessage($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 | ]; 47 | 48 | /** 49 | * Create new cookies helper 50 | * 51 | * @param array $cookies 52 | */ 53 | public function __construct(array $cookies = []) 54 | { 55 | $this->requestCookies = $cookies; 56 | } 57 | 58 | /** 59 | * Set default cookie properties 60 | * 61 | * @param array $settings 62 | */ 63 | public function setDefaults(array $settings) 64 | { 65 | $this->defaults = array_replace($this->defaults, $settings); 66 | } 67 | 68 | /** 69 | * Get request cookie 70 | * 71 | * @param string $name Cookie name 72 | * @param mixed $default Cookie default value 73 | * 74 | * @return mixed Cookie value if present, else default 75 | */ 76 | public function get($name, $default = null) 77 | { 78 | return isset($this->requestCookies[$name]) ? $this->requestCookies[$name] : $default; 79 | } 80 | 81 | /** 82 | * Set response cookie 83 | * 84 | * @param string $name Cookie name 85 | * @param string|array $value Cookie value, or cookie properties 86 | */ 87 | public function set($name, $value) 88 | { 89 | if (!is_array($value)) { 90 | $value = ['value' => (string)$value]; 91 | } 92 | $this->responseCookies[$name] = array_replace($this->defaults, $value); 93 | } 94 | 95 | /** 96 | * Convert to `Set-Cookie` headers 97 | * 98 | * @return string[] 99 | */ 100 | public function toHeaders() 101 | { 102 | $headers = []; 103 | foreach ($this->responseCookies as $name => $properties) { 104 | $headers[] = $this->toHeader($name, $properties); 105 | } 106 | 107 | return $headers; 108 | } 109 | 110 | /** 111 | * Convert to `Set-Cookie` header 112 | * 113 | * @param string $name Cookie name 114 | * @param array $properties Cookie properties 115 | * 116 | * @return string 117 | */ 118 | protected function toHeader($name, array $properties) 119 | { 120 | $result = urlencode($name) . '=' . urlencode($properties['value']); 121 | 122 | if (isset($properties['domain'])) { 123 | $result .= '; domain=' . $properties['domain']; 124 | } 125 | 126 | if (isset($properties['path'])) { 127 | $result .= '; path=' . $properties['path']; 128 | } 129 | 130 | if (isset($properties['expires'])) { 131 | if (is_string($properties['expires'])) { 132 | $timestamp = strtotime($properties['expires']); 133 | } else { 134 | $timestamp = (int)$properties['expires']; 135 | } 136 | if ($timestamp !== 0) { 137 | $result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp); 138 | } 139 | } 140 | 141 | if (isset($properties['secure']) && $properties['secure']) { 142 | $result .= '; secure'; 143 | } 144 | 145 | if (isset($properties['hostonly']) && $properties['hostonly']) { 146 | $result .= '; HostOnly'; 147 | } 148 | 149 | if (isset($properties['httponly']) && $properties['httponly']) { 150 | $result .= '; HttpOnly'; 151 | } 152 | 153 | return $result; 154 | } 155 | 156 | /** 157 | * Parse HTTP request `Cookie:` header and extract 158 | * into a PHP associative array. 159 | * 160 | * @param string $header The raw HTTP request `Cookie:` header 161 | * 162 | * @return array Associative array of cookie names and values 163 | * 164 | * @throws InvalidArgumentException if the cookie data cannot be parsed 165 | */ 166 | public static function parseHeader($header) 167 | { 168 | if (is_array($header) === true) { 169 | $header = isset($header[0]) ? $header[0] : ''; 170 | } 171 | 172 | if (is_string($header) === false) { 173 | throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.'); 174 | } 175 | 176 | $header = rtrim($header, "\r\n"); 177 | $pieces = preg_split('@[;]\s*@', $header); 178 | $cookies = []; 179 | 180 | foreach ($pieces as $cookie) { 181 | $cookie = explode('=', $cookie, 2); 182 | 183 | if (count($cookie) === 2) { 184 | $key = urldecode($cookie[0]); 185 | $value = urldecode($cookie[1]); 186 | 187 | if (!isset($cookies[$key])) { 188 | $cookies[$key] = $value; 189 | } 190 | } 191 | } 192 | 193 | return $cookies; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /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 Interop\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 (null === $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($container->get('settings')['displayErrorDetails']); 156 | }; 157 | } 158 | 159 | if (!isset($container['notFoundHandler'])) { 160 | /** 161 | * This service MUST return a callable 162 | * that accepts two arguments: 163 | * 164 | * 1. Instance of \Psr\Http\Message\ServerRequestInterface 165 | * 2. Instance of \Psr\Http\Message\ResponseInterface 166 | * 167 | * The callable MUST return an instance of 168 | * \Psr\Http\Message\ResponseInterface. 169 | * 170 | * @return callable 171 | */ 172 | $container['notFoundHandler'] = function () { 173 | return new NotFound; 174 | }; 175 | } 176 | 177 | if (!isset($container['notAllowedHandler'])) { 178 | /** 179 | * This service MUST return a callable 180 | * that accepts three arguments: 181 | * 182 | * 1. Instance of \Psr\Http\Message\ServerRequestInterface 183 | * 2. Instance of \Psr\Http\Message\ResponseInterface 184 | * 3. Array of allowed HTTP methods 185 | * 186 | * The callable MUST return an instance of 187 | * \Psr\Http\Message\ResponseInterface. 188 | * 189 | * @return callable 190 | */ 191 | $container['notAllowedHandler'] = function () { 192 | return new NotAllowed; 193 | }; 194 | } 195 | 196 | if (!isset($container['callableResolver'])) { 197 | /** 198 | * Instance of \Slim\Interfaces\CallableResolverInterface 199 | * 200 | * @param Container $container 201 | * 202 | * @return CallableResolverInterface 203 | */ 204 | $container['callableResolver'] = function ($container) { 205 | return new CallableResolver($container); 206 | }; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /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/Http/Message.php: -------------------------------------------------------------------------------- 1 | true, 41 | '1.1' => true, 42 | '2.0' => true, 43 | ]; 44 | 45 | /** 46 | * Headers 47 | * 48 | * @var \Slim\Interfaces\Http\HeadersInterface 49 | */ 50 | protected $headers; 51 | 52 | /** 53 | * Body object 54 | * 55 | * @var \Psr\Http\Message\StreamInterface 56 | */ 57 | protected $body; 58 | 59 | 60 | /** 61 | * Disable magic setter to ensure immutability 62 | */ 63 | public function __set($name, $value) 64 | { 65 | // Do nothing 66 | } 67 | 68 | /******************************************************************************* 69 | * Protocol 70 | ******************************************************************************/ 71 | 72 | /** 73 | * Retrieves the HTTP protocol version as a string. 74 | * 75 | * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). 76 | * 77 | * @return string HTTP protocol version. 78 | */ 79 | public function getProtocolVersion() 80 | { 81 | return $this->protocolVersion; 82 | } 83 | 84 | /** 85 | * Return an instance with the specified HTTP protocol version. 86 | * 87 | * The version string MUST contain only the HTTP version number (e.g., 88 | * "1.1", "1.0"). 89 | * 90 | * This method MUST be implemented in such a way as to retain the 91 | * immutability of the message, and MUST return an instance that has the 92 | * new protocol version. 93 | * 94 | * @param string $version HTTP protocol version 95 | * @return static 96 | * @throws InvalidArgumentException if the http version is an invalid number 97 | */ 98 | public function withProtocolVersion($version) 99 | { 100 | if (!isset(self::$validProtocolVersions[$version])) { 101 | throw new InvalidArgumentException( 102 | 'Invalid HTTP version. Must be one of: ' 103 | . implode(', ', array_keys(self::$validProtocolVersions)) 104 | ); 105 | } 106 | $clone = clone $this; 107 | $clone->protocolVersion = $version; 108 | 109 | return $clone; 110 | } 111 | 112 | /******************************************************************************* 113 | * Headers 114 | ******************************************************************************/ 115 | 116 | /** 117 | * Retrieves all message header values. 118 | * 119 | * The keys represent the header name as it will be sent over the wire, and 120 | * each value is an array of strings associated with the header. 121 | * 122 | * // Represent the headers as a string 123 | * foreach ($message->getHeaders() as $name => $values) { 124 | * echo $name . ": " . implode(", ", $values); 125 | * } 126 | * 127 | * // Emit headers iteratively: 128 | * foreach ($message->getHeaders() as $name => $values) { 129 | * foreach ($values as $value) { 130 | * header(sprintf('%s: %s', $name, $value), false); 131 | * } 132 | * } 133 | * 134 | * While header names are not case-sensitive, getHeaders() will preserve the 135 | * exact case in which headers were originally specified. 136 | * 137 | * @return array Returns an associative array of the message's headers. Each 138 | * key MUST be a header name, and each value MUST be an array of strings 139 | * for that header. 140 | */ 141 | public function getHeaders() 142 | { 143 | return $this->headers->all(); 144 | } 145 | 146 | /** 147 | * Checks if a header exists by the given case-insensitive name. 148 | * 149 | * @param string $name Case-insensitive header field name. 150 | * @return bool Returns true if any header names match the given header 151 | * name using a case-insensitive string comparison. Returns false if 152 | * no matching header name is found in the message. 153 | */ 154 | public function hasHeader($name) 155 | { 156 | return $this->headers->has($name); 157 | } 158 | 159 | /** 160 | * Retrieves a message header value by the given case-insensitive name. 161 | * 162 | * This method returns an array of all the header values of the given 163 | * case-insensitive header name. 164 | * 165 | * If the header does not appear in the message, this method MUST return an 166 | * empty array. 167 | * 168 | * @param string $name Case-insensitive header field name. 169 | * @return string[] An array of string values as provided for the given 170 | * header. If the header does not appear in the message, this method MUST 171 | * return an empty array. 172 | */ 173 | public function getHeader($name) 174 | { 175 | return $this->headers->get($name, []); 176 | } 177 | 178 | /** 179 | * Retrieves a comma-separated string of the values for a single header. 180 | * 181 | * This method returns all of the header values of the given 182 | * case-insensitive header name as a string concatenated together using 183 | * a comma. 184 | * 185 | * NOTE: Not all header values may be appropriately represented using 186 | * comma concatenation. For such headers, use getHeader() instead 187 | * and supply your own delimiter when concatenating. 188 | * 189 | * If the header does not appear in the message, this method MUST return 190 | * an empty string. 191 | * 192 | * @param string $name Case-insensitive header field name. 193 | * @return string A string of values as provided for the given header 194 | * concatenated together using a comma. If the header does not appear in 195 | * the message, this method MUST return an empty string. 196 | */ 197 | public function getHeaderLine($name) 198 | { 199 | return implode(',', $this->headers->get($name, [])); 200 | } 201 | 202 | /** 203 | * Return an instance with the provided value replacing the specified header. 204 | * 205 | * While header names are case-insensitive, the casing of the header will 206 | * be preserved by this function, and returned from getHeaders(). 207 | * 208 | * This method MUST be implemented in such a way as to retain the 209 | * immutability of the message, and MUST return an instance that has the 210 | * new and/or updated header and value. 211 | * 212 | * @param string $name Case-insensitive header field name. 213 | * @param string|string[] $value Header value(s). 214 | * @return static 215 | * @throws \InvalidArgumentException for invalid header names or values. 216 | */ 217 | public function withHeader($name, $value) 218 | { 219 | $clone = clone $this; 220 | $clone->headers->set($name, $value); 221 | 222 | return $clone; 223 | } 224 | 225 | /** 226 | * Return an instance with the specified header appended with the given value. 227 | * 228 | * Existing values for the specified header will be maintained. The new 229 | * value(s) will be appended to the existing list. If the header did not 230 | * exist previously, it will be added. 231 | * 232 | * This method MUST be implemented in such a way as to retain the 233 | * immutability of the message, and MUST return an instance that has the 234 | * new header and/or value. 235 | * 236 | * @param string $name Case-insensitive header field name to add. 237 | * @param string|string[] $value Header value(s). 238 | * @return static 239 | * @throws \InvalidArgumentException for invalid header names or values. 240 | */ 241 | public function withAddedHeader($name, $value) 242 | { 243 | $clone = clone $this; 244 | $clone->headers->add($name, $value); 245 | 246 | return $clone; 247 | } 248 | 249 | /** 250 | * Return an instance without the specified header. 251 | * 252 | * Header resolution MUST be done without case-sensitivity. 253 | * 254 | * This method MUST be implemented in such a way as to retain the 255 | * immutability of the message, and MUST return an instance that removes 256 | * the named header. 257 | * 258 | * @param string $name Case-insensitive header field name to remove. 259 | * @return static 260 | */ 261 | public function withoutHeader($name) 262 | { 263 | $clone = clone $this; 264 | $clone->headers->remove($name); 265 | 266 | return $clone; 267 | } 268 | 269 | /******************************************************************************* 270 | * Body 271 | ******************************************************************************/ 272 | 273 | /** 274 | * Gets the body of the message. 275 | * 276 | * @return StreamInterface Returns the body as a stream. 277 | */ 278 | public function getBody() 279 | { 280 | return $this->body; 281 | } 282 | 283 | /** 284 | * Return an instance with the specified message body. 285 | * 286 | * The body MUST be a StreamInterface object. 287 | * 288 | * This method MUST be implemented in such a way as to retain the 289 | * immutability of the message, and MUST return a new instance that has the 290 | * new body stream. 291 | * 292 | * @param StreamInterface $body Body. 293 | * @return static 294 | * @throws \InvalidArgumentException When the body is not valid. 295 | */ 296 | public function withBody(StreamInterface $body) 297 | { 298 | // TODO: Test for invalid body? 299 | $clone = clone $this; 300 | $clone->body = $body; 301 | 302 | return $clone; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /Slim/Route.php: -------------------------------------------------------------------------------- 1 | methods = is_string($methods) ? [$methods] : $methods; 93 | $this->pattern = $pattern; 94 | $this->callable = $callable; 95 | $this->groups = $groups; 96 | $this->identifier = 'route' . $identifier; 97 | } 98 | 99 | /** 100 | * Finalize the route in preparation for dispatching 101 | */ 102 | public function finalize() 103 | { 104 | if ($this->finalized) { 105 | return; 106 | } 107 | 108 | $groupMiddleware = []; 109 | foreach ($this->getGroups() as $group) { 110 | $groupMiddleware = array_merge($group->getMiddleware(), $groupMiddleware); 111 | } 112 | 113 | $this->middleware = array_merge($this->middleware, $groupMiddleware); 114 | 115 | foreach ($this->getMiddleware() as $middleware) { 116 | $this->addMiddleware($middleware); 117 | } 118 | 119 | $this->finalized = true; 120 | } 121 | 122 | /** 123 | * Get route callable 124 | * 125 | * @return callable 126 | */ 127 | public function getCallable() 128 | { 129 | return $this->callable; 130 | } 131 | 132 | /** 133 | * This method enables you to override the Route's callable 134 | * 135 | * @param string|\Closure $callable 136 | */ 137 | public function setCallable($callable) 138 | { 139 | $this->callable = $callable; 140 | } 141 | 142 | /** 143 | * Get route methods 144 | * 145 | * @return string[] 146 | */ 147 | public function getMethods() 148 | { 149 | return $this->methods; 150 | } 151 | 152 | /** 153 | * Get parent route groups 154 | * 155 | * @return RouteGroup[] 156 | */ 157 | public function getGroups() 158 | { 159 | return $this->groups; 160 | } 161 | 162 | /** 163 | * Get route name 164 | * 165 | * @return null|string 166 | */ 167 | public function getName() 168 | { 169 | return $this->name; 170 | } 171 | 172 | /** 173 | * Get route identifier 174 | * 175 | * @return string 176 | */ 177 | public function getIdentifier() 178 | { 179 | return $this->identifier; 180 | } 181 | 182 | /** 183 | * Get output buffering mode 184 | * 185 | * @return boolean|string 186 | */ 187 | public function getOutputBuffering() 188 | { 189 | return $this->outputBuffering; 190 | } 191 | 192 | /** 193 | * Set output buffering mode 194 | * 195 | * One of: false, 'prepend' or 'append' 196 | * 197 | * @param boolean|string $mode 198 | * 199 | * @throws InvalidArgumentException If an unknown buffering mode is specified 200 | */ 201 | public function setOutputBuffering($mode) 202 | { 203 | if (!in_array($mode, [false, 'prepend', 'append'], true)) { 204 | throw new InvalidArgumentException('Unknown output buffering mode'); 205 | } 206 | $this->outputBuffering = $mode; 207 | } 208 | 209 | /** 210 | * Set route name 211 | * 212 | * @param string $name 213 | * 214 | * @return self 215 | * 216 | * @throws InvalidArgumentException if the route name is not a string 217 | */ 218 | public function setName($name) 219 | { 220 | if (!is_string($name)) { 221 | throw new InvalidArgumentException('Route name must be a string'); 222 | } 223 | $this->name = $name; 224 | return $this; 225 | } 226 | 227 | /** 228 | * Set a route argument 229 | * 230 | * @param string $name 231 | * @param string $value 232 | * 233 | * @return self 234 | */ 235 | public function setArgument($name, $value) 236 | { 237 | $this->arguments[$name] = $value; 238 | return $this; 239 | } 240 | 241 | /** 242 | * Replace route arguments 243 | * 244 | * @param array $arguments 245 | * 246 | * @return self 247 | */ 248 | public function setArguments(array $arguments) 249 | { 250 | $this->arguments = $arguments; 251 | return $this; 252 | } 253 | 254 | /** 255 | * Retrieve route arguments 256 | * 257 | * @return array 258 | */ 259 | public function getArguments() 260 | { 261 | return $this->arguments; 262 | } 263 | 264 | /** 265 | * Retrieve a specific route argument 266 | * 267 | * @param string $name 268 | * @param string|null $default 269 | * 270 | * @return mixed 271 | */ 272 | public function getArgument($name, $default = null) 273 | { 274 | if (array_key_exists($name, $this->arguments)) { 275 | return $this->arguments[$name]; 276 | } 277 | return $default; 278 | } 279 | 280 | /******************************************************************************** 281 | * Route Runner 282 | *******************************************************************************/ 283 | 284 | /** 285 | * Prepare the route for use 286 | * 287 | * @param ServerRequestInterface $request 288 | * @param array $arguments 289 | */ 290 | public function prepare(ServerRequestInterface $request, array $arguments) 291 | { 292 | // Add the arguments 293 | foreach ($arguments as $k => $v) { 294 | $this->setArgument($k, $v); 295 | } 296 | } 297 | 298 | /** 299 | * Run route 300 | * 301 | * This method traverses the middleware stack, including the route's callable 302 | * and captures the resultant HTTP response object. It then sends the response 303 | * back to the Application. 304 | * 305 | * @param ServerRequestInterface $request 306 | * @param ResponseInterface $response 307 | * 308 | * @return ResponseInterface 309 | */ 310 | public function run(ServerRequestInterface $request, ResponseInterface $response) 311 | { 312 | // Finalise route now that we are about to run it 313 | $this->finalize(); 314 | 315 | // Traverse middleware stack and fetch updated response 316 | return $this->callMiddlewareStack($request, $response); 317 | } 318 | 319 | /** 320 | * Dispatch route callable against current Request and Response objects 321 | * 322 | * This method invokes the route object's callable. If middleware is 323 | * registered for the route, each callable middleware is invoked in 324 | * the order specified. 325 | * 326 | * @param ServerRequestInterface $request The current Request object 327 | * @param ResponseInterface $response The current Response object 328 | * @return \Psr\Http\Message\ResponseInterface 329 | * @throws \Exception if the route callable throws an exception 330 | */ 331 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response) 332 | { 333 | $this->callable = $this->resolveCallable($this->callable); 334 | 335 | /** @var InvocationStrategyInterface $handler */ 336 | $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse(); 337 | 338 | // invoke route callable 339 | if ($this->outputBuffering === false) { 340 | $newResponse = $handler($this->callable, $request, $response, $this->arguments); 341 | } else { 342 | try { 343 | ob_start(); 344 | $newResponse = $handler($this->callable, $request, $response, $this->arguments); 345 | $output = ob_get_clean(); 346 | // @codeCoverageIgnoreStart 347 | } catch (Throwable $e) { 348 | ob_end_clean(); 349 | throw $e; 350 | // @codeCoverageIgnoreEnd 351 | } catch (Exception $e) { 352 | ob_end_clean(); 353 | throw $e; 354 | } 355 | } 356 | 357 | if ($newResponse instanceof ResponseInterface) { 358 | // if route callback returns a ResponseInterface, then use it 359 | $response = $newResponse; 360 | } elseif (is_string($newResponse)) { 361 | // if route callback returns a string, then append it to the response 362 | if ($response->getBody()->isWritable()) { 363 | $response->getBody()->write($newResponse); 364 | } 365 | } 366 | 367 | if (!empty($output) && $response->getBody()->isWritable()) { 368 | if ($this->outputBuffering === 'prepend') { 369 | // prepend output buffer content 370 | $body = new Http\Body(fopen('php://temp', 'r+')); 371 | $body->write($output . $response->getBody()); 372 | $response = $response->withBody($body); 373 | } elseif ($this->outputBuffering === 'append') { 374 | // append output buffer content 375 | $response->getBody()->write($output); 376 | } 377 | } 378 | 379 | return $response; 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /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 %1s 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 %1s to %2s', $this->name, $targetPath)); 237 | } 238 | if (!unlink($this->file)) { 239 | throw new RuntimeException(sprintf('Error removing uploaded file %1s', $this->name)); 240 | } 241 | } elseif ($this->sapi) { 242 | if (!is_uploaded_file($this->file)) { 243 | throw new RuntimeException(sprintf('%1s 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 %1s to %2s', $this->name, $targetPath)); 248 | } 249 | } else { 250 | if (!rename($this->file, $targetPath)) { 251 | throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $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 | 101 => 'Switching Protocols', 52 | 102 => 'Processing', 53 | //Successful 2xx 54 | 200 => 'OK', 55 | 201 => 'Created', 56 | 202 => 'Accepted', 57 | 203 => 'Non-Authoritative Information', 58 | 204 => 'No Content', 59 | 205 => 'Reset Content', 60 | 206 => 'Partial Content', 61 | 207 => 'Multi-Status', 62 | 208 => 'Already Reported', 63 | 226 => 'IM Used', 64 | //Redirection 3xx 65 | 300 => 'Multiple Choices', 66 | 301 => 'Moved Permanently', 67 | 302 => 'Found', 68 | 303 => 'See Other', 69 | 304 => 'Not Modified', 70 | 305 => 'Use Proxy', 71 | 306 => '(Unused)', 72 | 307 => 'Temporary Redirect', 73 | 308 => 'Permanent Redirect', 74 | //Client Error 4xx 75 | 400 => 'Bad Request', 76 | 401 => 'Unauthorized', 77 | 402 => 'Payment Required', 78 | 403 => 'Forbidden', 79 | 404 => 'Not Found', 80 | 405 => 'Method Not Allowed', 81 | 406 => 'Not Acceptable', 82 | 407 => 'Proxy Authentication Required', 83 | 408 => 'Request Timeout', 84 | 409 => 'Conflict', 85 | 410 => 'Gone', 86 | 411 => 'Length Required', 87 | 412 => 'Precondition Failed', 88 | 413 => 'Request Entity Too Large', 89 | 414 => 'Request-URI Too Long', 90 | 415 => 'Unsupported Media Type', 91 | 416 => 'Requested Range Not Satisfiable', 92 | 417 => 'Expectation Failed', 93 | 418 => 'I\'m a teapot', 94 | 421 => 'Misdirected Request', 95 | 422 => 'Unprocessable Entity', 96 | 423 => 'Locked', 97 | 424 => 'Failed Dependency', 98 | 426 => 'Upgrade Required', 99 | 428 => 'Precondition Required', 100 | 429 => 'Too Many Requests', 101 | 431 => 'Request Header Fields Too Large', 102 | 444 => 'Connection Closed Without Response', 103 | 451 => 'Unavailable For Legal Reasons', 104 | 499 => 'Client Closed Request', 105 | //Server Error 5xx 106 | 500 => 'Internal Server Error', 107 | 501 => 'Not Implemented', 108 | 502 => 'Bad Gateway', 109 | 503 => 'Service Unavailable', 110 | 504 => 'Gateway Timeout', 111 | 505 => 'HTTP Version Not Supported', 112 | 506 => 'Variant Also Negotiates', 113 | 507 => 'Insufficient Storage', 114 | 508 => 'Loop Detected', 115 | 510 => 'Not Extended', 116 | 511 => 'Network Authentication Required', 117 | 599 => '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($status = 200, HeadersInterface $headers = null, StreamInterface $body = null) 135 | { 136 | $this->status = $this->filterStatus($status); 137 | $this->headers = $headers ? $headers : new Headers(); 138 | $this->body = $body ? $body : new Body(fopen('php://temp', 'r+')); 139 | } 140 | 141 | /** 142 | * This method is applied to the cloned object 143 | * after PHP performs an initial shallow-copy. This 144 | * method completes a deep-copy by creating new objects 145 | * for the cloned object's internal reference pointers. 146 | */ 147 | public function __clone() 148 | { 149 | $this->headers = clone $this->headers; 150 | } 151 | 152 | /******************************************************************************* 153 | * Status 154 | ******************************************************************************/ 155 | 156 | /** 157 | * Gets the response status code. 158 | * 159 | * The status code is a 3-digit integer result code of the server's attempt 160 | * to understand and satisfy the request. 161 | * 162 | * @return int Status code. 163 | */ 164 | public function getStatusCode() 165 | { 166 | return $this->status; 167 | } 168 | 169 | /** 170 | * Return an instance with the specified status code and, optionally, reason phrase. 171 | * 172 | * If no reason phrase is specified, implementations MAY choose to default 173 | * to the RFC 7231 or IANA recommended reason phrase for the response's 174 | * status code. 175 | * 176 | * This method MUST be implemented in such a way as to retain the 177 | * immutability of the message, and MUST return an instance that has the 178 | * updated status and reason phrase. 179 | * 180 | * @link http://tools.ietf.org/html/rfc7231#section-6 181 | * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 182 | * @param int $code The 3-digit integer result code to set. 183 | * @param string $reasonPhrase The reason phrase to use with the 184 | * provided status code; if none is provided, implementations MAY 185 | * use the defaults as suggested in the HTTP specification. 186 | * @return static 187 | * @throws \InvalidArgumentException For invalid status code arguments. 188 | */ 189 | public function withStatus($code, $reasonPhrase = '') 190 | { 191 | $code = $this->filterStatus($code); 192 | 193 | if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) { 194 | throw new InvalidArgumentException('ReasonPhrase must be a string'); 195 | } 196 | 197 | $clone = clone $this; 198 | $clone->status = $code; 199 | if ($reasonPhrase === '' && isset(static::$messages[$code])) { 200 | $reasonPhrase = static::$messages[$code]; 201 | } 202 | 203 | if ($reasonPhrase === '') { 204 | throw new InvalidArgumentException('ReasonPhrase must be supplied for this code'); 205 | } 206 | 207 | $clone->reasonPhrase = $reasonPhrase; 208 | 209 | return $clone; 210 | } 211 | 212 | /** 213 | * Filter HTTP status code. 214 | * 215 | * @param int $status HTTP status code. 216 | * @return int 217 | * @throws \InvalidArgumentException If an invalid HTTP status code is provided. 218 | */ 219 | protected function filterStatus($status) 220 | { 221 | if (!is_integer($status) || $status<100 || $status>599) { 222 | throw new InvalidArgumentException('Invalid HTTP status code'); 223 | } 224 | 225 | return $status; 226 | } 227 | 228 | /** 229 | * Gets the response reason phrase associated with the status code. 230 | * 231 | * Because a reason phrase is not a required element in a response 232 | * status line, the reason phrase value MAY be null. Implementations MAY 233 | * choose to return the default RFC 7231 recommended reason phrase (or those 234 | * listed in the IANA HTTP Status Code Registry) for the response's 235 | * status code. 236 | * 237 | * @link http://tools.ietf.org/html/rfc7231#section-6 238 | * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 239 | * @return string Reason phrase; must return an empty string if none present. 240 | */ 241 | public function getReasonPhrase() 242 | { 243 | if ($this->reasonPhrase) { 244 | return $this->reasonPhrase; 245 | } 246 | if (isset(static::$messages[$this->status])) { 247 | return static::$messages[$this->status]; 248 | } 249 | return ''; 250 | } 251 | 252 | /******************************************************************************* 253 | * Body 254 | ******************************************************************************/ 255 | 256 | /** 257 | * Write data to the response body. 258 | * 259 | * Note: This method is not part of the PSR-7 standard. 260 | * 261 | * Proxies to the underlying stream and writes the provided data to it. 262 | * 263 | * @param string $data 264 | * @return $this 265 | */ 266 | public function write($data) 267 | { 268 | $this->getBody()->write($data); 269 | 270 | return $this; 271 | } 272 | 273 | /******************************************************************************* 274 | * Response Helpers 275 | ******************************************************************************/ 276 | 277 | /** 278 | * Redirect. 279 | * 280 | * Note: This method is not part of the PSR-7 standard. 281 | * 282 | * This method prepares the response object to return an HTTP Redirect 283 | * response to the client. 284 | * 285 | * @param string|UriInterface $url The redirect destination. 286 | * @param int|null $status The redirect HTTP status code. 287 | * @return static 288 | */ 289 | public function withRedirect($url, $status = null) 290 | { 291 | $responseWithRedirect = $this->withHeader('Location', (string)$url); 292 | 293 | if (is_null($status) && $this->getStatusCode() === 200) { 294 | $status = 302; 295 | } 296 | 297 | if (!is_null($status)) { 298 | return $responseWithRedirect->withStatus($status); 299 | } 300 | 301 | return $responseWithRedirect; 302 | } 303 | 304 | /** 305 | * Json. 306 | * 307 | * Note: This method is not part of the PSR-7 standard. 308 | * 309 | * This method prepares the response object to return an HTTP Json 310 | * response to the client. 311 | * 312 | * @param mixed $data The data 313 | * @param int $status The HTTP status code. 314 | * @param int $encodingOptions Json encoding options 315 | * @throws \RuntimeException 316 | * @return static 317 | */ 318 | public function withJson($data, $status = null, $encodingOptions = 0) 319 | { 320 | $response = $this->withBody(new Body(fopen('php://temp', 'r+'))); 321 | $response->body->write($json = json_encode($data, $encodingOptions)); 322 | 323 | // Ensure that the json encoding passed successfully 324 | if ($json === false) { 325 | throw new \RuntimeException(json_last_error_msg(), json_last_error()); 326 | } 327 | 328 | $responseWithJson = $response->withHeader('Content-Type', 'application/json;charset=utf-8'); 329 | if (isset($status)) { 330 | return $responseWithJson->withStatus($status); 331 | } 332 | return $responseWithJson; 333 | } 334 | 335 | /** 336 | * Is this response empty? 337 | * 338 | * Note: This method is not part of the PSR-7 standard. 339 | * 340 | * @return bool 341 | */ 342 | public function isEmpty() 343 | { 344 | return in_array($this->getStatusCode(), [204, 205, 304]); 345 | } 346 | 347 | /** 348 | * Is this response informational? 349 | * 350 | * Note: This method is not part of the PSR-7 standard. 351 | * 352 | * @return bool 353 | */ 354 | public function isInformational() 355 | { 356 | return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; 357 | } 358 | 359 | /** 360 | * Is this response OK? 361 | * 362 | * Note: This method is not part of the PSR-7 standard. 363 | * 364 | * @return bool 365 | */ 366 | public function isOk() 367 | { 368 | return $this->getStatusCode() === 200; 369 | } 370 | 371 | /** 372 | * Is this response successful? 373 | * 374 | * Note: This method is not part of the PSR-7 standard. 375 | * 376 | * @return bool 377 | */ 378 | public function isSuccessful() 379 | { 380 | return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; 381 | } 382 | 383 | /** 384 | * Is this response a redirect? 385 | * 386 | * Note: This method is not part of the PSR-7 standard. 387 | * 388 | * @return bool 389 | */ 390 | public function isRedirect() 391 | { 392 | return in_array($this->getStatusCode(), [301, 302, 303, 307]); 393 | } 394 | 395 | /** 396 | * Is this response a redirection? 397 | * 398 | * Note: This method is not part of the PSR-7 standard. 399 | * 400 | * @return bool 401 | */ 402 | public function isRedirection() 403 | { 404 | return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; 405 | } 406 | 407 | /** 408 | * Is this response forbidden? 409 | * 410 | * Note: This method is not part of the PSR-7 standard. 411 | * 412 | * @return bool 413 | * @api 414 | */ 415 | public function isForbidden() 416 | { 417 | return $this->getStatusCode() === 403; 418 | } 419 | 420 | /** 421 | * Is this response not Found? 422 | * 423 | * Note: This method is not part of the PSR-7 standard. 424 | * 425 | * @return bool 426 | */ 427 | public function isNotFound() 428 | { 429 | return $this->getStatusCode() === 404; 430 | } 431 | 432 | /** 433 | * Is this response a client error? 434 | * 435 | * Note: This method is not part of the PSR-7 standard. 436 | * 437 | * @return bool 438 | */ 439 | public function isClientError() 440 | { 441 | return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; 442 | } 443 | 444 | /** 445 | * Is this response a server error? 446 | * 447 | * Note: This method is not part of the PSR-7 standard. 448 | * 449 | * @return bool 450 | */ 451 | public function isServerError() 452 | { 453 | return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; 454 | } 455 | 456 | /** 457 | * Convert response to string. 458 | * 459 | * Note: This method is not part of the PSR-7 standard. 460 | * 461 | * @return string 462 | */ 463 | public function __toString() 464 | { 465 | $output = sprintf( 466 | 'HTTP/%s %s %s', 467 | $this->getProtocolVersion(), 468 | $this->getStatusCode(), 469 | $this->getReasonPhrase() 470 | ); 471 | $output .= Response::EOL; 472 | foreach ($this->getHeaders() as $name => $values) { 473 | $output .= sprintf('%s: %s', $name, $this->getHeaderLine($name)) . Response::EOL; 474 | } 475 | $output .= Response::EOL; 476 | $output .= (string)$this->getBody(); 477 | 478 | return $output; 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /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 | * Route Groups 253 | * 254 | * This method accepts a route pattern and a callback. All route 255 | * declarations in the callback will be prepended by the group(s) 256 | * that it is in. 257 | * 258 | * @param string $pattern 259 | * @param callable $callable 260 | * 261 | * @return RouteGroupInterface 262 | */ 263 | public function group($pattern, $callable) 264 | { 265 | /** @var RouteGroup $group */ 266 | $group = $this->container->get('router')->pushGroup($pattern, $callable); 267 | $group->setContainer($this->container); 268 | $group($this); 269 | $this->container->get('router')->popGroup(); 270 | return $group; 271 | } 272 | 273 | /******************************************************************************** 274 | * Runner 275 | *******************************************************************************/ 276 | 277 | /** 278 | * Run application 279 | * 280 | * This method traverses the application middleware stack and then sends the 281 | * resultant Response object to the HTTP client. 282 | * 283 | * @param bool|false $silent 284 | * @return ResponseInterface 285 | * 286 | * @throws Exception 287 | * @throws MethodNotAllowedException 288 | * @throws NotFoundException 289 | */ 290 | public function run($silent = false) 291 | { 292 | $response = $this->container->get('response'); 293 | 294 | try { 295 | $response = $this->process($this->container->get('request'), $response); 296 | } catch (InvalidMethodException $e) { 297 | $response = $this->processInvalidMethod($e->getRequest(), $response); 298 | } 299 | 300 | if (!$silent) { 301 | $this->respond($response); 302 | } 303 | 304 | return $response; 305 | } 306 | 307 | /** 308 | * Pull route info for a request with a bad method to decide whether to 309 | * return a not-found error (default) or a bad-method error, then run 310 | * the handler for that error, returning the resulting response. 311 | * 312 | * Used for cases where an incoming request has an unrecognized method, 313 | * rather than throwing an exception and not catching it all the way up. 314 | * 315 | * @param ServerRequestInterface $request 316 | * @param ResponseInterface $response 317 | * @return ResponseInterface 318 | */ 319 | protected function processInvalidMethod(ServerRequestInterface $request, ResponseInterface $response) 320 | { 321 | $router = $this->container->get('router'); 322 | if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) { 323 | $router->setBasePath($request->getUri()->getBasePath()); 324 | } 325 | 326 | $request = $this->dispatchRouterAndPrepareRoute($request, $router); 327 | $routeInfo = $request->getAttribute('routeInfo', [RouterInterface::DISPATCH_STATUS => Dispatcher::NOT_FOUND]); 328 | 329 | if ($routeInfo[RouterInterface::DISPATCH_STATUS] === Dispatcher::METHOD_NOT_ALLOWED) { 330 | return $this->handleException( 331 | new MethodNotAllowedException($request, $response, $routeInfo[RouterInterface::ALLOWED_METHODS]), 332 | $request, 333 | $response 334 | ); 335 | } 336 | 337 | return $this->handleException(new NotFoundException($request, $response), $request, $response); 338 | } 339 | 340 | /** 341 | * Process a request 342 | * 343 | * This method traverses the application middleware stack and then returns the 344 | * resultant Response object. 345 | * 346 | * @param ServerRequestInterface $request 347 | * @param ResponseInterface $response 348 | * @return ResponseInterface 349 | * 350 | * @throws Exception 351 | * @throws MethodNotAllowedException 352 | * @throws NotFoundException 353 | */ 354 | public function process(ServerRequestInterface $request, ResponseInterface $response) 355 | { 356 | // Ensure basePath is set 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 | // Dispatch the Router first if the setting for this is on 363 | if ($this->container->get('settings')['determineRouteBeforeAppMiddleware'] === true) { 364 | // Dispatch router (note: you won't be able to alter routes after this) 365 | $request = $this->dispatchRouterAndPrepareRoute($request, $router); 366 | } 367 | 368 | // Traverse middleware stack 369 | try { 370 | $response = $this->callMiddlewareStack($request, $response); 371 | } catch (Exception $e) { 372 | $response = $this->handleException($e, $request, $response); 373 | } catch (Throwable $e) { 374 | $response = $this->handlePhpError($e, $request, $response); 375 | } 376 | 377 | $response = $this->finalize($response); 378 | 379 | return $response; 380 | } 381 | 382 | /** 383 | * Send the response the client 384 | * 385 | * @param ResponseInterface $response 386 | */ 387 | public function respond(ResponseInterface $response) 388 | { 389 | // Send response 390 | if (!headers_sent()) { 391 | // Status 392 | header(sprintf( 393 | 'HTTP/%s %s %s', 394 | $response->getProtocolVersion(), 395 | $response->getStatusCode(), 396 | $response->getReasonPhrase() 397 | )); 398 | 399 | // Headers 400 | foreach ($response->getHeaders() as $name => $values) { 401 | foreach ($values as $value) { 402 | header(sprintf('%s: %s', $name, $value), false); 403 | } 404 | } 405 | } 406 | 407 | // Body 408 | if (!$this->isEmptyResponse($response)) { 409 | $body = $response->getBody(); 410 | if ($body->isSeekable()) { 411 | $body->rewind(); 412 | } 413 | $settings = $this->container->get('settings'); 414 | $chunkSize = $settings['responseChunkSize']; 415 | 416 | $contentLength = $response->getHeaderLine('Content-Length'); 417 | if (!$contentLength) { 418 | $contentLength = $body->getSize(); 419 | } 420 | 421 | 422 | if (isset($contentLength)) { 423 | $amountToRead = $contentLength; 424 | while ($amountToRead > 0 && !$body->eof()) { 425 | $data = $body->read(min($chunkSize, $amountToRead)); 426 | echo $data; 427 | 428 | $amountToRead -= strlen($data); 429 | 430 | if (connection_status() != CONNECTION_NORMAL) { 431 | break; 432 | } 433 | } 434 | } else { 435 | while (!$body->eof()) { 436 | echo $body->read($chunkSize); 437 | if (connection_status() != CONNECTION_NORMAL) { 438 | break; 439 | } 440 | } 441 | } 442 | } 443 | } 444 | 445 | /** 446 | * Invoke application 447 | * 448 | * This method implements the middleware interface. It receives 449 | * Request and Response objects, and it returns a Response object 450 | * after compiling the routes registered in the Router and dispatching 451 | * the Request object to the appropriate Route callback routine. 452 | * 453 | * @param ServerRequestInterface $request The most recent Request object 454 | * @param ResponseInterface $response The most recent Response object 455 | * 456 | * @return ResponseInterface 457 | * @throws MethodNotAllowedException 458 | * @throws NotFoundException 459 | */ 460 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response) 461 | { 462 | // Get the route info 463 | $routeInfo = $request->getAttribute('routeInfo'); 464 | 465 | /** @var \Slim\Interfaces\RouterInterface $router */ 466 | $router = $this->container->get('router'); 467 | 468 | // If router hasn't been dispatched or the URI changed then dispatch 469 | if (null === $routeInfo || ($routeInfo['request'] !== [$request->getMethod(), (string) $request->getUri()])) { 470 | $request = $this->dispatchRouterAndPrepareRoute($request, $router); 471 | $routeInfo = $request->getAttribute('routeInfo'); 472 | } 473 | 474 | if ($routeInfo[0] === Dispatcher::FOUND) { 475 | $route = $router->lookupRoute($routeInfo[1]); 476 | return $route->run($request, $response); 477 | } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { 478 | if (!$this->container->has('notAllowedHandler')) { 479 | throw new MethodNotAllowedException($request, $response, $routeInfo[1]); 480 | } 481 | /** @var callable $notAllowedHandler */ 482 | $notAllowedHandler = $this->container->get('notAllowedHandler'); 483 | return $notAllowedHandler($request, $response, $routeInfo[1]); 484 | } 485 | 486 | if (!$this->container->has('notFoundHandler')) { 487 | throw new NotFoundException($request, $response); 488 | } 489 | /** @var callable $notFoundHandler */ 490 | $notFoundHandler = $this->container->get('notFoundHandler'); 491 | return $notFoundHandler($request, $response); 492 | } 493 | 494 | /** 495 | * Perform a sub-request from within an application route 496 | * 497 | * This method allows you to prepare and initiate a sub-request, run within 498 | * the context of the current request. This WILL NOT issue a remote HTTP 499 | * request. Instead, it will route the provided URL, method, headers, 500 | * cookies, body, and server variables against the set of registered 501 | * application routes. The result response object is returned. 502 | * 503 | * @param string $method The request method (e.g., GET, POST, PUT, etc.) 504 | * @param string $path The request URI path 505 | * @param string $query The request URI query string 506 | * @param array $headers The request headers (key-value array) 507 | * @param array $cookies The request cookies (key-value array) 508 | * @param string $bodyContent The request body 509 | * @param ResponseInterface $response The response object (optional) 510 | * @return ResponseInterface 511 | */ 512 | public function subRequest( 513 | $method, 514 | $path, 515 | $query = '', 516 | array $headers = [], 517 | array $cookies = [], 518 | $bodyContent = '', 519 | ResponseInterface $response = null 520 | ) { 521 | $env = $this->container->get('environment'); 522 | $uri = Uri::createFromEnvironment($env)->withPath($path)->withQuery($query); 523 | $headers = new Headers($headers); 524 | $serverParams = $env->all(); 525 | $body = new Body(fopen('php://temp', 'r+')); 526 | $body->write($bodyContent); 527 | $body->rewind(); 528 | $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); 529 | 530 | if (!$response) { 531 | $response = $this->container->get('response'); 532 | } 533 | 534 | return $this($request, $response); 535 | } 536 | 537 | /** 538 | * Dispatch the router to find the route. Prepare the route for use. 539 | * 540 | * @param ServerRequestInterface $request 541 | * @param RouterInterface $router 542 | * @return ServerRequestInterface 543 | */ 544 | protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request, RouterInterface $router) 545 | { 546 | $routeInfo = $router->dispatch($request); 547 | 548 | if ($routeInfo[0] === Dispatcher::FOUND) { 549 | $routeArguments = []; 550 | foreach ($routeInfo[2] as $k => $v) { 551 | $routeArguments[$k] = urldecode($v); 552 | } 553 | 554 | $route = $router->lookupRoute($routeInfo[1]); 555 | $route->prepare($request, $routeArguments); 556 | 557 | // add route to the request's attributes in case a middleware or handler needs access to the route 558 | $request = $request->withAttribute('route', $route); 559 | } 560 | 561 | $routeInfo['request'] = [$request->getMethod(), (string) $request->getUri()]; 562 | 563 | return $request->withAttribute('routeInfo', $routeInfo); 564 | } 565 | 566 | /** 567 | * Finalize response 568 | * 569 | * @param ResponseInterface $response 570 | * @return ResponseInterface 571 | */ 572 | protected function finalize(ResponseInterface $response) 573 | { 574 | // stop PHP sending a Content-Type automatically 575 | ini_set('default_mimetype', ''); 576 | 577 | if ($this->isEmptyResponse($response)) { 578 | return $response->withoutHeader('Content-Type')->withoutHeader('Content-Length'); 579 | } 580 | 581 | // Add Content-Length header if `addContentLengthHeader` setting is set 582 | if (isset($this->container->get('settings')['addContentLengthHeader']) && 583 | $this->container->get('settings')['addContentLengthHeader'] == true) { 584 | if (ob_get_length() > 0) { 585 | throw new \RuntimeException("Unexpected data in output buffer. " . 586 | "Maybe you have characters before an opening getBody()->getSize(); 589 | if ($size !== null && !$response->hasHeader('Content-Length')) { 590 | $response = $response->withHeader('Content-Length', (string) $size); 591 | } 592 | } 593 | 594 | return $response; 595 | } 596 | 597 | /** 598 | * Helper method, which returns true if the provided response must not output a body and false 599 | * if the response could have a body. 600 | * 601 | * @see https://tools.ietf.org/html/rfc7231 602 | * 603 | * @param ResponseInterface $response 604 | * @return bool 605 | */ 606 | protected function isEmptyResponse(ResponseInterface $response) 607 | { 608 | if (method_exists($response, 'isEmpty')) { 609 | return $response->isEmpty(); 610 | } 611 | 612 | return in_array($response->getStatusCode(), [204, 205, 304]); 613 | } 614 | 615 | /** 616 | * Call relevant handler from the Container if needed. If it doesn't exist, 617 | * then just re-throw. 618 | * 619 | * @param Exception $e 620 | * @param ServerRequestInterface $request 621 | * @param ResponseInterface $response 622 | * 623 | * @return ResponseInterface 624 | * @throws Exception if a handler is needed and not found 625 | */ 626 | protected function handleException(Exception $e, ServerRequestInterface $request, ResponseInterface $response) 627 | { 628 | if ($e instanceof MethodNotAllowedException) { 629 | $handler = 'notAllowedHandler'; 630 | $params = [$e->getRequest(), $e->getResponse(), $e->getAllowedMethods()]; 631 | } elseif ($e instanceof NotFoundException) { 632 | $handler = 'notFoundHandler'; 633 | $params = [$e->getRequest(), $e->getResponse(), $e]; 634 | } elseif ($e instanceof SlimException) { 635 | // This is a Stop exception and contains the response 636 | return $e->getResponse(); 637 | } else { 638 | // Other exception, use $request and $response params 639 | $handler = 'errorHandler'; 640 | $params = [$request, $response, $e]; 641 | } 642 | 643 | if ($this->container->has($handler)) { 644 | $callable = $this->container->get($handler); 645 | // Call the registered handler 646 | return call_user_func_array($callable, $params); 647 | } 648 | 649 | // No handlers found, so just throw the exception 650 | throw $e; 651 | } 652 | 653 | /** 654 | * Call relevant handler from the Container if needed. If it doesn't exist, 655 | * then just re-throw. 656 | * 657 | * @param Throwable $e 658 | * @param ServerRequestInterface $request 659 | * @param ResponseInterface $response 660 | * @return ResponseInterface 661 | * @throws Throwable 662 | */ 663 | protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response) 664 | { 665 | $handler = 'phpErrorHandler'; 666 | $params = [$request, $response, $e]; 667 | 668 | if ($this->container->has($handler)) { 669 | $callable = $this->container->get($handler); 670 | // Call the registered handler 671 | return call_user_func_array($callable, $params); 672 | } 673 | 674 | // No handlers found, so just throw the exception 675 | throw $e; 676 | } 677 | } 678 | --------------------------------------------------------------------------------