diff --git a/lib/Alchemy/Phrasea/Controller/Root/DeveloperController.php b/lib/Alchemy/Phrasea/Controller/Root/DeveloperController.php index e46bb5bf4a..e39c8f8cd5 100644 --- a/lib/Alchemy/Phrasea/Controller/Root/DeveloperController.php +++ b/lib/Alchemy/Phrasea/Controller/Root/DeveloperController.php @@ -19,6 +19,7 @@ use Alchemy\Phrasea\Model\Manipulator\ApiOauthTokenManipulator; use Alchemy\Phrasea\Model\Repositories\ApiAccountRepository; use Alchemy\Phrasea\Model\Repositories\ApiApplicationRepository; use Alchemy\Phrasea\Model\Repositories\ApiOauthTokenRepository; +use Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -244,8 +245,12 @@ class DeveloperController extends Controller throw new AccessDeniedHttpException(); } + $deliveries = $this->getWebhookDeliveryRepository() + ->findLastDeliveries($account->getApplication(), 10); + return $this->render('developers/application.html.twig', [ "application" => $application, + "deliveries" => $deliveries, "user" => $user, "token" => $token, ]); @@ -298,4 +303,12 @@ class DeveloperController extends Controller { return $this->app['repo.api-applications']; } + + /** + * @return WebhookEventDeliveryRepository + */ + private function getWebhookDeliveryRepository() + { + return $this->app['webhook.delivery_repository']; + } } diff --git a/lib/Alchemy/Phrasea/Core/Provider/WebhookServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/WebhookServiceProvider.php index def052f1d6..4b280c92f0 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/WebhookServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/WebhookServiceProvider.php @@ -21,6 +21,10 @@ class WebhookServiceProvider implements ServiceProviderInterface $this->createAlias($app, 'webhook.delivery_repository', 'repo.webhook-delivery'); $this->createAlias($app, 'webhook.delivery_manipulator', 'manipulator.webhook-delivery'); + $app['webhook.delivery_payload_repository'] = $app->share(function ($app) { + return $app['orm.em']->getRepository('Phraseanet:WebhookEventPayload'); + }); + $app['webhook.processor_factory'] = $app->share(function ($app) { return new EventProcessorFactory($app); }); @@ -32,7 +36,8 @@ class WebhookServiceProvider implements ServiceProviderInterface $app['webhook.event_repository'], $app['webhook.event_manipulator'], $app['webhook.delivery_repository'], - $app['webhook.delivery_manipulator'] + $app['webhook.delivery_manipulator'], + $app['webhook.delivery_payload_repository'] ); }); diff --git a/lib/Alchemy/Phrasea/Model/Entities/WebhookEventDelivery.php b/lib/Alchemy/Phrasea/Model/Entities/WebhookEventDelivery.php index 9e63da730e..8e871a5d46 100644 --- a/lib/Alchemy/Phrasea/Model/Entities/WebhookEventDelivery.php +++ b/lib/Alchemy/Phrasea/Model/Entities/WebhookEventDelivery.php @@ -56,6 +56,11 @@ class WebhookEventDelivery */ private $created; + /** + * @ORM\OneToOne(targetEntity="WebhookEventPayload", mappedBy="delivery") + */ + private $payload; + /** * @param \DateTime $created * @@ -163,4 +168,12 @@ class WebhookEventDelivery { return $this->event; } + + /** + * @return WebhookEventPayload + */ + public function getPayload() + { + return $this->payload; + } } diff --git a/lib/Alchemy/Phrasea/Model/Entities/WebhookEventPayload.php b/lib/Alchemy/Phrasea/Model/Entities/WebhookEventPayload.php new file mode 100644 index 0000000000..1befac1843 --- /dev/null +++ b/lib/Alchemy/Phrasea/Model/Entities/WebhookEventPayload.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Alchemy\Phrasea\Model\Entities; + +use Doctrine\ORM\Mapping as ORM; +use Ramsey\Uuid\Uuid; + +/** + * @ORM\Table(name="WebhookEventPayloads") + * @ORM\Entity(repositoryClass="Alchemy\Phrasea\Model\Repositories\WebhookEventPayloadRepository") + */ +class WebhookEventPayload +{ + /** + * @ORM\Column(type="guid") + * @ORM\Id + * @ORM\GeneratedValue(strategy="NONE") + * + * @var string + */ + private $id; + + /** + * @ORM\OneToOne(targetEntity="WebhookEventDelivery") + * @ORM\JoinColumn(name="delivery_id", referencedColumnName="id") + */ + private $delivery; + + /** + * @ORM\Column(type="text", name="request") + * @var string + */ + private $requestPayload; + + /** + * @ORM\Column(type="text", name="response") + * @var string + */ + private $responsePayload; + + /** + * @ORM\Column(type="integer", name="status") + * @var int + */ + private $statusCode; + + /** + * @ORM\Column(type="text") + * @var string + */ + private $headers; + + /** + * @param WebhookEventDelivery $eventDelivery + * @param string $requestPayload + * @param string $responsePayload + * @param int $statusCode + * @param string $headers + */ + public function __construct(WebhookEventDelivery $eventDelivery, $requestPayload, $responsePayload, $statusCode, $headers) + { + $this->id = Uuid::uuid4()->toString(); + + $this->delivery = $eventDelivery; + $this->requestPayload = $requestPayload; + $this->responsePayload = $responsePayload; + $this->statusCode = $statusCode; + $this->headers = $headers; + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return WebhookEventDelivery + */ + public function getDelivery() + { + return $this->delivery; + } + + /** + * @return string + */ + public function getRequestPayload() + { + return $this->requestPayload; + } + + /** + * @return string + */ + public function getResponsePayload() + { + return $this->responsePayload; + } + + /** + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * @return string + */ + public function getResponseHeaders() + { + return $this->headers; + } +} diff --git a/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventDeliveryRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventDeliveryRepository.php index e8e965be01..f162a12ff3 100644 --- a/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventDeliveryRepository.php +++ b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventDeliveryRepository.php @@ -11,6 +11,7 @@ namespace Alchemy\Phrasea\Model\Repositories; +use Alchemy\Phrasea\Model\Entities\ApiApplication; use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery; use Doctrine\ORM\EntityRepository; @@ -22,6 +23,10 @@ use Doctrine\ORM\EntityRepository; */ class WebhookEventDeliveryRepository extends EntityRepository { + + /** + * @return WebhookEventDelivery[] + */ public function findUndeliveredEvents() { $qb = $this->createQueryBuilder('e'); @@ -34,4 +39,22 @@ class WebhookEventDeliveryRepository extends EntityRepository return $qb->getQuery()->getResult(); } + + /** + * @param ApiApplication $apiApplication + * @param int $count + * @return WebhookEventDelivery[] + */ + public function findLastDeliveries(ApiApplication $apiApplication, $count = 10) + { + $qb = $this->createQueryBuilder('e'); + + $qb + ->where('e.application = :app') + ->setMaxResults(max(0, (int) $count)) + ->orderBy('e.created', 'DESC') + ->setParameters([ 'app' => $apiApplication ]); + + return $qb->getQuery()->getResult(); + } } diff --git a/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventPayloadRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventPayloadRepository.php new file mode 100644 index 0000000000..1b3c028454 --- /dev/null +++ b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventPayloadRepository.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Alchemy\Phrasea\Model\Repositories; + +use Alchemy\Phrasea\Model\Entities\WebhookEventPayload; +use Doctrine\ORM\EntityRepository; + +class WebhookEventPayloadRepository extends EntityRepository +{ + + public function save(WebhookEventPayload $payload) + { + $this->_em->persist($payload); + $this->_em->persist($payload->getDelivery()); + + $this->_em->flush([ $payload, $payload->getDelivery() ]); + } +} diff --git a/lib/Alchemy/Phrasea/Setup/DoctrineMigrations/Version20161013115559.php b/lib/Alchemy/Phrasea/Setup/DoctrineMigrations/Version20161013115559.php new file mode 100644 index 0000000000..0877e87c90 --- /dev/null +++ b/lib/Alchemy/Phrasea/Setup/DoctrineMigrations/Version20161013115559.php @@ -0,0 +1,40 @@ +tableExists('Orders'); + } + + /** + * @param Schema $schema + */ + public function doUpSql(Schema $schema) + { + // this up() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql("CREATE TABLE WebhookEventPayloads (id CHAR(36) NOT NULL COMMENT '(DC2Type:guid)', delivery_id INT DEFAULT NULL, request LONGTEXT NOT NULL, response LONGTEXT NOT NULL, status INT NOT NULL, headers LONGTEXT NOT NULL, UNIQUE INDEX UNIQ_B949629612136921 (delivery_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;"); + $this->addSql("ALTER TABLE WebhookEventPayloads ADD CONSTRAINT FK_B949629612136921 FOREIGN KEY (delivery_id) REFERENCES WebhookEventDeliveries (id);"); + } + + /** + * @param Schema $schema + */ + public function doDownSql(Schema $schema) + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('DROP TABLE WebhookEventPayloads'); + } +} diff --git a/lib/Alchemy/Phrasea/Webhook/WebhookInvoker.php b/lib/Alchemy/Phrasea/Webhook/WebhookInvoker.php index a1704e861f..3a2935a87a 100644 --- a/lib/Alchemy/Phrasea/Webhook/WebhookInvoker.php +++ b/lib/Alchemy/Phrasea/Webhook/WebhookInvoker.php @@ -14,15 +14,18 @@ namespace Alchemy\Phrasea\Webhook; use Alchemy\Phrasea\Model\Entities\ApiApplication; use Alchemy\Phrasea\Model\Entities\WebhookEvent; use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery; +use Alchemy\Phrasea\Model\Entities\WebhookEventPayload; use Alchemy\Phrasea\Model\Manipulator\WebhookEventDeliveryManipulator; use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator; use Alchemy\Phrasea\Model\Repositories\ApiApplicationRepository; use Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository; +use Alchemy\Phrasea\Model\Repositories\WebhookEventPayloadRepository; use Alchemy\Phrasea\Model\Repositories\WebhookEventRepository; -use Guzzle\Batch\BatchBuilder; use Guzzle\Common\Event; use Guzzle\Http\Client; +use Guzzle\Http\Message\EntityEnclosingRequestInterface; use Guzzle\Http\Message\Request; +use Guzzle\Http\Message\Response; use Guzzle\Plugin\Backoff\BackoffPlugin; use Guzzle\Plugin\Backoff\CallbackBackoffStrategy; use Guzzle\Plugin\Backoff\CurlBackoffStrategy; @@ -62,26 +65,35 @@ class WebhookInvoker implements LoggerAwareInterface * @var LoggerInterface */ private $logger; + /** * @var EventProcessorFactory */ private $processorFactory; + /** * @var WebhookEventRepository */ private $eventRepository; + /** * @var WebhookEventManipulator */ private $eventManipulator; + /** + * @var WebhookEventPayloadRepository + */ + private $eventPayloadRepository; + /** * @param ApiApplicationRepository $applicationRepository * @param EventProcessorFactory $processorFactory * @param WebhookEventRepository $eventRepository * @param WebhookEventManipulator $eventManipulator - * @param WebhookEventDeliveryManipulator $eventDeliveryManipulator * @param WebhookEventDeliveryRepository $eventDeliveryRepository + * @param WebhookEventDeliveryManipulator $eventDeliveryManipulator + * @param WebhookEventPayloadRepository $eventPayloadRepository * @param Client $client * * @todo Extract classes to reduce number of required dependencies @@ -93,6 +105,7 @@ class WebhookInvoker implements LoggerAwareInterface WebhookEventManipulator $eventManipulator, WebhookEventDeliveryRepository $eventDeliveryRepository, WebhookEventDeliveryManipulator $eventDeliveryManipulator, + WebhookEventPayloadRepository $eventPayloadRepository, Client $client = null ) { $this->applicationRepository = $applicationRepository; @@ -101,6 +114,8 @@ class WebhookInvoker implements LoggerAwareInterface $this->eventManipulator = $eventManipulator; $this->eventDeliveryManipulator = $eventDeliveryManipulator; $this->eventDeliveryRepository = $eventDeliveryRepository; + $this->eventPayloadRepository = $eventPayloadRepository; + $this->client = $client ?: new Client(); $this->logger = new NullLogger(); @@ -216,24 +231,41 @@ class WebhookInvoker implements LoggerAwareInterface $eventProcessor = $this->processorFactory->getProcessor($event); $data = $eventProcessor->process($event); - // Batch requests - $batch = BatchBuilder::factory() - ->transferRequests(10) - ->build(); - foreach ($targets as $thirdPartyApplication) { $delivery = $this->eventDeliveryManipulator->create($thirdPartyApplication, $event); - - // append delivery id as url anchor + // Append delivery id as url anchor $uniqueUrl = $this->buildUrl($thirdPartyApplication, $delivery); - // create http request with data as request body - $batch->add($this->client->createRequest('POST', $uniqueUrl, [ + // Create http request with data as request body + $request = $this->client->createRequest('POST', $uniqueUrl, [ 'Content-Type' => 'application/vnd.phraseanet.event+json' - ], json_encode($data))); - } + ], json_encode($data)); - $batch->flush(); + $requestBody = $request instanceof EntityEnclosingRequestInterface ? $request->getBody() : ''; + + try { + $response = $request->send(); + + $responseBody = $response->getBody(true); + $statusCode = $response->getStatusCode(); + $headers = $this->extractResponseHeaders($response); + } + catch (\Exception $exception) { + $responseBody = $exception->getMessage(); + $statusCode = -1; + $headers = ''; + } + + $deliveryPayload = new WebhookEventPayload( + $delivery, + $requestBody, + $responseBody, + $statusCode, + $headers + ); + + $this->eventPayloadRepository->save($deliveryPayload); + } } /** @@ -245,4 +277,16 @@ class WebhookInvoker implements LoggerAwareInterface { return sprintf('%s#%s', $application->getWebhookUrl(), $delivery->getId()); } + + private function extractResponseHeaders(Response $response) + { + $headerCollection = $response->getHeaders()->toArray(); + $headers = ''; + + foreach ($headerCollection as $name => $value) { + $headers .= sprintf('%s: %s', $name, $value) . PHP_EOL; + } + + return trim($headers); + } } diff --git a/templates/web/developers/application.html.twig b/templates/web/developers/application.html.twig index 35f1e7ff51..4bf851b168 100644 --- a/templates/web/developers/application.html.twig +++ b/templates/web/developers/application.html.twig @@ -19,16 +19,16 @@ {% block content_account %}
-

{{ "Application" | trans }}

+

{{ "Application" | trans }} - {{ application.getName() }}

-
{{ application.getName() }}
{{ application.getDescription() }}
+
+

{{ "settings OAuth" | trans }}

-

{{ "Les parametres oauth de votre application." | trans }}

@@ -121,9 +121,39 @@
+ + {% if deliveries | length > 0 %} +

{{ "Derniers envois" | trans }}

+

{{ "Résultats des derniers envois effectués pour cette application" | trans }}

+ + + + + + + + + + + + + {% for delivery in deliveries %} + + + + + + + + {% endfor %} + +
 IDTypeDateStatus code
{{ delivery.isDelivered ? 'OK' : 'KO' }}{{ delivery.payload ? delivery.payload.id : '-' }}{{ delivery.webhookEvent.type }}{{ delivery.created | date }}{{ delivery.payload ? delivery.payload.statusCode : '-' }}
+ {% endif %} + +
{% endblock %}