/** * 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'); } }
/** * {@inheritdoc} */ public function transform(EventInterface $event) { $image = $event->getArgument('image'); try { // Contrast $this->imagick->contrastImage(1); // Noise $this->imagick->addNoiseImage(Imagick::NOISE_GAUSSIAN, Imagick::CHANNEL_GREEN); // Desaturate + adjust brightness $this->imagick->modulateImage(135, 25, 100); // Adjust color balance $this->imagick->evaluateImage(Imagick::EVALUATE_MULTIPLY, 1.1, Imagick::CHANNEL_RED); $this->imagick->evaluateImage(Imagick::EVALUATE_MULTIPLY, 1.02, Imagick::CHANNEL_BLUE); $this->imagick->evaluateImage(Imagick::EVALUATE_MULTIPLY, 1.1, Imagick::CHANNEL_GREEN); // Gamma $this->imagick->gammaImage(0.87); // Vignette $width = $image->getWidth(); $height = $image->getHeight(); $size = $height > $width ? $width / 6 : $height / 6; $this->imagick->setImageBackgroundColor(new ImagickPixel('black')); $this->imagick->vignetteImage(0, 60, 0 - $size, 0 - $size); // Mark as transformed $image->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * Handle GET requests * * @param EventInterface $event The current event */ public function get(EventInterface $event) { $response = $event->getResponse(); $response->setMaxAge(0)->setPrivate(); $response->headers->addCacheControlDirective('no-store'); $event->getManager()->trigger('db.stats.load'); }
/** * Handle GET requests * * @param EventInterface $event The current event */ public function get(EventInterface $event) { $response = $event->getResponse(); $database = $event->getDatabase(); $storage = $event->getStorage(); $databaseStatus = $database->getStatus(); $storageStatus = $storage->getStatus(); if (!$databaseStatus || !$storageStatus) { if (!$databaseStatus && !$storageStatus) { $message = 'Database and storage error'; } else { if (!$storageStatus) { $message = 'Storage error'; } else { $message = 'Database error'; } } $response->setStatusCode(503, $message); } $response->setMaxAge(0)->setPrivate(); $response->headers->addCacheControlDirective('no-store'); $statusModel = new Model\Status(); $statusModel->setDate(new DateTime('now', new DateTimeZone('UTC')))->setDatabaseStatus($databaseStatus)->setStorageStatus($storageStatus); $response->setModel($statusModel); }
/** * 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; }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { $image = $event->getArgument('image'); $params = $event->getArgument('params'); if (empty($params['width']) && empty($params['height'])) { throw new TransformationException('Missing both width and height. You need to specify at least one of them', 400); } $width = !empty($params['width']) ? (int) $params['width'] : 0; $height = !empty($params['height']) ? (int) $params['height'] : 0; $originalWidth = $image->getWidth(); $originalHeight = $image->getHeight(); if ($width === $originalWidth && $height === $originalHeight) { // Resize params match the current image size, no need for any resizing return; } // Calculate width or height if not both have been specified if (!$height) { $height = $originalHeight / $originalWidth * $width; } else { if (!$width) { $width = $originalWidth / $originalHeight * $height; } } try { $this->imagick->setOption('jpeg:size', $width . 'x' . $height); $this->imagick->thumbnailImage($width, $height); $size = $this->imagick->getImageGeometry(); $image->setWidth($size['width'])->setHeight($size['height'])->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * 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'])); } } }
/** * {@inheritdoc} */ public function checkAccessToken(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $query = $request->query; $eventName = $event->getName(); 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 = array($request->getRawUri(), $request->getUriAsIs()); $privateKeys = $event->getUserLookup()->getPrivateKeys($request->getPublicKey()) ?: []; 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), '&?'); foreach ($privateKeys as $privateKey) { $correctToken = hash_hmac('sha256', $uri, $privateKey); if ($correctToken === $token) { return; } } } throw new RuntimeException('Incorrect access token', 400); }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { $image = $event->getArgument('image'); $params = $event->getArgument('params'); $color = !empty($params['color']) ? $this->formatColor($params['color']) : $this->color; $width = isset($params['width']) ? (int) $params['width'] : $this->width; $height = isset($params['height']) ? (int) $params['height'] : $this->height; $mode = !empty($params['mode']) ? $params['mode'] : $this->mode; try { if ($mode === 'outbound') { // Paint the border outside of the image, increasing the width/height if ($this->imagick->getImageAlphaChannel() !== 0) { // If we have an alpha channel and call `borderImage()`, Imagick will remove // the alpha channel - if we have an alpha channel, use an alternative approach $this->expandImage($color, $width, $height, $image); } else { // If we don't have an alpha channel, use the more cost-efficient `borderImage()` $this->imagick->borderImage($color, $width, $height); } } else { // Paint the border inside of the image, keeping the orignal width/height $this->drawBorderInside($color, $width, $height, $image); } $size = $this->imagick->getImageGeometry(); $image->setWidth($size['width'])->setHeight($size['height'])->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } catch (ImagickPixelException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * Send the response * * @param EventInterface $event The current event */ public function send(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); // Vary on public key header. Public key specified in query and URL path doesn't have to be // taken into consideration, since they will have varying URLs $response->setVary('X-Imbo-PublicKey', false); // Optionally mark this response as not modified $response->isNotModified($request); // Inject a possible image identifier into the response headers $imageIdentifier = null; if ($image = $request->getImage()) { // The request has an image. This means that an image was just added. // Get the image identifier from the image model $imageIdentifier = $image->getImageIdentifier(); } else { if ($identifier = $request->getImageIdentifier()) { // An image identifier exists in the request URI, use that $imageIdentifier = $identifier; } } if ($imageIdentifier) { $response->headers->set('X-Imbo-ImageIdentifier', $imageIdentifier); } $response->send(); }
/** * Prepare an image * * This method should prepare an image object from php://input. The method must also figure out * the width, height, mime type and extension of the image. * * @param EventInterface $event The current event * @throws ImageException */ public function prepareImage(EventInterface $event) { $request = $event->getRequest(); // Fetch image data from input $imageBlob = $request->getContent(); if (empty($imageBlob)) { $e = new ImageException('No image attached', 400); $e->setImboErrorCode(Exception::IMAGE_NO_IMAGE_ATTACHED); throw $e; } // Open the image with imagick to fetch the mime type $imagick = new Imagick(); try { $imagick->readImageBlob($imageBlob); $mime = $imagick->getImageMimeType(); $size = $imagick->getImageGeometry(); } catch (ImagickException $e) { $e = new ImageException('Invalid image', 415); $e->setImboErrorCode(Exception::IMAGE_INVALID_IMAGE); throw $e; } if (!Image::supportedMimeType($mime)) { $e = new ImageException('Unsupported image type: ' . $mime, 415); $e->setImboErrorCode(Exception::IMAGE_UNSUPPORTED_MIMETYPE); throw $e; } // Store relevant information in the image instance and attach it to the request $image = new Image(); $image->setMimeType($mime)->setExtension(Image::getFileExtension($mime))->setBlob($imageBlob)->setWidth($size['width'])->setHeight($size['height'])->setOriginalChecksum(md5($imageBlob)); $request->setImage($image); }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { $image = $event->getArgument('image'); $params = $event->getArgument('params'); $maxWidth = !empty($params['width']) ? (int) $params['width'] : 0; $maxHeight = !empty($params['height']) ? (int) $params['height'] : 0; try { $sourceWidth = $image->getWidth(); $sourceHeight = $image->getHeight(); $width = $maxWidth ?: $sourceWidth; $height = $maxHeight ?: $sourceHeight; // Figure out original ratio $ratio = $sourceWidth / $sourceHeight; // Is the original image larger than the max-parameters? if ($sourceWidth > $width || $sourceHeight > $height) { if ($width / $height > $ratio) { $width = round($height * $ratio); } else { $height = round($width / $ratio); } } else { // Original image is smaller than the max-parameters, don't transform return; } $this->imagick->setOption('jpeg:size', $width . 'x' . $height); $this->imagick->thumbnailImage($width, $height); $size = $this->imagick->getImageGeometry(); $image->setWidth($size['width'])->setHeight($size['height'])->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
public function get(EventInterface $event) { $model = new ListModel(); $model->setContainer('foo'); $model->setEntry('bar'); $model->setList([1, 2, 3]); $event->getResponse()->setModel($model); }
/** * Add the HashTwo header to the response * * @param EventInterface $event The current event */ public function addHeader(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $user = $request->getUser(); $imageIdentifier = $response->getModel()->getImageIdentifier(); $response->headers->set($this->header, ['imbo;image;' . $user . ';' . $imageIdentifier, 'imbo;user;' . $user]); }
/** * Add the HashTwo header to the response * * @param EventInterface $event The current event */ public function addHeader(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $publicKey = $request->getPublicKey(); $imageIdentifier = $response->getModel()->getImageIdentifier(); $response->headers->set($this->header, array('imbo;image;' . $publicKey . ';' . $imageIdentifier, 'imbo;user;' . $publicKey)); }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { try { $this->imagick->setInterlaceScheme(Imagick::INTERLACE_PLANE); $event->getArgument('image')->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * {@inheritdoc} */ public function enforceMaxSize(EventInterface $event) { $image = $event->getRequest()->getImage(); $width = $image->getWidth(); $height = $image->getHeight(); if ($this->width && $width > $this->width || $this->height && $height > $this->height) { $event->getManager()->trigger('image.transformation.maxsize', array('image' => $image, 'params' => array('width' => $this->width, 'height' => $this->height))); } }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { try { $this->imagick->transverseImage(); $event->getArgument('image')->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * Handle other requests * * @param EventInterface $event The event instance */ public function queueRequest(EventInterface $event) { $request = $event->getRequest(); $eventName = $event->getName(); $urls = (array) $this->params[$eventName]; $data = ['event' => $eventName, 'url' => $request->getRawUri(), 'imageIdentifier' => $request->getImageIdentifier(), 'publicKey' => $request->getPublicKey()]; foreach ($urls as $url) { $this->requestQueue[] = $this->getHttpClient()->post($url, null, $data); } }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { $params = $event->getArgument('params'); if (empty($params['level'])) { throw new TransformationException('Missing required parameter: level', 400); } $this->level = (int) $params['level']; if ($this->level < 0 || $this->level > 100) { throw new TransformationException('level must be between 0 and 100', 400); } }
/** * Handle POST requests * * @param EventInterface */ public function addImage(EventInterface $event) { $event->getManager()->trigger('db.image.insert'); $event->getManager()->trigger('storage.image.insert'); $request = $event->getRequest(); $response = $event->getResponse(); $image = $request->getImage(); $model = new Model\ArrayModel(); $model->setData(['imageIdentifier' => $image->getImageIdentifier(), 'width' => $image->getWidth(), 'height' => $image->getHeight(), 'extension' => $image->getExtension()]); $response->setModel($model); }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { $params = $event->getArgument('params'); $threshold = !empty($params['threshold']) ? (double) $params['threshold'] : $this->threshold; try { $this->imagick->sepiaToneImage($threshold); $event->getArgument('image')->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * {@inheritdoc} */ public function checkAccess(EventInterface $event) { $request = $event->getRequest(); $ip = $request->getClientIp(); if ($this->isIPv6($ip)) { $ip = $this->expandIPv6($ip); } $access = $this->isAllowed($ip); if (!$access) { throw new RuntimeException('Access denied', 403); } }
/** * Handle GET requests * * @param EventInterface $event The current event */ public function get(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $response->setStatusCode(200, 'Hell Yeah'); $baseUrl = $request->getSchemeAndHttpHost() . $request->getBaseUrl(); $model = new Model\ArrayModel(); $model->setData(array('version' => Version::VERSION, 'urls' => array('site' => 'http://www.imbo-project.org', 'source' => 'https://github.com/imbo/imbo', 'issues' => 'https://github.com/imbo/imbo/issues', 'docs' => 'http://docs.imbo-project.org'), 'endpoints' => array('status' => $baseUrl . '/status', 'stats' => $baseUrl . '/stats', 'user' => $baseUrl . '/users/{publicKey}', 'images' => $baseUrl . '/users/{publicKey}/images', 'image' => $baseUrl . '/users/{publicKey}/images/{imageIdentifier}', 'globalShortImageUrl' => $baseUrl . '/s/{id}', 'metadata' => $baseUrl . '/users/{publicKey}/images/{imageIdentifier}/metadata', 'shortImageUrls' => $baseUrl . '/users/{publicKey}/images/{imageIdentifier}/shorturls', 'shortImageUrl' => $baseUrl . '/users/{publicKey}/images/{imageIdentifier}/shorturls/{id}'))); $response->setModel($model); // Prevent caching $response->setMaxAge(0)->setPrivate(); $response->headers->addCacheControlDirective('no-store'); }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { $params = $event->getArgument('params'); $brightness = isset($params['b']) ? (int) $params['b'] : 100; $saturation = isset($params['s']) ? (int) $params['s'] : 100; $hue = isset($params['h']) ? (int) $params['h'] : 100; try { $this->imagick->modulateImage($brightness, $saturation, $hue); $event->getArgument('image')->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * {@inheritdoc} */ public function transform(EventInterface $event) { try { $this->imagick->modulateImage(100, 68, 101); $this->imagick->gammaImage(1.19); $range = $this->imagick->getQuantumRange()['quantumRangeLong']; $blackPoint = 0 - round(27 / 255 * $range); $this->imagick->levelImage(0, 1, $range, Imagick::CHANNEL_RED); $this->imagick->levelImage($blackPoint, 1, $range, Imagick::CHANNEL_RED); $event->getArgument('image')->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * {@inheritdoc} */ public function authenticate(EventInterface $event) { $response = $event->getResponse(); $request = $event->getRequest(); // 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(); $privateKeys = $event->getUserLookup()->getPrivateKeys($publicKey, UserLookupInterface::MODE_READ_WRITE) ?: []; $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), '&?'); } // Add the URL used for auth to the response headers $response->headers->set('X-Imbo-AuthUrl', $url); if (!$this->signatureIsValid($request->getMethod(), $url, $publicKey, $privateKeys, $timestamp, $signature)) { $exception = new RuntimeException('Signature mismatch', 400); $exception->setImboErrorCode(Exception::AUTH_SIGNATURE_MISMATCH); throw $exception; } }
/** * Set the correct ETag for the response * * @param EventInterface $event The current event */ public function setETag(EventInterface $event) { $response = $event->getResponse(); $request = $event->getRequest(); $routesWithETags = ['user' => true, 'images' => true, 'image' => true, 'metadata' => true, 'globalshorturl' => true]; $currentRoute = (string) $request->getRoute(); if (!isset($routesWithETags[$currentRoute])) { // The current route does not use ETags return; } if ($response->isOk()) { $response->setETag('"' . md5($response->getContent()) . '"'); } }
/** * Transform the image * * @param EventInterface $event The event instance */ public function transform(EventInterface $event) { $params = $event->getArgument('params'); $preset = isset($params['preset']) ? $params['preset'] : null; switch ($preset) { case 'moderate': $radius = 2; $sigma = 1; $gain = 2; $threshold = 0.05; break; case 'strong': $radius = 2; $sigma = 1; $gain = 3; $threshold = 0.025; break; case 'extreme': $radius = 2; $sigma = 1; $gain = 4; $threshold = 0; break; case 'light': default: // Default values (with only adding ?t[]=sharpen) $radius = 2; $sigma = 1; $gain = 1; $threshold = 0.05; } if (isset($params['radius'])) { $radius = (double) $params['radius']; } if (isset($params['sigma'])) { $sigma = (double) $params['sigma']; } if (isset($params['gain'])) { $gain = (double) $params['gain']; } if (isset($params['threshold'])) { $threshold = (double) $params['threshold']; } try { $this->imagick->unsharpMaskImage($radius, $sigma, $gain, $threshold); $event->getArgument('image')->hasBeenTransformed(true); } catch (ImagickException $e) { throw new TransformationException($e->getMessage(), 400, $e); } }
/** * Fetch an image via a short URL * * @param EventInterface $event */ public function getImage(EventInterface $event) { $request = $event->getRequest(); $route = $request->getRoute(); $params = $event->getDatabase()->getShortUrlParams($route->get('shortUrlId')); if (!$params) { throw new ResourceException('Image not found', 404); } $route->set('publicKey', $params['publicKey']); $route->set('imageIdentifier', $params['imageIdentifier']); $route->set('extension', $params['extension']); $request->query = new ParameterBag($params['query']); $event->getResponse()->headers->set('X-Imbo-ShortUrl', $request->getUri()); $event->getManager()->trigger('image.get'); }