/** * 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 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'])); } } }
/** * 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'); }
/** * {@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))); } }
/** * Load an image * * @param EventInterface $event An event instance */ public function loadImage(EventInterface $event) { $storage = $event->getStorage(); $request = $event->getRequest(); $response = $event->getResponse(); $user = $request->getUser(); $imageIdentifier = $request->getImageIdentifier(); $imageData = $storage->getImage($user, $imageIdentifier); $lastModified = $storage->getLastModified($user, $imageIdentifier); $response->setLastModified($lastModified)->getModel()->setBlob($imageData); $event->getManager()->trigger('image.loaded'); }
/** * Figure out which resources we have available and subscribe to them * * @param EventInterface $event */ public function subscribe(EventInterface $event) { $resources = Resource::getAllResources(); if ($this->params['additionalResources']) { $resources = array_merge($resources, $this->params['additionalResources']); } $events = []; foreach ($resources as $resource) { $events[$resource] = ['checkAccess' => 500]; } $manager = $event->getManager(); $manager->addCallbacks($event->getHandler(), $events); }
/** * 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'); }
/** * Handle GET and HEAD requests * * @param EventInterface $event The current event */ public function getImages(EventInterface $event) { $acl = $event->getAccessControl(); $missingAccess = []; $users = $event->getRequest()->getUsers(); foreach ($users as $user) { $hasAccess = $acl->hasAccess($event->getRequest()->getPublicKey(), 'images.get', $user); if (!$hasAccess) { $missingAccess[] = $user; } } if (!empty($missingAccess)) { throw new RuntimeException('Public key does not have access to the users: [' . implode(', ', $missingAccess) . ']', 400); } $event->getManager()->trigger('db.images.load', ['users' => $users]); }
/** * Subscribe to events based on configuration parameters * * @param EventInterface $event The event instance */ public function subscribe(EventInterface $event) { $events = array(); // Enable the event listener only for resources and methods specified foreach ($this->params['allowedMethods'] as $resource => $methods) { foreach ($methods as $method) { $eventName = $resource . '.' . strtolower($method); $events[$eventName] = array('invoke' => 1000); } // Always enable the listener for the OPTIONS method $eventName = $resource . '.options'; $events[$eventName] = array('options' => 20); } $manager = $event->getManager(); $manager->addCallbacks($event->getHandler(), $events); // Add OPTIONS to the Allow header $event->getResponse()->headers->set('Allow', 'OPTIONS', false); }
/** * Handle GET and HEAD requests * * @param EventInterface */ public function getImage(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $eventManager = $event->getManager(); $publicKey = $request->getPublicKey(); $imageIdentifier = $request->getImageIdentifier(); $image = new Model\Image(); $image->setImageIdentifier($imageIdentifier)->setPublicKey($publicKey); $response->setModel($image); // Load image details from database $eventManager->trigger('db.image.load'); // Set a long max age as the image itself won't change $response->setMaxAge(31536000); // Custom Imbo headers, based on original $response->headers->add(['X-Imbo-OriginalMimeType' => $image->getMimeType(), 'X-Imbo-OriginalWidth' => $image->getWidth(), 'X-Imbo-OriginalHeight' => $image->getHeight(), 'X-Imbo-OriginalFileSize' => $image->getFilesize(), 'X-Imbo-OriginalExtension' => $image->getExtension()]); // Trigger loading of the image $eventManager->trigger('storage.image.load'); // Trigger possible image transformations $eventManager->trigger('image.transform'); }
/** * Generate multiple variations based on the configuration * * If any of the operations fail Imbo will trigger errors * * @param EventInterface $event */ public function generateVariations(EventInterface $event) { // Fetch the event manager to trigger events $eventManager = $event->getManager(); $request = $event->getRequest(); $publicKey = $request->getPublicKey(); $originalImage = $request->getImage(); $imageIdentifier = $originalImage->getChecksum(); $originalWidth = $originalImage->getWidth(); // Fetch parameters specified in the Imbo configuration related to what sort of variations // should be generated $minWidth = $this->params['minWidth']; $maxWidth = $this->params['maxWidth']; $minDiff = $this->params['minDiff']; $scaleFactor = $this->params['scaleFactor']; // Remove widths which are larger than the original image $widths = array_filter($this->params['widths'], function ($value) use($originalWidth) { return $value < $originalWidth; }); if ($this->params['autoScale'] === true) { // Have Imbo figure out the widths to generate in addition to the ones in the "width" // configuration parameter $variationWidth = $previousWidth = $originalWidth; while ($variationWidth > $minWidth) { $variationWidth = round($variationWidth * $scaleFactor); if ($variationWidth > $maxWidth) { // Width too big, try again (twss) continue; } if ($previousWidth - $variationWidth < $minDiff || $variationWidth < $minWidth) { // The diff is too small, or the variation is too small, stop generating more // widths break; } $previousWidth = $variationWidth; $widths[] = $variationWidth; } } foreach ($widths as $width) { // Clone the image so that the resize operation will happen on the original every time $image = clone $originalImage; try { // Trigger a loading of the image, using the clone of the original as an argument $eventManager->trigger('image.loaded', ['image' => $image]); // If configured, use a lossless variation format if ($this->params['lossless'] === true) { $eventManager->trigger('image.transformation.convert', ['image' => $image, 'params' => ['type' => 'png']]); } // Trigger a resize of the image (the transformation handles aspect ratio) $eventManager->trigger('image.transformation.resize', ['image' => $image, 'params' => ['width' => $width]]); // Trigger an update of the model $eventManager->trigger('image.transformed', ['image' => $image]); // Store the image $this->storage->storeImageVariation($publicKey, $imageIdentifier, $image->getBlob(), $width); // Store some data about the variation $this->database->storeImageVariationMetadata($publicKey, $imageIdentifier, $image->getWidth(), $image->getHeight()); } catch (TransformationException $e) { // Could not transform the image trigger_error(sprintf('Could not generate image variation for %s (%s), width: %d', $publicKey, $imageIdentifier, $width), E_USER_WARNING); } catch (StorageException $e) { // Could not store the image trigger_error(sprintf('Could not store image variation for %s (%s), width: %d', $publicKey, $imageIdentifier, $width), E_USER_WARNING); } catch (DatabaseException $e) { // Could not store metadata about the variation trigger_error(sprintf('Could not store image variation metadata for %s (%s), width: %d', $publicKey, $imageIdentifier, $width), E_USER_WARNING); try { $this->storage->deleteImageVariations($publicKey, $imageIdentifier, $width); } catch (StorageException $e) { trigger_error('Could not remove the stored variation', E_USER_WARNING); } } } }
/** * Perform a simple crop/resize operation on the image * * @param EventInterface $event * @param int $width * @param int $height */ private function simpleCrop(EventInterface $event, $width, $height) { $image = $event->getArgument('image'); $sourceRatio = $image->getWidth() / $image->getHeight(); $cropRatio = $width / $height; $params = []; if ($cropRatio > $sourceRatio) { $params['width'] = $width; } else { $params['height'] = $height; } $event->getManager()->trigger('image.transformation.maxsize', ['image' => $event->getArgument('image'), 'params' => $params]); $event->getManager()->trigger('image.transformation.crop', ['image' => $event->getArgument('image'), 'params' => ['width' => $width, 'height' => $height, 'mode' => 'center']]); }
/** * Handle metadata search operation * * page => Page number. Defaults to 1 * limit => Limit to a number of images pr. page. Defaults to 20 * metadata => Whether or not to include metadata pr. image. Set to 1 to enable * query => urlencoded json data to use in the query * from => Unix timestamp to fetch from * to => Unit timestamp to fetch to * * @param Imbo\EventListener\ListenerInterface $event The current event * @param array $users Array with image identifiers */ protected function searchHandler(EventInterface $event, array $users) { $request = $event->getRequest(); $params = $request->query; // Extract query $metadataQuery = $request->getContent(); // If no metadata is provided, we'll let db.images.load take over if (!$metadataQuery) { $event->getManager()->trigger('db.images.load'); return; } // Check access token $event->getManager()->trigger('auth.accesstoken'); // Check that the public key has access to the users $this->validateAccess($event, $users); // Build query params array $queryParams = ['page' => $params->get('page', 1), 'limit' => $params->get('limit', 20), 'from' => $params->get('from'), 'to' => $params->get('to'), 'sort' => $this->getSortParams($event)]; if ($queryParams['page'] < 1) { throw new RuntimeException('Invalid param. "page" must be a positive number.', 400); } if ($queryParams['limit'] < 1) { throw new RuntimeException('Invalid param. "limit" must be a positive number.', 400); } // Parse the query JSON and transform it to an AST $ast = DslParser::parse($metadataQuery); // Query backend using the AST $backendResponse = $this->backend->search($users, $ast, $queryParams); // If we didn't get hits in the search backend, prepare a response if (!$backendResponse->getImageIdentifiers()) { // Create the model and set some pagination values $model = new ImagesModel(); $model->setLimit($queryParams['limit'])->setPage($queryParams['page'])->setHits($backendResponse->getHits()); $response = $event->getResponse(); $response->setModel($model); return; } $imageIdentifiers = $backendResponse->getImageIdentifiers(); // Set the ids to fetch from the Imbo backend $params->set('ids', $imageIdentifiers); // In order to paginate the already paginated resultset, we'll // set the page param to 0 before triggering db.images.load $params->set('page', 0); // Unset date range parameters $params->remove('to'); $params->remove('from'); // Trigger image loading from imbo DB $event->getManager()->trigger('db.images.load'); $responseModel = $event->getResponse()->getModel(); // Set the actual page used for querying search backend on the response $responseModel->setPage($queryParams['page']); $responseModel->setHits($backendResponse->getHits()); // Sort the response image so they match the order of identifiers // returned from search backend $this->sortSearchResponse($responseModel, $imageIdentifiers); }
/** * Get a list of available resource groups * * @param EventInterface $event The current event */ public function listGroups(EventInterface $event) { $event->getManager()->trigger('acl.groups.load'); }
/** * Autorotate images when new images are added to Imbo * * @param EventInterface $event The triggered event */ public function autoRotate(EventInterface $event) { $event->getManager()->trigger('image.transformation.autorotate', ['image' => $event->getRequest()->getImage()]); }
/** * Subscribe to events based on configuration parameters * * @param EventInterface $event The event instance */ public function subscribe(EventInterface $event) { $events = array_fill_keys(array_keys($this->params), 'queueRequest'); $manager = $event->getManager(); $manager->addCallbacks($event->getHandler(), $events); }
/** * Handle GET requests * * @param EventInterface $event The current event */ public function get(EventInterface $event) { $event->getManager()->trigger('db.user.load'); }
/** * Response send hook * * @param EventInterface $event The current event */ public function format(EventInterface $event) { $response = $event->getResponse(); $model = $response->getModel(); if ($response->getStatusCode() === 204 || !$model) { // No content to write return; } $request = $event->getRequest(); // If we are dealing with an image we want to trigger an event that handles a possible // conversion if ($model instanceof Model\Image) { $eventManager = $event->getManager(); if ($this->extensionsToMimeType[$this->formatter] !== $model->getMimeType()) { $eventManager->trigger('image.transformation.convert', array('image' => $model, 'params' => array('type' => $this->formatter))); } // Finished transforming the image $eventManager->trigger('image.transformed', array('image' => $model)); $formattedData = $model->getBlob(); $contentType = $model->getMimeType(); } else { // Create an instance of the formatter $formatter = $this->formatters[$this->formatter]; $formattedData = $formatter->format($model); $contentType = $formatter->getContentType(); } if ($contentType === 'application/json') { foreach (array('callback', 'jsonp', 'json') as $validParam) { if ($request->query->has($validParam)) { $formattedData = sprintf("%s(%s)", $request->query->get($validParam), $formattedData); break; } } } $response->headers->add(array('Content-Type' => $contentType, 'Content-Length' => strlen($formattedData))); if ($request->getMethod() !== 'HEAD') { $response->setContent($formattedData); } }