/** * Transform images * * @param EventInterface $event The current event */ public function transform(EventInterface $event) { $request = $event->getRequest(); $image = $event->getResponse()->getModel(); $eventManager = $event->getManager(); $presets = $event->getConfig()['transformationPresets']; // Fetch transformations specifed in the query and transform the image foreach ($request->getTransformations() as $transformation) { if (isset($presets[$transformation['name']])) { // Preset foreach ($presets[$transformation['name']] as $name => $params) { if (is_int($name)) { // No hardcoded params, use the ones from the request $name = $params; $params = $transformation['params']; } else { // Some hardcoded params. Merge with the ones from the request, making the // hardcoded params overwrite the ones from the request $params = array_replace($transformation['params'], $params); } $eventManager->trigger('image.transformation.' . strtolower($name), array('image' => $image, 'params' => $params)); } } else { // Regular transformation $eventManager->trigger('image.transformation.' . strtolower($transformation['name']), array('image' => $image, 'params' => $transformation['params'])); } } }
/** * Right before the response is sent to the client, check if any HTTP cache control headers * have explicity been set for this response. If not, apply the configured defaults. * * @param EventInterface $event The event instance */ public function setHeaders(EventInterface $event) { $method = $event->getRequest()->getMethod(); // Obviously we shouldn't bother doing any HTTP caching logic for non-GET/HEAD requests if ($method !== 'GET' && $method !== 'HEAD') { return; } $response = $event->getResponse(); $headers = $event->getResponse()->headers; // Imbo defaults to 'public' as cache-control value - if it has changed from this value, // assume the resource requested has explicitly defined its own caching rules and fall back if ($headers->get('Cache-Control') !== 'public') { return; } // Get configured HTTP cache defaults from configuration, then apply them $config = $event->getConfig()['httpCacheHeaders']; if (isset($config['maxAge'])) { $response->setMaxAge((int) $config['maxAge']); } if (isset($config['sharedMaxAge'])) { $response->setSharedMaxAge($config['sharedMaxAge']); } if (isset($config['public']) && $config['public']) { $response->setPublic(); } else { if (isset($config['public'])) { $response->setPrivate(); } } if (isset($config['mustRevalidate']) && $config['mustRevalidate']) { $headers->addCacheControlDirective('must-revalidate'); } }
/** * Using the configured image identifier generator, attempt to generate a unique image * identifier for the given image until we either have found a unique ID or we hit the maximum * allowed attempts. * * @param EventInterface $event The current event * @param Image $image The event to generate the image identifier for * @return string * @throws ImageException */ private function generateImageIdentifier(EventInterface $event, Image $image) { $database = $event->getDatabase(); $config = $event->getConfig(); $user = $event->getRequest()->getUser(); $imageIdentifierGenerator = $config['imageIdentifierGenerator']; if (is_callable($imageIdentifierGenerator) && !$imageIdentifierGenerator instanceof GeneratorInterface) { $imageIdentifierGenerator = $imageIdentifierGenerator(); } if ($imageIdentifierGenerator->isDeterministic()) { return $imageIdentifierGenerator->generate($image); } // Continue generating image identifiers until we get one that does not already exist $maxAttempts = 100; $attempts = 0; do { $imageIdentifier = $imageIdentifierGenerator->generate($image); $attempts++; } while ($attempts < $maxAttempts && $database->imageExists($user, $imageIdentifier)); // Did we reach our max attempts limit? if ($attempts === $maxAttempts) { $e = new ImageException('Failed to generate unique image identifier', 503); $e->setImboErrorCode(Exception::IMAGE_IDENTIFIER_GENERATION_FAILED); // Tell the client it's OK to retry later $event->getResponse()->headers->set('Retry-After', 1); throw $e; } return $imageIdentifier; }
/** * {@inheritdoc} */ public function checkAccessToken(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $query = $request->query; $eventName = $event->getName(); $config = $event->getConfig(); if (($eventName === 'image.get' || $eventName === 'image.head') && $this->isWhitelisted($request)) { // All transformations in the request are whitelisted. Skip the access token check return; } // If the response has a short URL header, we can skip the access token check if ($response->headers->has('X-Imbo-ShortUrl')) { return; } if (!$query->has('accessToken')) { throw new RuntimeException('Missing access token', 400); } $token = $query->get('accessToken'); // First the the raw un-encoded URI, then the URI as is $uris = [$request->getRawUri(), $request->getUriAsIs()]; $privateKey = $event->getAccessControl()->getPrivateKey($request->getPublicKey()); // append uris with [] expanded or [0] reduced $uris[] = $this->getUnescapedAlternativeURL($request->getRawUri()); $uris[] = $this->getEscapedAlternativeURL($request->getRawUri()); // See if we should modify the protocol for the incoming request $protocol = $config['authentication']['protocol']; if ($protocol === 'both') { $uris = array_reduce($uris, function ($dest, $uri) { $baseUrl = preg_replace('#^https?#', '', $uri); $dest[] = 'http' . $baseUrl; $dest[] = 'https' . $baseUrl; return $dest; }, []); } else { if (in_array($protocol, ['http', 'https'])) { $uris = array_map(function ($uri) use($protocol) { return preg_replace('#^https?#', $protocol, $uri); }, $uris); } } foreach ($uris as $uri) { // Remove the access token from the query string as it's not used to generate the HMAC $uri = rtrim(preg_replace('/(?<=(\\?|&))accessToken=[^&]+&?/', '', $uri), '&?'); $correctToken = hash_hmac('sha256', $uri, $privateKey); if ($correctToken === $token) { return; } } throw new RuntimeException('Incorrect access token', 400); }
/** * Perform content negotiation by looking the the current URL and the Accept request header * * @param EventInterface $event The event instance */ public function negotiate(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $formatter = null; $extension = $request->getExtension(); $routeName = (string) $request->getRoute(); $config = $event->getConfig(); $contentNegotiateImages = $config['contentNegotiateImages']; $model = $response->getModel(); if (!$extension && !$contentNegotiateImages && $model instanceof Model\Image) { // Configuration is telling us not to use content negotiation for images, // instead we want to use the original format of the image $mime = $model->getMimeType(); $formatter = $this->supportedTypes[$mime]; } else { if ($extension && !($model instanceof Model\Error && $routeName === 'image')) { // The user agent wants a specific type. Skip content negotiation completely, but not // if the request is against the image resource, and ended up as an error, because then // Imbo would try to render the error as an image. $mime = $this->defaultMimeType; if (isset($this->extensionsToMimeType[$extension])) { $mime = $this->extensionsToMimeType[$extension]; } $formatter = $this->supportedTypes[$mime]; } else { // Set Vary to Accept since we are doing content negotiation based on Accept $response->setVary('Accept', false); // No extension have been provided $acceptableTypes = array(); foreach (AcceptHeader::fromString($request->headers->get('Accept', '*/*'))->all() as $item) { $acceptableTypes[$item->getValue()] = $item->getQuality(); } $match = false; $maxQ = 0; // Specify which types to check for since all models can't be formatted by all // formatters $modelClass = get_class($model); $modelType = strtolower(substr($modelClass, strrpos($modelClass, '\\') + 1)); $types = $this->defaultModelTypes; if (isset($this->modelTypes[$modelType])) { $types = $this->modelTypes[$modelType]; } // If we are dealing with images we want to make sure the original mime type of the // image is checked first. If the client does not really have any preference with // regards to the mime type (*/* or image/*) this results in the original mime type of // the image being sent. if ($model instanceof Model\Image) { $original = $model->getMimeType(); if ($types[0] !== $original) { $types = array_filter($types, function ($type) use($original) { return $type !== $original; }); array_unshift($types, $original); } } foreach ($types as $mime) { if (($q = $this->contentNegotiation->isAcceptable($mime, $acceptableTypes)) && $q > $maxQ) { $maxQ = $q; $match = true; $formatter = $this->supportedTypes[$mime]; } } if (!$match && !$event->hasArgument('noStrict')) { // No types matched with strict mode enabled. The client does not want any of Imbo's // supported types. Y U NO ACCEPT MY TYPES?! FFFFUUUUUUU! throw new Exception\RuntimeException('Not acceptable', 406); } else { if (!$match) { // There was no match but we don't want to be an ass about it. Send a response // anyway (allowed according to RFC2616, section 10.4.7) $formatter = $this->supportedTypes[$this->defaultMimeType]; } } } } $this->formatter = $formatter; }
/** * {@inheritdoc} */ public function authenticate(EventInterface $event) { $response = $event->getResponse(); $request = $event->getRequest(); $config = $event->getConfig(); // Whether or not the authentication info is in the request headers $fromHeaders = $request->headers->has('x-imbo-authenticate-timestamp') && $request->headers->has('x-imbo-authenticate-signature'); // Fetch timestamp header, fallback to query param $timestamp = $request->headers->get('x-imbo-authenticate-timestamp', $request->query->get('timestamp')); if (!$timestamp) { $exception = new RuntimeException('Missing authentication timestamp', 400); $exception->setImboErrorCode(Exception::AUTH_MISSING_PARAM); } else { if (!$this->timestampIsValid($timestamp)) { $exception = new RuntimeException('Invalid timestamp: ' . $timestamp, 400); $exception->setImboErrorCode(Exception::AUTH_INVALID_TIMESTAMP); } else { if ($this->timestampHasExpired($timestamp)) { $exception = new RuntimeException('Timestamp has expired: ' . $timestamp, 400); $exception->setImboErrorCode(Exception::AUTH_TIMESTAMP_EXPIRED); } } } if (isset($exception)) { throw $exception; } // Fetch signature header, fallback to query param $signature = $request->headers->get('x-imbo-authenticate-signature', $request->query->get('signature')); if (!$signature) { $exception = new RuntimeException('Missing authentication signature', 400); $exception->setImboErrorCode(Exception::AUTH_MISSING_PARAM); } if (isset($exception)) { throw $exception; } $publicKey = $request->getPublicKey(); $privateKey = $event->getAccessControl()->getPrivateKey($publicKey); $url = $request->getRawUri(); if (!$fromHeaders) { // Remove the signature and timestamp from the query parameters as they are not used // when generating the HMAC $url = rtrim(preg_replace('/(?<=(\\?|&))(signature|timestamp)=[^&]+&?/', '', $url), '&?'); } // See if we should modify the protocol for the incoming request $uris = [$url]; $protocol = $config['authentication']['protocol']; if ($protocol === 'both') { $uris = [preg_replace('#^https?#', 'http', $url), preg_replace('#^https?#', 'https', $url)]; } else { if (in_array($protocol, ['http', 'https'])) { $uris = [preg_replace('#^https?#', $protocol, $url)]; } } // Add the URL used for auth to the response headers $response->headers->set('X-Imbo-AuthUrl', implode(', ', $uris)); foreach ($uris as $uri) { if ($this->signatureIsValid($request->getMethod(), $uri, $publicKey, $privateKey, $timestamp, $signature)) { return; } } $exception = new RuntimeException('Signature mismatch', 400); $exception->setImboErrorCode(Exception::AUTH_SIGNATURE_MISMATCH); throw $exception; }