PHP Classes

File: src/Chronicle/Handlers/Lookup.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   Chronicle   src/Chronicle/Handlers/Lookup.php   Download  
File: src/Chronicle/Handlers/Lookup.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Chronicle
Append arbitrary data to a storage container
Author: By
Last change: Optional feature: Cache responses for a very short period of time.

This will allow us to serve a lot more requests per second.
Add pagination to replica
Paginate /export
Add pagination to /since
Extend lasthash output
Proper whitespace
Docblocks
Concurrent Chronicles

Add support for multiple instances via the ?instance=name parameter.

To implement, add something like this to your local/settings.json in the
instances key:

"public_prefix" => "table_name_prefix"

Then run bin/make-tables.php as normal.

Every instance is totally independent of each other. They have their own

* Clients
* Chain data
* Cross-Signing Targets and Policies
* Replications

If merged, I will document these features and roll it into v1.1.0
Docblock consistency, fix composer internal server
Type safety
Upgrade composer dependencies
Date: 1 year ago
Size: 11,622 bytes
 

Contents

Class file image Download
<?php namespace ParagonIE\Chronicle\Handlers; use ParagonIE\Chronicle\{ Chronicle, Exception\FilesystemException, Exception\HashNotFound, Exception\InvalidInstanceException, HandlerInterface, Pagination }; use Psr\Http\Message\{ RequestInterface, ResponseInterface }; /** * Class Lookup * @package ParagonIE\Chronicle\Handlers */ class Lookup implements HandlerInterface { use Pagination; /** @var string */ protected $method = 'index'; /** * Lookup constructor. * @param string $method */ public function __construct(string $method = 'index') { $this->method = $method; } /** * The handler gets invoked by the router. This accepts a Request * and returns a Response. * * @param RequestInterface $request * @param ResponseInterface $response * @param array $args * @return ResponseInterface * * @throws FilesystemException * @throws \Psr\Cache\InvalidArgumentException */ public function __invoke( RequestInterface $request, ResponseInterface $response, array $args = [] ): ResponseInterface { $cache = Chronicle::getFromCache($request); if (!is_null($cache)) { return $cache; } try { // Whitelist of acceptable methods: switch ($this->method) { case 'export': return Chronicle::cache($request, $this->exportChain($args)); case 'lasthash': return Chronicle::cache($request, $this->getLastHash()); case 'hash': if (!empty($args['hash'])) { return Chronicle::cache($request, $this->getByHash($args)); } break; case 'since': if (!empty($args['hash'])) { return Chronicle::cache($request, $this->getSince($args)); } break; } } catch (\Throwable $ex) { return Chronicle::errorResponse($response, $ex->getMessage()); } return Chronicle::errorResponse($response, 'Unknown method: '.$this->method); } /** * Gets the entire Blakechain. * * @param array $args * @return ResponseInterface * * @throws \Exception * @return ResponseInterface * @throws FilesystemException * @throws InvalidInstanceException */ public function exportChain(array $args = []): ResponseInterface { /** @var bool $paginated */ $paginated = Chronicle::shouldPaginate(); /** @var int $total */ $total = 0; /** @var int $offset */ $offset = 0; /** @var int $limit */ $limit = 0; $response = [ 'version' => Chronicle::VERSION, 'datetime' => (new \DateTime())->format(\DateTime::ATOM), 'status' => 'OK', ]; if ($paginated) { $response['paginated'] = true; $total = (int) Chronicle::getDatabase()->cell( "SELECT count(id) FROM " . Chronicle::getTableName('chain') . " " ); /** @var int $offset */ $offset = (int) $this->getOffset((string) ($args['page'] ?? '')); /** @var int $limit */ $limit = Chronicle::getPageSize(); $page = (int) ($args['page'] ?? 1); if ($page > 1) { $response['prev'] = '/export/' . ($page - 1); } if ($offset + $limit <= $total) { if ($page < 1) { $page = 1; } $response['next'] = '/export/' . ($page + 1); } $response['results'] = $this->getPartialChain($offset, $limit); } else { $fullChain = $this->getFullChain(); $response['total'] = count($fullChain); $response['results'] = $fullChain; } return Chronicle::getSapient()->createSignedJsonResponse( 200, $response, Chronicle::getSigningKey() ); } /** * Get information about a particular entry, given its hash. * * @param array $args * @return ResponseInterface * * @throws \Exception * @throws FilesystemException * @throws HashNotFound */ public function getByHash(array $args = []): ResponseInterface { /** @var array<int, array<string, string>> $record */ $record = Chronicle::getDatabase()->run( "SELECT data AS contents, prevhash, currhash, summaryhash, created, publickey, signature FROM " . Chronicle::getTableName('chain') . " WHERE currhash = ? OR summaryhash = ? ", $args['hash'], $args['hash'] ); if (!$record) { throw new HashNotFound('No record found matching this hash.'); } return Chronicle::getSapient()->createSignedJsonResponse( 200, [ 'version' => Chronicle::VERSION, 'datetime' => (new \DateTime())->format(\DateTime::ATOM), 'status' => 'OK', 'results' => $record ], Chronicle::getSigningKey() ); } /** * List the latest current record * * @return ResponseInterface * * @throws FilesystemException * @throws InvalidInstanceException */ public function getLastHash(): ResponseInterface { /** @var array<string, string> $record */ $record = Chronicle::getDatabase()->run( "SELECT data AS contents, prevhash, currhash, summaryhash, created, publickey, signature FROM " . Chronicle::getTableName('chain') . " ORDER BY id DESC LIMIT 1 " ); return Chronicle::getSapient()->createSignedJsonResponse( 200, [ 'version' => Chronicle::VERSION, 'datetime' => (new \DateTime())->format(\DateTime::ATOM), 'status' => 'OK', 'results' => $record, ], Chronicle::getSigningKey() ); } /** * Get updates to the chain since a given hash * * @param array $args * @return ResponseInterface * * @throws \Exception * @throws FilesystemException * @throws HashNotFound * @throws InvalidInstanceException */ public function getSince(array $args = []): ResponseInterface { /** @var bool $paginated */ $paginated = Chronicle::shouldPaginate(); /** @var int $total */ $total = 0; /** @var int $offset */ $offset = 0; /** @var int $limit */ $limit = 0; /** @var int $id */ $id = Chronicle::getDatabase()->cell( "SELECT id FROM " . Chronicle::getTableName('chain') . " WHERE currhash = ? OR summaryhash = ? ORDER BY id ASC ", $args['hash'], $args['hash'] ); if (!$id) { throw new HashNotFound('No record found matching this hash.'); } /** @var string $sinceQuery */ $sinceQuery = "SELECT data AS contents, prevhash, currhash, summaryhash, created, publickey, signature FROM " . Chronicle::getTableName('chain') . " WHERE id > ?"; // Append an offset and limit to the query string if applicable if ($paginated) { $total = (int) Chronicle::getDatabase()->cell( "SELECT count(id) FROM " . Chronicle::getTableName('chain') . " WHERE id > ?", $id ); /** @var int $offset */ $offset = (int) $this->getOffset((string) ($args['page'] ?? '')); /** @var int $limit */ $limit = Chronicle::getPageSize(); $sinceQuery .= $this->formatOffsetSuffix($offset, $limit); } // Fetch the results /** @var array<int, array<string, string>> $since */ $since = Chronicle::getDatabase()->run($sinceQuery, $id); if (!$total) { $total = count($since); } // Process the response $response = [ 'version' => Chronicle::VERSION, 'datetime' => (new \DateTime())->format(\DateTime::ATOM), 'status' => 'OK' ]; // Add total and optional 'next' URL if ($paginated) { $response['paginated'] = true; $page = (int) ($args['page'] ?? 1); if ($page > 1) { $response['prev'] = '/since/' . (string)($args['hash']) . '/' . ($page - 1); } if ($offset + $limit <= $total) { if ($page < 1) { $page = 1; } $response['next'] = '/since/' . (string) ($args['hash']) . '/' . ($page + 1); } $response['total'] = $total; } else { $response['total'] = count($since); } $response['results'] = $since; return Chronicle::getSapient()->createSignedJsonResponse( 200, $response, Chronicle::getSigningKey() ); } /** * Get a subset of the total chain. * * @param int $offset * @param int $limit * @return array * @throws InvalidInstanceException */ protected function getPartialChain(int $offset, int $limit): array { return $this->getChain( "SELECT * FROM " . Chronicle::getTableName('chain') . " ORDER BY id ASC" . $this->formatOffsetSuffix($offset, $limit) ); } /** * Get the entire chain, as-is, as of the time of the request. * * @return array * @throws InvalidInstanceException */ protected function getFullChain(): array { return $this->getChain( "SELECT * FROM " . Chronicle::getTableName('chain') . " ORDER BY id ASC" ); } /** * @param string $queryString * @return array */ protected function getChain(string $queryString): array { $chain = []; /** @var array<int, array<string, string>> $rows */ $rows = Chronicle::getDatabase()->run($queryString); /** @var array<string, string> $row */ foreach ($rows as $row) { $chain[] = [ 'contents' => $row['data'], 'prev' => $row['prevhash'], 'hash' => $row['currhash'], 'summary' => $row['summaryhash'], 'created' => $row['created'], 'publickey' => $row['publickey'], 'signature' => $row['signature'] ]; } return $chain; } }