/** * Route the current request * * @param Request $request The current request */ public function route(Request $request) { $httpMethod = $request->getMethod(); if ($httpMethod === 'BREW') { throw new RuntimeException('I\'m a teapot!', 418); } if (!isset(self::$supportedHttpMethods[$httpMethod])) { throw new RuntimeException('Unsupported HTTP method: ' . $httpMethod, 501); } $path = $request->getPathInfo(); $matches = []; foreach ($this->routes as $resourceName => $route) { if (preg_match($route, $path, $matches)) { break; } } // Path matched no route if (!$matches) { throw new RuntimeException('Not Found', 404); } // Create and populate a route instance that we want to inject into the request $route = new Route(); $route->setName($resourceName); // Inject all matches into the route as parameters foreach ($matches as $key => $value) { if (is_string($key)) { $route->set($key, $value); } } // Store the route in the request $request->setRoute($route); }
/** * @covers Imbo\Application::run */ public function testApplicationSetsTrustedProxies() { $this->expectOutputRegex('|{"version":"' . preg_quote(Version::VERSION, '|') . '",.*}|'); $this->assertEmpty(Request::getTrustedProxies()); $this->application->run(array('database' => $this->getMock('Imbo\\Database\\DatabaseInterface'), 'storage' => $this->getMock('Imbo\\Storage\\StorageInterface'), 'eventListenerInitializers' => [], 'eventListeners' => [], 'contentNegotiateImages' => false, 'resources' => [], 'routes' => [], 'auth' => [], 'trustedProxies' => ['10.0.0.77'])); $this->assertSame(['10.0.0.77'], Request::getTrustedProxies()); }
/** * Create an error based on an exception instance * * @param Exception $exception An Imbo\Exception instance * @param Request The current request * @return Error */ public static function createFromException(Exception $exception, Request $request) { $date = new DateTime('now', new DateTimeZone('UTC')); $model = new self(); $model->setHttpCode($exception->getCode())->setErrorMessage($exception->getMessage())->setDate($date)->setImboErrorCode($exception->getImboErrorCode() ?: Exception::ERR_UNSPECIFIED); if ($image = $request->getImage()) { $model->setImageIdentifier($image->getChecksum()); } else { if ($identifier = $request->getImageIdentifier()) { $model->setImageIdentifier($identifier); } } return $model; }
/** * @dataProvider getQueryStrings */ public function testGetRawUriDecodesUri($queryString, $expectedQueryString) { $request = new Request([], [], [], [], [], ['SERVER_NAME' => 'imbo', 'SERVER_PORT' => 80, 'QUERY_STRING' => $queryString]); $uri = $request->getRawUri(); $this->assertSame($expectedQueryString, substr($uri, strpos($uri, '?') + 1)); }
/** * Check if the request is whitelisted * * This method will whitelist a request only if all the transformations present in the request * are listed in the whitelist filter OR if the whitelist filter is empty, and the blacklist * filter has enties, but none of the transformations in the request are present in the * blacklist. * * @param Request $request The request instance * @return boolean */ private function isWhitelisted(Request $request) { $filter = $this->params['transformations']; if (empty($filter['whitelist']) && empty($filter['blacklist'])) { // No filter has been configured return false; } // Fetch transformations from the request $transformations = $request->getTransformations(); if (empty($transformations)) { // No transformations are present in the request, no need to check return false; } $whitelist = array_flip($filter['whitelist']); $blacklist = array_flip($filter['blacklist']); foreach ($transformations as $transformation) { if (isset($blacklist[$transformation['name']])) { // Transformation is explicitly blacklisted return false; } if (!empty($whitelist) && !isset($whitelist[$transformation['name']])) { // We have a whitelist, but the transformation is not listed in it, so we must deny // the request return false; } } // All transformations in the request are whitelisted return true; }
/** * Generate a cache key * * @param Request $request The current request instance * @return string Returns a string that can be used as a cache key for the current image */ private function getCacheKey(Request $request) { $user = $request->getUser(); $imageIdentifier = $request->getImageIdentifier(); $accept = $request->headers->get('Accept', '*/*'); $accept = array_filter(explode(',', $accept), function (&$value) { // Trim whitespace $value = trim($value); // Remove optional params $pos = strpos($value, ';'); if ($pos !== false) { $value = substr($value, 0, $pos); } // Keep values starting with "*/" or "image/" return $value[0] === '*' && $value[1] === '/' || substr($value, 0, 6) === 'image/'; }); // Sort the remaining values sort($accept); $accept = implode(',', $accept); $extension = $request->getExtension(); $transformations = $request->query->get('t'); if (!empty($transformations)) { $transformations = implode('&', $transformations); } return md5($user . $imageIdentifier . $accept . $extension . $transformations); }
/** * Run the application */ public function run(array $config) { // Request and response objects $request = Request::createFromGlobals(); Request::setTrustedProxies($config['trustedProxies']); $response = new Response(); $response->setPublic(); $response->headers->set('X-Imbo-Version', Version::VERSION); // Database and storage adapters $database = $config['database']; if (is_callable($database) && !$database instanceof DatabaseInterface) { $database = $database(); } if (!$database instanceof DatabaseInterface) { throw new InvalidArgumentException('Invalid database adapter', 500); } $storage = $config['storage']; if (is_callable($storage) && !$storage instanceof StorageInterface) { $storage = $storage(); } if (!$storage instanceof StorageInterface) { throw new InvalidArgumentException('Invalid storage adapter', 500); } // User lookup adapters $userLookup = $config['auth']; // Construct an ArrayStorage instance if the auth details is an array if (is_array($userLookup)) { $userLookup = new Auth\ArrayStorage($userLookup); } // Make sure the "auth" part of the configuration is an instance of the user lookup // interface if (!$userLookup instanceof Auth\UserLookupInterface) { throw new InvalidArgumentException('Invalid auth configuration', 500); } // Create a router based on the routes in the configuration and internal routes $router = new Router($config['routes']); // Create the event manager and the event template $eventManager = new EventManager(); $event = new Event(); $event->setArguments(array('request' => $request, 'response' => $response, 'database' => $database, 'storage' => $storage, 'userLookup' => $userLookup, 'config' => $config, 'manager' => $eventManager)); $eventManager->setEventTemplate($event); // A date formatter helper $dateFormatter = new Helpers\DateFormatter(); // Response formatters $formatters = array('json' => new Formatter\JSON($dateFormatter), 'xml' => new Formatter\XML($dateFormatter)); $contentNegotiation = new Http\ContentNegotiation(); // Collect event listener data $eventListeners = array('Imbo\\Resource\\Index', 'Imbo\\Resource\\Status', 'Imbo\\Resource\\Stats', 'Imbo\\Resource\\GlobalShortUrl', 'Imbo\\Resource\\ShortUrls', 'Imbo\\Resource\\ShortUrl', 'Imbo\\Resource\\User', 'Imbo\\Resource\\Images', 'Imbo\\Resource\\Image', 'Imbo\\Resource\\Metadata', 'Imbo\\Http\\Response\\ResponseFormatter' => array('formatters' => $formatters, 'contentNegotiation' => $contentNegotiation), 'Imbo\\EventListener\\DatabaseOperations', 'Imbo\\EventListener\\StorageOperations', 'Imbo\\Image\\ImagePreparation', 'Imbo\\EventListener\\ImageTransformer', 'Imbo\\EventListener\\ResponseSender', 'Imbo\\EventListener\\ResponseETag'); foreach ($eventListeners as $listener => $params) { if (is_string($params)) { $listener = $params; $params = array(); } $eventManager->addEventHandler($listener, $listener, $params)->addCallbacks($listener, $listener::getSubscribedEvents()); } // Event listener initializers foreach ($config['eventListenerInitializers'] as $name => $initializer) { if (!$initializer) { // The initializer has been disabled via config continue; } if (is_string($initializer)) { // The initializer has been specified as a string, representing a class name. Create // an instance $initializer = new $initializer(); } if (!$initializer instanceof InitializerInterface) { throw new InvalidArgumentException('Invalid event listener initializer: ' . $name, 500); } $eventManager->addInitializer($initializer); } // Listeners from configuration foreach ($config['eventListeners'] as $name => $definition) { if (!$definition) { // This occurs when a user disables a default event listener continue; } if (is_string($definition)) { // Class name $eventManager->addEventHandler($name, $definition)->addCallbacks($name, $definition::getSubscribedEvents()); continue; } if (is_callable($definition) && !$definition instanceof ListenerInterface) { // Callable piece of code which is not an implementation of the listener interface $definition = $definition(); } if ($definition instanceof ListenerInterface) { $eventManager->addEventHandler($name, $definition)->addCallbacks($name, $definition::getSubscribedEvents()); continue; } if (is_array($definition) && !empty($definition['listener'])) { $listener = $definition['listener']; $params = is_string($listener) && isset($definition['params']) ? $definition['params'] : array(); $publicKeys = isset($definition['publicKeys']) ? $definition['publicKeys'] : array(); if (is_callable($listener) && !$listener instanceof ListenerInterface) { $listener = $listener(); } if (!is_string($listener) && !$listener instanceof ListenerInterface) { throw new InvalidArgumentException('Invalid event listener definition', 500); } $eventManager->addEventHandler($name, $listener, $params)->addCallbacks($name, $listener::getSubscribedEvents(), $publicKeys); } else { if (is_array($definition) && !empty($definition['callback']) && !empty($definition['events'])) { $priority = 0; $events = array(); $publicKeys = array(); if (isset($definition['priority'])) { $priority = (int) $definition['priority']; } if (isset($definition['publicKeys'])) { $publicKeys = $definition['publicKeys']; } foreach ($definition['events'] as $event => $p) { if (is_int($event)) { $event = $p; $p = $priority; } $events[$event] = $p; } $eventManager->addEventHandler($name, $definition['callback'])->addCallbacks($name, $events, $publicKeys); } else { throw new InvalidArgumentException('Invalid event listener definition', 500); } } } // Custom resources foreach ($config['resources'] as $name => $resource) { if (is_callable($resource)) { $resource = $resource(); } $eventManager->addEventHandler($name, $resource)->addCallbacks($name, $resource::getSubscribedEvents()); } try { // Route the request $router->route($request); $eventManager->trigger('route.match'); // Create the resource $routeName = (string) $request->getRoute(); if (isset($config['resources'][$routeName])) { $resource = $config['resources'][$routeName]; if (is_callable($resource)) { $resource = $resource(); } if (is_string($resource)) { $resource = new $resource(); } if (!$resource instanceof ResourceInterface) { throw new InvalidArgumentException('Invalid resource class for route: ' . $routeName, 500); } } else { $className = 'Imbo\\Resource\\' . ucfirst($routeName); $resource = new $className(); } // Inform the user agent of which methods are allowed against this resource $response->headers->set('Allow', $resource->getAllowedMethods(), false); if ($publicKey = $request->getPublicKey()) { // Ensure that the public key actually exists if (!$userLookup->publicKeyExists($publicKey)) { $e = new RuntimeException('Public key not found', 404); $e->setImboErrorCode(Exception::AUTH_UNKNOWN_PUBLIC_KEY); throw $e; } } $methodName = strtolower($request->getMethod()); // Generate the event name based on the accessed resource and the HTTP method $eventName = $routeName . '.' . $methodName; if (!$eventManager->hasListenersForEvent($eventName)) { throw new RuntimeException('Method not allowed', 405); } $eventManager->trigger($eventName)->trigger('response.negotiate'); } catch (Exception $exception) { $negotiated = false; $error = Error::createFromException($exception, $request); $response->setError($error); // If the error is not from the previous attempt at doing content negotiation, force // another round since the model has changed into an error model. if ($exception->getCode() !== 406) { try { $eventManager->trigger('response.negotiate'); $negotiated = true; } catch (Exception $exception) { // The client does not accept any of the content types. Generate a new error $error = Error::createFromException($exception, $request); $response->setError($error); } } // Try to negotiate in a non-strict manner if the response format still has not been // chosen if (!$negotiated) { $eventManager->trigger('response.negotiate', array('noStrict' => true)); } } // Send the response $eventManager->trigger('response.send'); }
/** * Construct a message body with the data we need * * @param string $eventName Event that was triggered * @param Request $request Request that triggered this event * @param Response $response Response for this request * @param Imbo\EventManager\EventInterface $eventName Current event * @return array */ public function constructMessageBody($eventName, Request $request, Response $response, EventInterface $event) { if ($response->getModel() instanceof ErrorModel) { trigger_error($response->getModel()->getErrorMessage()); return; } // Construct the basics $message = ['eventName' => $eventName, 'request' => ['date' => date('c'), 'user' => $request->getUser(), 'publicKey' => $request->getPublicKey(), 'extension' => $request->getExtension(), 'url' => $request->getRawUri(), 'clientIp' => $request->getClientIp()], 'response' => ['statusCode' => $response->getStatusCode()]]; // Include any JSON request body in the message if ($request->getContentType() === 'application/json') { $message['request']['body'] = $request->getContent(); } // See if we've got an image identifier for this request $imageIdentifier = $request->getImageIdentifier(); // The imageIdentifier was not part of the URL, see if we have it in the response model if (!$imageIdentifier) { $responseData = $response->getModel()->getData(); if (isset($responseData['imageIdentifier'])) { $imageIdentifier = $responseData['imageIdentifier']; } else { return $message; } } // Get image information $image = $this->getImageData($event, $imageIdentifier); // Construct an array of all the image information we have $message['image'] = ['identifier' => $imageIdentifier, 'user' => $image->getUser() ?: $request->getUser(), 'size' => $image->getFilesize(), 'extension' => $image->getExtension(), 'mime' => $image->getMimeType(), 'added' => $image->getAddedDate()->format(DateTime::ATOM), 'updated' => $image->getUpdatedDate()->format(DateTime::ATOM), 'width' => $image->getWidth(), 'height' => $image->getHeight(), 'metadata' => $image->getMetadata()]; return $message; }