| 
<?phpnamespace ParagonIE\Chronicle\Handlers;
 
 use GuzzleHttp\Exception\GuzzleException;
 use ParagonIE\Chronicle\{
 Chronicle,
 Exception\BaseException,
 Exception\ChainAppendException,
 Exception\ClientNotFound,
 Exception\FilesystemException,
 Exception\SecurityViolation,
 Exception\TargetNotFound,
 HandlerInterface,
 Scheduled
 };
 use ParagonIE\Sapient\CryptographyKeys\SigningPublicKey;
 use ParagonIE\Sapient\Exception\InvalidMessageException;
 use ParagonIE\Sapient\Sapient;
 use Psr\Http\Message\{
 RequestInterface,
 ResponseInterface
 };
 use Slim\Http\Request;
 
 /**
 * Class Publish
 * @package ParagonIE\Chronicle\Handlers
 */
 class Publish implements HandlerInterface
 {
 /**
 * 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 BaseException
 * @throws ChainAppendException
 * @throws ClientNotFound
 * @throws FilesystemException
 * @throws InvalidMessageException
 * @throws SecurityViolation
 * @throws TargetNotFound
 * @throws GuzzleException
 * @throws \SodiumException
 */
 public function __invoke(
 RequestInterface $request,
 ResponseInterface $response,
 array $args = []
 ): ResponseInterface {
 // Sanity checks
 if ($request instanceof Request) {
 if (!$request->getAttribute('authenticated')) {
 return Chronicle::errorResponse(
 $response,
 'Access denied',
 403
 );
 }
 } else {
 return Chronicle::errorResponse(
 $response,
 'Something unexpected happen when attempting to publish.',
 500
 );
 }
 
 try {
 Chronicle::validateTimestamps($request, 'now');
 } catch (SecurityViolation $ex) {
 // Invalid timestamp. Possibly a replay attack.
 return Chronicle::errorResponse(
 $response,
 $ex->getMessage(),
 $ex->getCode()
 );
 } catch (\Throwable $ex) {
 // We aren't concerned with non-security failures here.
 }
 
 // Get the public key and signature; store this information:
 /**
 * @var SigningPublicKey $publicKey
 * @var string $signature
 */
 list($publicKey, $signature) = $this->getHeaderData($request);
 
 $requestBody = (string) $request->getBody();
 $this->throwIfUnsafe($requestBody);
 
 $result = Chronicle::extendBlakechain(
 $requestBody,
 $signature,
 $publicKey
 );
 
 // If we need to do a cross-sign, do it now:
 (new Scheduled())->doCrossSigns();
 
 try {
 $now = (new \DateTime())->format(\DateTime::ATOM);
 } catch (\Exception $ex) {
 return Chronicle::errorResponse($response, $ex->getMessage(), 500);
 }
 
 return Chronicle::getSapient()->createSignedJsonResponse(
 200,
 [
 'version' => Chronicle::VERSION,
 'datetime' => $now,
 'status' => 'OK',
 'results' => $result
 ],
 Chronicle::getSigningKey(),
 $response->getHeaders(),
 $response->getProtocolVersion()
 );
 }
 
 /**
 * Get the SigningPublicKey and signature for this message.
 *
 * @param RequestInterface $request
 * @return array
 *
 * @throws BaseException
 * @throws SecurityViolation
 * @throws ClientNotFound
 */
 public function getHeaderData(RequestInterface $request): array
 {
 $clientHeader = $request->getHeader(Chronicle::CLIENT_IDENTIFIER_HEADER);
 if (!$clientHeader) {
 throw new SecurityViolation('No client header provided');
 }
 $signatureHeader = $request->getHeader(Sapient::HEADER_SIGNATURE_NAME);
 if (!$signatureHeader) {
 throw new SecurityViolation('No signature provided');
 }
 
 if (\count($signatureHeader) === 1 && count($clientHeader) === 1) {
 return [
 Chronicle::getClientsPublicKey(\array_shift($clientHeader)),
 \array_shift($signatureHeader)
 ];
 }
 throw new ClientNotFound('Could not find the correct client');
 }
 
 /**
 * @param string $data
 * @return void
 *
 * @throws ChainAppendException
 */
 public function throwIfUnsafe(string $data)
 {
 $encoded = json_encode(['data' => $data]);
 if (!\is_string($encoded)) {
 throw new ChainAppendException(
 'Stored data cannot safely be returned in our JSON API: ' .
 \json_last_error_msg()
 );
 }
 }
 }
 
 |