/** * 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); }
/** * 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; }
/** * Add access rules for the specified public key * * @param EventInterface $event The current event */ public function addRules(EventInterface $event) { $accessControl = $event->getAccessControl(); if (!$accessControl instanceof MutableAdapterInterface) { throw new ResourceException('Access control adapter is immutable', 405); } $request = $event->getRequest(); $publicKey = $request->getRoute()->get('publickey'); $data = json_decode($request->getContent(), true); if (!is_array($data)) { throw new InvalidArgumentException('No access rule data provided', 400); } // If a single rule was provided, wrap it in an array if (!count($data) || !isset($data[0])) { $data = [$data]; } $accessControl = $event->getAccessControl(); // Perform rule validation foreach ($data as $rule) { $this->validateRule($event, $rule); } // Insert the rules foreach ($data as $rule) { $accessControl->addAccessRule($publicKey, $rule); } }
/** * 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); }
/** * 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(); }
/** * Update the image model blob before storing it in case an event listener has changed the * image * * @param EventInterface $event The event instance */ public function updateModelBeforeStoring(EventInterface $event) { $image = $event->getRequest()->getImage(); if ($image->hasBeenTransformed()) { $image->setBlob($this->imagick->getImageBlob()); } }
/** * 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]); }
/** * 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)); }
/** * {@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))); } }
/** * 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); } }
/** * 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); }
/** * {@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'); }
/** * {@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); }
/** * {@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()) . '"'); } }
/** * 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'); }
/** * Insert an image * * @param EventInterface $event An event instance */ public function insertImage(EventInterface $event) { $request = $event->getRequest(); $user = $request->getUser(); $image = $request->getImage(); $imageIdentifier = $image->getImageIdentifier(); $blob = $image->getBlob(); try { $exists = $event->getStorage()->imageExists($user, $imageIdentifier); $event->getStorage()->store($user, $imageIdentifier, $blob); } catch (StorageException $e) { $event->getDatabase()->deleteImage($user, $imageIdentifier); throw $e; } $event->getResponse()->setStatusCode($exists ? 200 : 201); }
/** * Delete a single short URL * * @param EventInterface $event */ public function deleteShortUrl(EventInterface $event) { $database = $event->getDatabase(); $request = $event->getRequest(); $publicKey = $request->getPublicKey(); $imageIdentifier = $request->getImageIdentifier(); $shortUrlId = $request->getRoute()->get('shortUrlId'); if (!($params = $database->getShortUrlParams($shortUrlId))) { throw new ResourceException('ShortURL not found', 404); } if ($params['publicKey'] !== $publicKey || $params['imageIdentifier'] !== $imageIdentifier) { throw new ResourceException('ShortURL not found', 404); } $database->deleteShortUrls($publicKey, $imageIdentifier, $shortUrlId); $model = new ArrayModel(); $model->setData(array('id' => $shortUrlId)); $event->getResponse()->setModel($model); }
/** * Delete the specified access control rule * * @param EventInterface $event The current event */ public function deleteRule(EventInterface $event) { $acl = $event->getAccessControl(); if (!$acl instanceof MutableAdapterInterface) { throw new ResourceException('Access control adapter is immutable', 405); } $request = $event->getRequest(); $publicKey = $request->getRoute()->get('publickey'); $accessRuleId = $request->getRoute()->get('accessRuleId'); $keyExists = $acl->publicKeyExists($publicKey); if (!$keyExists) { throw new RuntimeException('Public key not found', 404); } $accessRule = $acl->getAccessRule($publicKey, $accessRuleId); if (!$accessRule) { throw new RuntimeException('Access rule not found', 404); } $acl->deleteAccessRule($publicKey, $accessRuleId); }
/** * 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'); }
/** * 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); } }
/** * Delete data from the cache * * @param EventInterface $event The event instance */ public function deleteFromCache(EventInterface $event) { $request = $event->getRequest(); $cacheKey = $this->getCacheKey($request->getPublicKey(), $request->getImageIdentifier()); $this->cache->delete($cacheKey); }
/** * Load user data * * @param EventInterface $event An event instance */ public function loadUser(EventInterface $event) { $request = $event->getRequest(); $response = $event->getResponse(); $publicKey = $request->getPublicKey(); $database = $event->getDatabase(); $numImages = $database->getNumImages($publicKey); $lastModified = $database->getLastModified($publicKey); $userModel = new Model\User(); $userModel->setPublicKey($publicKey)->setNumImages($numImages)->setLastModified($lastModified); $response->setModel($userModel)->setLastModified($lastModified); }
/** * Delete all image variations attached to an image * * If any of the delete operations fail Imbo will trigger an error * * @param EventInterface $event The current event */ public function deleteVariations(EventInterface $event) { $request = $event->getRequest(); $publicKey = $request->getPublicKey(); $imageIdentifier = $request->getImageIdentifier(); try { $this->database->deleteImageVariations($publicKey, $imageIdentifier); } catch (DatabaseException $e) { trigger_error(sprintf('Could not delete image variation metadata for %s (%s)', $publicKey, $imageIdentifier), E_USER_WARNING); } try { $this->storage->deleteImageVariations($publicKey, $imageIdentifier); } catch (StorageException $e) { trigger_error(sprintf('Could not delete image variations from storage for %s (%s)', $publicKey, $imageIdentifier), E_USER_WARNING); } }
/** * Delete a resource group * * @param EventInterface $event The current event */ public function deleteGroup(EventInterface $event) { $accessControl = $event->getAccessControl(); if (!$accessControl instanceof MutableAdapterInterface) { throw new ResourceException('Access control adapter is immutable', 405); } $route = $event->getRequest()->getRoute(); $groupName = $route->get('group'); $group = $accessControl->getGroup($groupName); if (!$group) { throw new ResourceException('Resource group not found', 404); } $accessControl->deleteResourceGroup($groupName); }
/** * 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); }