| 
<?phpnamespace ParagonIE\Chronicle\Handlers;
 
 use GuzzleHttp\Exception\GuzzleException;
 use ParagonIE\Chronicle\{
 Chronicle,
 Exception\AccessDenied,
 Exception\BaseException,
 Exception\FilesystemException,
 Exception\InvalidInstanceException,
 Exception\TargetNotFound,
 HandlerInterface,
 Scheduled
 };
 use ParagonIE\ConstantTime\Base64UrlSafe;
 use ParagonIE\Sapient\Exception\InvalidMessageException;
 use Psr\Http\Message\{
 RequestInterface,
 ResponseInterface
 };
 use Slim\Http\Request;
 
 /**
 * Class Revoke
 * @package ParagonIE\Chronicle\Handlers
 */
 class Revoke 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 AccessDenied
 * @throws BaseException
 * @throws FilesystemException
 * @throws GuzzleException
 * @throws InvalidInstanceException
 * @throws InvalidMessageException
 * @throws TargetNotFound
 * @throws \SodiumException
 */
 public function __invoke(
 RequestInterface $request,
 ResponseInterface $response,
 array $args = []
 ): ResponseInterface {
 // Sanity checks:
 if ($request instanceof Request) {
 if (!$request->getAttribute('authenticated')) {
 throw new AccessDenied('Unauthenticated request');
 }
 if (!$request->getAttribute('administrator')) {
 throw new AccessDenied('Unprivileged request');
 }
 } else {
 throw new \TypeError('Something unexpected happen when attempting to revoke.');
 }
 
 /* Revoking a public key cannot be replayed. */
 try {
 Chronicle::validateTimestamps($request);
 } catch (\Throwable $ex) {
 return Chronicle::errorResponse(
 $response,
 $ex->getMessage(),
 $ex->getCode()
 );
 }
 
 // Get the parsed POST body:
 $post = $request->getParsedBody();
 if (!\is_array($post)) {
 return Chronicle::errorResponse($response, 'POST body empty or invalid', 406);
 }
 if (empty($post['clientid'])) {
 return Chronicle::errorResponse($response, 'Error: Client ID expected', 401);
 }
 if (empty($post['publickey'])) {
 return Chronicle::errorResponse($response, 'Error: Public key expected', 401);
 }
 
 $db = Chronicle::getDatabase();
 $db->beginTransaction();
 
 /** @var bool $found */
 $found = $db->exists(
 'SELECT count(id) FROM ' . Chronicle::getTableName('clients') . ' WHERE publicid = ? AND publickey = ?',
 $post['clientid'],
 $post['publickey']
 );
 if (!$found) {
 return Chronicle::errorResponse(
 $response,
 'Error: Client not found. It may have already been deleted.',
 404
 );
 }
 /** @var bool $isAdmin */
 $isAdmin = $db->cell(
 'SELECT isAdmin FROM ' . Chronicle::getTableName('clients') . ' WHERE publicid = ? AND publickey = ?',
 $post['clientid'],
 $post['publickey']
 );
 if ($isAdmin) {
 return Chronicle::errorResponse(
 $response,
 'You cannot delete administrators from this API.',
 403
 );
 }
 
 $db->delete(
 Chronicle::getTableName('clients', true),
 [
 'publicid' => $post['clientid'],
 'publickey' => $post['publickey'],
 'isAdmin' => false
 ]
 );
 if ($db->commit()) {
 // Confirm deletion:
 $result = [
 'deleted' => !$db->exists(
 'SELECT count(id) FROM ' .
 Chronicle::getTableName('clients') .
 ' WHERE publicid = ? AND publickey = ?',
 $post['clientid'],
 $post['publickey']
 )
 ];
 
 if (!$result['deleted']) {
 $result['reason'] = 'Delete operation was unsuccessful due to unknown reasons.';
 }
 try {
 $now = (new \DateTime())->format(\DateTime::ATOM);
 } catch (\Exception $ex) {
 return Chronicle::errorResponse($response, $ex->getMessage(), 500);
 }
 
 $settings = Chronicle::getSettings();
 if (!empty($settings['publish-revoked-clients'])) {
 $serverKey = Chronicle::getSigningKey();
 $message = \json_encode(
 [
 'server-action' => 'Client Access Revocation',
 'now' => $now,
 'clientid' => $post['clientid'],
 'publickey' => $post['publickey']
 ],
 JSON_PRETTY_PRINT
 );
 if (!\is_string($message)) {
 throw new \TypeError('Invalid messsage');
 }
 $signature = Base64UrlSafe::encode(
 \ParagonIE_Sodium_Compat::crypto_sign_detached(
 $message,
 $serverKey->getString(true)
 )
 );
 $result['revoke'] = Chronicle::extendBlakechain(
 $signature,
 $message,
 $serverKey->getPublicKey()
 );
 
 // If we need to do a cross-sign, do it now:
 (new Scheduled())->doCrossSigns();
 }
 } else {
 /* PDO should have already thrown an exception. */
 $db->rollBack();
 /** @var array<int, string> $errorInfo */
 $errorInfo = $db->errorInfo();
 return Chronicle::errorResponse(
 $response,
 $errorInfo[0],
 500
 );
 }
 
 return Chronicle::getSapient()->createSignedJsonResponse(
 200,
 [
 'version' => Chronicle::VERSION,
 'datetime' => (new \DateTime())->format(\DateTime::ATOM),
 'status' => 'OK',
 'results' => $result
 ],
 Chronicle::getSigningKey(),
 $response->getHeaders(),
 $response->getProtocolVersion()
 );
 }
 }
 
 |