diff --git a/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php b/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php
index 9a575081a7..ea57fc67ff 100644
--- a/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php
+++ b/lib/Alchemy/Phrasea/Command/Developer/RegenerateSqliteDb.php
@@ -37,6 +37,8 @@ use Alchemy\Phrasea\Model\Entities\UsrList;
use Alchemy\Phrasea\Model\Entities\UsrListEntry;
use Alchemy\Phrasea\Model\Entities\StoryWZ;
use Alchemy\Phrasea\Core\Provider\ORMServiceProvider;
+use Alchemy\Phrasea\Model\Entities\WebhookEvent;
+use Alchemy\Phrasea\Model\Entities\WebhookEventDelivery;
use Alchemy\Phrasea\Model\Manipulator\TokenManipulator;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\SchemaTool;
@@ -109,6 +111,8 @@ class RegenerateSqliteDb extends Command
$this->insertTwoTokens($this->container['EM'], $DI);
$this->insertOneInvalidToken($this->container['EM'], $DI);
$this->insertOneValidationToken($this->container['EM'], $DI);
+ $this->insertWebhookEvent($this->container['EM'], $DI);
+ $this->insertWebhookEventDelivery($this->container['EM'], $DI);
$this->container['EM']->flush();
@@ -121,6 +125,7 @@ class RegenerateSqliteDb extends Command
$fixtures['token']['token_2'] = $DI['token_2']->getValue();
$fixtures['token']['token_invalid'] = $DI['token_invalid']->getValue();
$fixtures['token']['token_validation'] = $DI['token_validation']->getValue();
+
$fixtures['user']['test_phpunit'] = $DI['user']->getId();
$fixtures['user']['test_phpunit_not_admin'] = $DI['user_notAdmin']->getId();
$fixtures['user']['test_phpunit_alt1'] = $DI['user_alt1']->getId();
@@ -133,6 +138,7 @@ class RegenerateSqliteDb extends Command
$fixtures['oauth']['acc-user-not-admin'] = $DI['api-app-acc-user-not-admin']->getId();
$fixtures['databox']['records'] = $DI['databox']->get_sbas_id();
+
$fixtures['collection']['coll'] = $DI['coll']->get_base_id();
$fixtures['collection']['coll_no_access'] = $DI['coll_no_access']->get_base_id();
$fixtures['collection']['coll_no_status'] = $DI['coll_no_status']->get_base_id();
@@ -170,6 +176,8 @@ class RegenerateSqliteDb extends Command
$fixtures['feed']['private']['feed'] = $DI['feed_private']->getId();
$fixtures['feed']['private']['entry'] = $DI['feed_private_entry']->getId();
$fixtures['feed']['private']['token'] = $DI['feed_private_token']->getId();
+
+ $fixtures['webhook']['event'] = $DI['event_webhook_1']->getId();
} catch (\Exception $e) {
$output->writeln("".$e->getMessage()."");
if ($renamed) {
@@ -329,6 +337,45 @@ class RegenerateSqliteDb extends Command
return $this->container['manipulator.user']->createUser($login, uniqid('pass'), $email, $admin);
}
+ protected function insertWebhookEvent(EntityManager $em, \Pimple $DI)
+ {
+ $event = new WebhookEvent();
+ $event->setName(WebhookEvent::NEW_FEED_ENTRY);
+ $event->setType(WebhookEvent::FEED_ENTRY_TYPE);
+ $event->setData(array(
+ 'feed_id' => $DI['feed_public_entry']->getFeed()->getId(),
+ 'entry_id' => $DI['feed_public_entry']->getId()
+ ));
+ $em->persist($event);
+
+ $DI['event_webhook_1'] = $event;
+
+ $event2 = new WebhookEvent();
+ $event2->setName(WebhookEvent::NEW_FEED_ENTRY);
+ $event2->setType(WebhookEvent::FEED_ENTRY_TYPE);
+ $event2->setData(array(
+ 'feed_id' => $DI['feed_public_entry']->getFeed()->getId(),
+ 'entry_id' => $DI['feed_public_entry']->getId()
+ ));
+ $event2->setProcessed(true);
+ $em->persist($event2);
+ }
+
+ protected function insertWebhookEventDelivery(EntityManager $em, \Pimple $DI)
+ {
+ $delivery = new WebhookEventDelivery();
+ $delivery->setThirdPartyApplication($DI['api-app-user']);
+ $delivery->setWebhookEvent($DI['event_webhook_1']);
+ $delivery->setDelivered(true);
+ $em->persist($delivery);
+
+ $delivery2 = new WebhookEventDelivery();
+ $delivery2->setThirdPartyApplication($DI['api-app-user-not-admin']);
+ $delivery2->setWebhookEvent($DI['event_webhook_1']);
+ $delivery2->setDeliverTries(1);
+ $em->persist($delivery2);
+ }
+
private function generateCollection(\Pimple $DI)
{
$coll = $collection_no_acces = $collection_no_acces_by_status = $db = null;
diff --git a/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php
index 9fd059093d..f76aa0bace 100644
--- a/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php
+++ b/lib/Alchemy/Phrasea/Core/Provider/ManipulatorServiceProvider.php
@@ -23,6 +23,8 @@ use Alchemy\Phrasea\Model\Manipulator\RegistrationManipulator;
use Alchemy\Phrasea\Model\Manipulator\TaskManipulator;
use Alchemy\Phrasea\Model\Manipulator\TokenManipulator;
use Alchemy\Phrasea\Model\Manipulator\UserManipulator;
+use Alchemy\Phrasea\Model\Manipulator\WebhookEventDeliveryManipulator;
+use Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator;
use Alchemy\Phrasea\Model\Manager\UserManager;
use Silex\Application as SilexApplication;
use Silex\ServiceProviderInterface;
@@ -82,6 +84,14 @@ class ManipulatorServiceProvider implements ServiceProviderInterface
$app['manipulator.api-log'] = $app->share(function ($app) {
return new ApiLogManipulator($app['EM'], $app['repo.api-logs']);
});
+
+ $app['manipulator.webhook-event'] = $app->share(function ($app) {
+ return new WebhookEventManipulator($app['EM'], $app['repo.webhook-event']);
+ });
+
+ $app['manipulator.webhook-delivery'] = $app->share(function ($app) {
+ return new WebhookEventDeliveryManipulator($app['EM'], $app['repo.webhook-delivery']);
+ });
}
public function boot(SilexApplication $app)
diff --git a/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php
index bceb656bf3..3020c30fcb 100644
--- a/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php
+++ b/lib/Alchemy/Phrasea/Core/Provider/RepositoriesServiceProvider.php
@@ -115,6 +115,12 @@ class RepositoriesServiceProvider implements ServiceProviderInterface
$app['repo.api-oauth-refresh-tokens'] = $app->share(function (PhraseaApplication $app) {
return $app['EM']->getRepository('Phraseanet:ApiOauthRefreshToken');
});
+ $app['repo.webhook-event'] = $app->share(function (PhraseaApplication $app) {
+ return $app['EM']->getRepository('Phraseanet:WebhookEvent');
+ });
+ $app['repo.webhook-delivery'] = $app->share(function (PhraseaApplication $app) {
+ return $app['EM']->getRepository('Phraseanet:WebhookEventDelivery');
+ });
}
public function boot(Application $app)
diff --git a/lib/Alchemy/Phrasea/Core/Provider/TasksServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/TasksServiceProvider.php
index 8562a3aa36..b6decfe294 100644
--- a/lib/Alchemy/Phrasea/Core/Provider/TasksServiceProvider.php
+++ b/lib/Alchemy/Phrasea/Core/Provider/TasksServiceProvider.php
@@ -18,12 +18,14 @@ use Alchemy\Phrasea\TaskManager\Job\FtpPullJob;
use Alchemy\Phrasea\TaskManager\Job\PhraseanetIndexerJob;
use Alchemy\Phrasea\TaskManager\Job\RecordMoverJob;
use Alchemy\Phrasea\TaskManager\Job\SubdefsJob;
+use Alchemy\Phrasea\TaskManager\Job\WebhookJob;
use Alchemy\Phrasea\TaskManager\Job\WriteMetadataJob;
use Alchemy\Phrasea\TaskManager\Job\Factory as JobFactory;
use Alchemy\Phrasea\TaskManager\LiveInformation;
use Alchemy\Phrasea\TaskManager\TaskManagerStatus;
use Alchemy\Phrasea\TaskManager\Log\LogFileFactory;
use Alchemy\Phrasea\TaskManager\Notifier;
+use Alchemy\Phrasea\Webhook\EventProcessorFactory;
use Silex\Application;
use Silex\ServiceProviderInterface;
@@ -76,6 +78,7 @@ class TasksServiceProvider implements ServiceProviderInterface
new RecordMoverJob($app['dispatcher'], $logger, $app['translator']),
new SubdefsJob($app['dispatcher'], $logger, $app['translator']),
new WriteMetadataJob($app['dispatcher'], $logger, $app['translator']),
+ new WebhookJob($app['dispatcher'], $logger, $app['translator']),
];
});
}
diff --git a/lib/Alchemy/Phrasea/Model/Entities/ApiApplication.php b/lib/Alchemy/Phrasea/Model/Entities/ApiApplication.php
index de2795d67b..87904c4224 100644
--- a/lib/Alchemy/Phrasea/Model/Entities/ApiApplication.php
+++ b/lib/Alchemy/Phrasea/Model/Entities/ApiApplication.php
@@ -127,7 +127,7 @@ class ApiApplication
/**
* @var string
*
- * @ORM\Column(name="webhook_url", type="string", length=128)
+ * @ORM\Column(name="webhook_url", type="string", length=128, nullable=true)
*/
private $webhookUrl;
diff --git a/lib/Alchemy/Phrasea/Model/Entities/WebhookEvent.php b/lib/Alchemy/Phrasea/Model/Entities/WebhookEvent.php
new file mode 100644
index 0000000000..5776008506
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Model/Entities/WebhookEvent.php
@@ -0,0 +1,180 @@
+created = $created;
+
+ return $this;
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getCreated()
+ {
+ return $this->created;
+ }
+
+ /**
+ * @return integer
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param array $data
+ *
+ * @return WebhookEvent
+ */
+ public function setData(array $data)
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return WebhookEvent
+ * @throws \InvalidArgumentException
+ */
+ public function setName($name)
+ {
+ if (!in_array($name, self::events())) {
+ throw new \InvalidArgumentException("Invalid event name");
+ }
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param boolean $processed
+ *
+ * @return $this
+ */
+ public function setProcessed($processed)
+ {
+ $this->processed = (Boolean) $processed;
+
+ return $this;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function isProcessed()
+ {
+ return $this->processed;
+ }
+
+ /**
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @param $type
+ *
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setType($type)
+ {
+ if (!in_array($type, self::types())) {
+ throw new \InvalidArgumentException("Invalid event name");
+ }
+ $this->type = $type;
+
+ return $this;
+ }
+}
diff --git a/lib/Alchemy/Phrasea/Model/Entities/WebhookEventDelivery.php b/lib/Alchemy/Phrasea/Model/Entities/WebhookEventDelivery.php
new file mode 100644
index 0000000000..d5c6cbbfdc
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Model/Entities/WebhookEventDelivery.php
@@ -0,0 +1,166 @@
+created = $created;
+
+ return $this;
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getCreated()
+ {
+ return $this->created;
+ }
+
+ /**
+ * @return integer
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param $delivered
+ *
+ * @return $this
+ */
+ public function setDelivered($delivered)
+ {
+ $this->delivered = (Boolean) $delivered;
+
+ return $this;
+ }
+
+ /**
+ * @return Boolean
+ */
+ public function isDelivered()
+ {
+ return $this->delivered;
+ }
+
+ /**
+ * @return integer
+ */
+ public function getDeliveryTries()
+ {
+ return $this->deliveryTries;
+ }
+
+ /**
+ * @param integer $try
+ *
+ * @return $this
+ */
+ public function setDeliverTries($try)
+ {
+ $this->deliveryTries = (int) $try;
+
+ return $this;
+ }
+
+ /**
+ * @return ApiApplication
+ */
+ public function getThirdPartyApplication()
+ {
+ return $this->application;
+ }
+
+ /**
+ * @param ApiApplication $application
+ *
+ * @return $this
+ */
+ public function setThirdPartyApplication(ApiApplication $application)
+ {
+ $this->application = $application;
+
+ return $this;
+ }
+
+ /**
+ * @param WebhookEvent $event
+ *
+ * @return $this
+ */
+ public function setWebhookEvent(WebhookEvent $event)
+ {
+ $this->event = $event;
+
+ return $this;
+ }
+
+ /**
+ * @return WebhookEvent
+ */
+ public function getWebhookEvent()
+ {
+ return $this->event;
+ }
+}
diff --git a/lib/Alchemy/Phrasea/Model/Manipulator/WebhookEventDeliveryManipulator.php b/lib/Alchemy/Phrasea/Model/Manipulator/WebhookEventDeliveryManipulator.php
new file mode 100644
index 0000000000..4a8bef1440
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Model/Manipulator/WebhookEventDeliveryManipulator.php
@@ -0,0 +1,74 @@
+om = $om;
+ $this->repository = $repo;
+ }
+
+ public function create(ApiApplication $application, WebhookEvent $event)
+ {
+ $delivery = new WebhookEventDelivery();
+ $delivery->setThirdPartyApplication($application);
+ $delivery->setWebhookEvent($event);
+
+ $this->update($delivery);
+
+ return $delivery;
+ }
+
+ public function delete(WebhookEventDelivery $delivery)
+ {
+ $this->om->remove($delivery);
+ $this->om->flush();
+ }
+
+ public function update(WebhookEventDelivery $delivery)
+ {
+ $this->om->persist($delivery);
+ $this->om->flush();
+ }
+
+ public function deliverySuccess(WebhookEventDelivery $delivery)
+ {
+ $delivery->setDelivered(true);
+ $delivery->setDeliverTries($delivery->getDeliveryTries() + 1);
+ $this->update($delivery);
+ }
+
+ public function deliveryFailure(WebhookEventDelivery $delivery)
+ {
+ $delivery->setDelivered(false);
+ $delivery->setDeliverTries($delivery->getDeliveryTries() + 1);
+ $this->update($delivery);
+ }
+}
diff --git a/lib/Alchemy/Phrasea/Model/Manipulator/WebhookEventManipulator.php b/lib/Alchemy/Phrasea/Model/Manipulator/WebhookEventManipulator.php
new file mode 100644
index 0000000000..21a7d87011
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Model/Manipulator/WebhookEventManipulator.php
@@ -0,0 +1,65 @@
+om = $om;
+ $this->repository = $repo;
+ }
+
+ public function create($eventName, $type, array $data)
+ {
+ $event = new WebhookEvent();
+ $event->setName($eventName);
+ $event->setType($type);
+ $event->setData($data);
+
+ $this->update($event);
+
+ return $event;
+ }
+
+ public function delete(WebhookEvent $event)
+ {
+ $this->om->remove($event);
+ $this->om->flush();
+ }
+
+ public function update(WebhookEvent $event)
+ {
+ $this->om->persist($event);
+ $this->om->flush();
+ }
+
+ public function processed(WebhookEvent $event)
+ {
+ $event->setProcessed(true);
+ $this->update($event);
+ }
+}
diff --git a/lib/Alchemy/Phrasea/Model/Repositories/ApiApplicationRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/ApiApplicationRepository.php
index 8f50e1c4c1..aff778ffe0 100644
--- a/lib/Alchemy/Phrasea/Model/Repositories/ApiApplicationRepository.php
+++ b/lib/Alchemy/Phrasea/Model/Repositories/ApiApplicationRepository.php
@@ -50,4 +50,12 @@ class ApiApplicationRepository extends EntityRepository
return $qb->getQuery()->getResult();
}
+
+ public function findWithDefinedWebhookCallback()
+ {
+ $qb = $this->createQueryBuilder('app');
+ $qb->where($qb->expr()->isNotNull('app.webhookUrl'));
+
+ return $qb->getQuery()->getResult();
+ }
}
diff --git a/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventDeliveryRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventDeliveryRepository.php
new file mode 100644
index 0000000000..e8e965be01
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventDeliveryRepository.php
@@ -0,0 +1,37 @@
+createQueryBuilder('e');
+
+ $qb
+ ->where($qb->expr()->eq('e.delivered', $qb->expr()->literal(false)))
+ ->andWhere($qb->expr()->lt('e.deliveryTries', ':nb_tries'));
+
+ $qb->setParameter(':nb_tries', WebhookEventDelivery::MAX_DELIVERY_TRIES);
+
+ return $qb->getQuery()->getResult();
+ }
+}
diff --git a/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventRepository.php
new file mode 100644
index 0000000000..eb29814d8d
--- /dev/null
+++ b/lib/Alchemy/Phrasea/Model/Repositories/WebhookEventRepository.php
@@ -0,0 +1,33 @@
+createQueryBuilder('e');
+
+ $qb->where($qb->expr()->eq('e.processed', $qb->expr()->literal(false)));
+
+ return $qb->getQuery()->getResult();
+ }
+}
diff --git a/lib/Alchemy/Phrasea/TaskManager/Job/WebhookJob.php b/lib/Alchemy/Phrasea/TaskManager/Job/WebhookJob.php
new file mode 100644
index 0000000000..4c4c45eda7
--- /dev/null
+++ b/lib/Alchemy/Phrasea/TaskManager/Job/WebhookJob.php
@@ -0,0 +1,168 @@
+httpClient = $httpClient ?: new GuzzleClient();
+ $this->httpClient->setUserAgent(sprintf('Phraseanet/%s (%s)', Version::getNumber(), Version::getName()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->translator->trans("API Webhook");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getJobId()
+ {
+ return 'Webhook';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription()
+ {
+ return $this->translator->trans("Notify third party application when an event occurs in Phraseanet");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEditor()
+ {
+ return new DefaultEditor($this->translator);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doJob(JobData $data)
+ {
+ $app = $data->getApplication();
+ $thirdPartyApplications = $app['repo.api-applications']->findWithDefinedWebhookCallback();
+ $that = $this;
+
+ $this->httpClient->getEventDispatcher()->addListener('request.error', function (Event $event) {
+ // override guzzle default behavior of throwing exceptions
+ // when 4xx & 5xx responses are encountered
+ $event->stopPropagation();
+ }, -254);
+
+ $this->httpClient->addSubscriber(new BackoffPlugin(
+ // set max retries
+ new TruncatedBackoffStrategy(WebhookEventDelivery::MAX_DELIVERY_TRIES,
+ // set callback which logs success or failure
+ new CallbackBackoffStrategy(function($retries, $request, $response, $e) use ($app, $that) {
+ $retry = true;
+ if ($response && (null !== $deliverId = parse_url($request->getUrl(), PHP_URL_FRAGMENT))) {
+ $delivery = $app['repo.webhook-delivery']->find($deliverId);
+ if ($response->isSuccessful()) {
+ $app['manipulator.webhook-delivery']->deliverySuccess($delivery);
+
+ $that->log('info', sprintf('Deliver success event "%d:%s" for app "%s"', $delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(), $delivery->getThirdPartyApplication()->getName()));
+ $retry = false;
+ } else {
+ $app['manipulator.webhook-delivery']->deliveryFailure($delivery);
+
+ $that->log('error', sprintf('Deliver failure event "%d:%s" for app "%s"', $delivery->getWebhookEvent()->getId(), $delivery->getWebhookEvent()->getName(), $delivery->getThirdPartyApplication()->getName()));
+ }
+
+ return $retry;
+ }},
+ true,
+ new CurlBackoffStrategy()
+ )
+ )
+ ));
+
+ foreach ($app['repo.webhook-event']->findUnprocessedEvents() as $event) {
+ // set event as processed
+ $app['manipulator.webhook-event']->processed($event);
+
+ $this->log('info', sprintf('Processing event "%s" with id %d', $event->getName(), $event->getId()));
+
+ // send requests
+ $this->deliverEvent($app, $thirdPartyApplications, $event);
+ }
+ }
+
+ private function deliverEvent(Application $app, array $thirdPartyApplications, WebhookEvent $event)
+ {
+ if (count($thirdPartyApplications) === 0) {
+ $this->log('info', sprintf('No applications defined to listen for webhook events'));
+
+ return;
+ }
+
+ // format event data
+ $eventFactory = new EventProcessorFactory($app);
+ $eventProcessor = $eventFactory->get($event);
+ $data = $eventProcessor->process();
+
+ // batch requests
+ $batch = BatchBuilder::factory()
+ ->transferRequests(10)
+ ->build();
+
+ foreach ($thirdPartyApplications as $thirdPartyApplication) {
+ $delivery = $app['manipulator.webhook-delivery']->create($thirdPartyApplication, $event);
+
+ // append delivery id as url anchor
+ $uniqueUrl = $this->getUrl($thirdPartyApplication, $delivery);
+
+ // create http request with data as request body
+ $batch->add($this->httpClient->createRequest('POST', $uniqueUrl, array(
+ 'Content-Type' => 'application/vnd.phraseanet.event+json'
+ ), json_encode($data)));
+ }
+
+ $batch->flush();
+ }
+
+ private function getUrl(ApiApplication $application, WebhookEventDelivery $delivery)
+ {
+ return sprintf('%s#%s', $application->getWebhookUrl(), $delivery->getId());
+ }
+}
diff --git a/lib/classes/eventsmanager/notify/feed.php b/lib/classes/eventsmanager/notify/feed.php
index 9dd5679ac2..0e6d1e01de 100644
--- a/lib/classes/eventsmanager/notify/feed.php
+++ b/lib/classes/eventsmanager/notify/feed.php
@@ -11,6 +11,7 @@
use Alchemy\Phrasea\Notification\Receiver;
use Alchemy\Phrasea\Notification\Mail\MailInfoNewPublication;
+use Alchemy\Phrasea\Model\Entities\WebhookEvent;
class eventsmanager_notify_feed extends eventsmanager_notifyAbstract
{
@@ -59,9 +60,11 @@ class eventsmanager_notify_feed extends eventsmanager_notifyAbstract
$data = $dom_xml->saveXml();
- API_Webhook::create($this->app['phraseanet.appbox'], API_Webhook::NEW_FEED_ENTRY, array_merge(
- array('feed_id' => $entry->getFeed()->getId()), $params
- ));
+ $this->app['manipulator.webhook-event']->create(
+ WebhookEvent::NEW_FEED_ENTRY,
+ WebhookEvent::FEED_ENTRY_TYPE,
+ array_merge(array('feed_id' => $entry->getFeed()->getId()), $params)
+ );
$Query = new \User_Query($this->app);
diff --git a/lib/classes/task/period/apiwebhooks.php b/lib/classes/task/period/apiwebhooks.php
deleted file mode 100644
index 20532cd272..0000000000
--- a/lib/classes/task/period/apiwebhooks.php
+++ /dev/null
@@ -1,144 +0,0 @@
-get_connection()->prepare('SELECT id, `type`, `data` FROM api_webhooks');
- $stmt->execute();
- $rs = $stmt->fetchAll(\PDO::FETCH_ASSOC);
- $stmt->closeCursor();
-
- return $rs;
- }
-
- protected function processOneContent(appbox $appbox, array $row)
- {
- $data = null;
- switch ($row['type']) {
- case \API_Webhook::NEW_FEED_ENTRY:
- $data = $this->processNewFeedEntry($row);
- }
-
- if (null === $data) {
- return;
- }
- $urls = $this->getApplicationHookUrls($appbox);
- $this->sendData($urls, $data);
- }
-
- protected function postProcessOneContent(appbox $appbox, array $row)
- {
- $w = new API_Webhook($appbox, $row['id']);
- $w->delete();
- }
-
- protected function getApplicationHookUrls(appbox $appbox)
- {
- $stmt = $appbox->get_connection()->prepare('
- SELECT webhook_url
- FROM api_applications
- WHERE webhook_url IS NOT NULL
- ');
- $stmt->execute();
- $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
- $stmt->closeCursor();
-
- return array_map(function ($row) {
- return $row['webhook_url'];
- }, $rows);
- }
-
- protected function sendData(array $urls, array $data)
- {
- if (count($urls) === 0) {
- return;
- }
- $client = new GuzzleClient();
- $body = json_encode($data);
- $requests = array();
- foreach ($urls as $url) {
- $requests[] = $client->createRequest('POST', $url, array(
- 'Content-Type' => 'application/vnd.phraseanet.event+json'
- ), $body);
- }
- $client->send($requests);
- }
-
- protected function processNewFeedEntry(array $row)
- {
- $data = json_decode($row['data']);
- if (!isset($data->{"feed_id"}) || !isset($data->{"entry_id"})) {
- return;
- }
- $feed = new Feed_Adapter($this->dependencyContainer, $data->{"feed_id"});
- $entry = new \Feed_Entry_Adapter($this->dependencyContainer, $feed, $data->{"entry_id"});
- $query = new \User_Query($this->dependencyContainer);
-
- $query->include_phantoms(true)
- ->include_invite(false)
- ->include_templates(false)
- ->email_not_null(true);
-
- if ($entry->get_feed()->get_collection()) {
- $query->on_base_ids(array($entry->get_feed()->get_collection()->get_base_id()));
- }
-
- $start = 0;
- $perLoop = 100;
- $users = array();
-
- do {
- $results = $query->limit($start, $perLoop)->execute()->get_results();
- foreach ($results as $user) {
- $users[] = array(
- 'email' => $user->get_email(),
- 'firstname' => $user->get_firstname() ?: null,
- 'lastname' => $user->get_lastname() ?: null,
- );
- }
- $start += $perLoop;
- } while (count($results) > 0);
-
- return array(
- 'event' => $row['type'],
- 'users_were_notified' => !!$data->{"notify_email"},
- 'feed' => array(
- 'id' => $feed->get_id(),
- 'title' => $feed->get_title(),
- 'description' => $feed->get_subtitle() ?: null,
- ),
- 'entry' => array(
- 'id' => $entry->get_id(),
- 'author' => array(
- 'name' => $entry->get_author_name(),
- 'email' => $entry->get_author_email()
- ),
- 'title' => $entry->get_title(),
- 'description' => $entry->get_subtitle() ?: null,
- ),
- 'users' => $users
- );
- }
-}
diff --git a/lib/conf.d/bases_structure.xml b/lib/conf.d/bases_structure.xml
index da68dfdea5..f1ad2be4f4 100644
--- a/lib/conf.d/bases_structure.xml
+++ b/lib/conf.d/bases_structure.xml
@@ -76,53 +76,6 @@
InnoDB
-
-
-
- id
- int(11) unsigned
-
- auto_increment
-
-
-
-
- type
- varchar(64)
-
-
-
-
-
-
- data
- longtext
-
-
-
-
-
-
- created
- datetime
-
-
-
-
-
-
-
-
- PRIMARY
- PRIMARY
-
- id
-
-
-
- InnoDB
-
-
diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php
index f7777daaa7..9e0d20fdab 100644
--- a/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php
+++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/ManipulatorServiceProviderTest.php
@@ -57,6 +57,16 @@ class ManipulatorServiceProviderTest extends ServiceProviderTestCase
'manipulator.api-oauth-refresh-token',
'Alchemy\Phrasea\Model\Manipulator\ApiOauthRefreshTokenManipulator'
],
+ [
+ 'Alchemy\Phrasea\Core\Provider\ManipulatorServiceProvider',
+ 'manipulator.webhook-event',
+ 'Alchemy\Phrasea\Model\Manipulator\WebhookEventManipulator'
+ ],
+ [
+ 'Alchemy\Phrasea\Core\Provider\ManipulatorServiceProvider',
+ 'manipulator.webhook-delivery',
+ 'Alchemy\Phrasea\Model\Manipulator\WebhookEventDelivery'
+ ],
];
}
}
diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php
index 7ccc478279..5400ee73d7 100644
--- a/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php
+++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/RepositoriesServiceProviderTest.php
@@ -39,6 +39,8 @@ class RepositoriesServiceProviderTest extends ServiceProviderTestCase
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-tokens', 'Alchemy\Phrasea\Model\Repositories\ApiOauthTokenRepository'],
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-codes', 'Alchemy\Phrasea\Model\Repositories\ApiOauthCodeRepository'],
['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.api-oauth-refresh-tokens', 'Alchemy\Phrasea\Model\Repositories\ApiOauthRefreshTokenRepository'],
+ ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.webhook-event', 'Alchemy\Phrasea\Model\Repositories\WebhookEventRepository'],
+ ['Alchemy\Phrasea\Core\Provider\RepositoriesServiceProvider', 'repo.webhook-delivery', 'Alchemy\Phrasea\Model\Repositories\WebhookEventDeliveryRepository'],
];
}
}
diff --git a/tests/Alchemy/Tests/Phrasea/Model/Manipulator/WebhookEventDeliveryManipulatorTest.php b/tests/Alchemy/Tests/Phrasea/Model/Manipulator/WebhookEventDeliveryManipulatorTest.php
new file mode 100644
index 0000000000..a61880afea
--- /dev/null
+++ b/tests/Alchemy/Tests/Phrasea/Model/Manipulator/WebhookEventDeliveryManipulatorTest.php
@@ -0,0 +1,106 @@
+create(
+ uniqid('app'),
+ ApiApplication::WEB_TYPE,
+ 'Desktop application description',
+ 'http://web-app-url.net',
+ self::$DI['user'],
+ 'http://web-app-url.net/callback'
+ );
+
+ $manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
+ $nbHooks = count(self::$DI['app']['repo.webhook-delivery']->findAll());
+ $manipulator->create($application, self::$DI['webhook-event']);
+ $this->assertGreaterThan($nbHooks, count(self::$DI['app']['repo.webhook-delivery']->findAll()));
+ }
+
+ public function testDelete()
+ {
+ $manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
+ $application = $manipApp->create(
+ uniqid('app'),
+ ApiApplication::WEB_TYPE,
+ 'Desktop application description',
+ 'http://web-app-url.net',
+ self::$DI['user'],
+ 'http://web-app-url.net/callback'
+ );
+ $manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
+ $eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
+ $countBefore = count(self::$DI['app']['repo.webhook-delivery']->findAll());
+ $manipulator->delete($eventDelivery);
+ $this->assertGreaterThan(count(self::$DI['app']['repo.webhook-delivery']->findAll()), $countBefore);
+ }
+
+ public function testUpdate()
+ {
+ $manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
+ $application = $manipApp->create(
+ uniqid('app'),
+ ApiApplication::WEB_TYPE,
+ 'Desktop application description',
+ 'http://web-app-url.net',
+ self::$DI['user'],
+ 'http://web-app-url.net/callback'
+ );
+ $manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
+ $eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
+ $this->assertEquals(0, $eventDelivery->getDeliveryTries());
+ $eventDelivery->setDeliverTries(1);
+ $manipulator->update($eventDelivery);
+ $eventDelivery = self::$DI['app']['repo.webhook-delivery']->find($eventDelivery->getId());
+ $this->assertEquals(1, $eventDelivery->getDeliveryTries());
+ }
+
+ public function testDeliverySuccess()
+ {
+ $manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
+ $application = $manipApp->create(
+ uniqid('app'),
+ ApiApplication::WEB_TYPE,
+ 'Desktop application description',
+ 'http://web-app-url.net',
+ self::$DI['user'],
+ 'http://web-app-url.net/callback'
+ );
+ $manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
+ $eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
+ $tries = $eventDelivery->getDeliveryTries();
+ $manipulator->deliverySuccess($eventDelivery);
+ $this->assertTrue($eventDelivery->isDelivered());
+ $this->assertGreaterThan($tries, $eventDelivery->getDeliveryTries());
+ }
+
+ public function testDeliveryFailure()
+ {
+ $manipApp = new ApiApplicationManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.api-applications'], self::$DI['app']['random.medium']);
+ $application = $manipApp->create(
+ uniqid('app'),
+ ApiApplication::WEB_TYPE,
+ 'Desktop application description',
+ 'http://web-app-url.net',
+ self::$DI['user'],
+ 'http://web-app-url.net/callback'
+ );
+ $manipulator = new WebhookEventDeliveryManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-delivery']);
+ $eventDelivery = $manipulator->create($application, self::$DI['webhook-event']);
+ $tries = $eventDelivery->getDeliveryTries();
+ $manipulator->deliveryFailure($eventDelivery);
+ $this->assertfalse($eventDelivery->isDelivered());
+ $this->assertGreaterThan($tries, $eventDelivery->getDeliveryTries());
+ }
+}
diff --git a/tests/Alchemy/Tests/Phrasea/Model/Manipulator/WebhookEventManipulatorTest.php b/tests/Alchemy/Tests/Phrasea/Model/Manipulator/WebhookEventManipulatorTest.php
new file mode 100644
index 0000000000..816caf33a3
--- /dev/null
+++ b/tests/Alchemy/Tests/Phrasea/Model/Manipulator/WebhookEventManipulatorTest.php
@@ -0,0 +1,57 @@
+findAll());
+ $event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
+ 'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
+ ));
+ $this->assertGreaterThan($nbEvents, count(self::$DI['app']['repo.webhook-event']->findAll()));
+ }
+
+ public function testDelete()
+ {
+ $manipulator = new WebhookEventManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-event']);
+ $event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
+ 'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
+ ));
+ $eventMem = clone $event;
+ $countBefore = count(self::$DI['app']['repo.webhook-event']->findAll());
+ self::$DI['app']['manipulator.webhook-delivery']->create($event);
+ $manipulator->delete($event);
+ $this->assertGreaterThan(count(self::$DI['app']['repo.webhook-event']->findAll()), $countBefore);
+ $tokens = self::$DI['app']['repo.api-oauth-tokens']->findOauthTokens($eventMem);
+ $this->assertEquals(0, count($tokens));
+ }
+
+ public function testUpdate()
+ {
+ $manipulator = new WebhookEventManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-event']);
+ $event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
+ 'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
+ ));
+ $event->setProcessed(true);
+ $manipulator->update($event);
+ $event = self::$DI['app']['repo.webhook-event']->find($event->getId());
+ $this->assertTrue($event->isProcessed());
+ }
+
+ public function testProcessed()
+ {
+ $manipulator = new WebhookEventManipulator(self::$DI['app']['EM'], self::$DI['app']['repo.webhook-event']);
+ $event = $manipulator->create(WebhookEvent::NEW_FEED_ENTRY, WebhookEvent::FEED_ENTRY_TYPE, array(
+ 'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(), 'entry_id' => self::$DI['feed_public_entry']->getId()
+ ));
+ $manipulator->processed($event);
+ $this->assertTrue($event->isProcessed());
+ }
+}
diff --git a/tests/Alchemy/Tests/Phrasea/Model/Repositories/WebhookEventDeliveryRepositoryTest.php b/tests/Alchemy/Tests/Phrasea/Model/Repositories/WebhookEventDeliveryRepositoryTest.php
new file mode 100644
index 0000000000..d466d60d8a
--- /dev/null
+++ b/tests/Alchemy/Tests/Phrasea/Model/Repositories/WebhookEventDeliveryRepositoryTest.php
@@ -0,0 +1,12 @@
+getRepository('Phraseanet:WebhookEventDelivery')->findUndeliveredEvents();
+ $this->assertCount(1, $events);
+ }
+}
diff --git a/tests/Alchemy/Tests/Phrasea/Model/Repositories/WebhookEventRepositoryTest.php b/tests/Alchemy/Tests/Phrasea/Model/Repositories/WebhookEventRepositoryTest.php
new file mode 100644
index 0000000000..a08800b987
--- /dev/null
+++ b/tests/Alchemy/Tests/Phrasea/Model/Repositories/WebhookEventRepositoryTest.php
@@ -0,0 +1,12 @@
+getRepository('Phraseanet:WebhookEvent')->findUnprocessedEvents();
+ $this->assertCount(1, $events);
+ }
+}
diff --git a/tests/Alchemy/Tests/Phrasea/TaskManager/Job/WebhookJobTest.php b/tests/Alchemy/Tests/Phrasea/TaskManager/Job/WebhookJobTest.php
new file mode 100644
index 0000000000..bc09dd07c5
--- /dev/null
+++ b/tests/Alchemy/Tests/Phrasea/TaskManager/Job/WebhookJobTest.php
@@ -0,0 +1,13 @@
+createTranslatorMock());
+ }
+}
diff --git a/tests/Alchemy/Tests/Phrasea/Webhook/EventProcessorFactoryTest.php b/tests/Alchemy/Tests/Phrasea/Webhook/EventProcessorFactoryTest.php
new file mode 100644
index 0000000000..b1007679d8
--- /dev/null
+++ b/tests/Alchemy/Tests/Phrasea/Webhook/EventProcessorFactoryTest.php
@@ -0,0 +1,37 @@
+setType($type);
+ $this->assertInstanceOf($expected, $factory->get($event));
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ */
+ public function testUnknownProcessor()
+ {
+ $factory = new EventProcessorFactory(self::$DI['app']);
+ $event = new WebhookEvent();
+ $factory->get($event);
+ }
+
+ public function eventProvider()
+ {
+ return array(
+ array(WebhookEvent::FEED_ENTRY_TYPE, 'Alchemy\Phrasea\Webhook\Processor\FeedEntryProcessor'),
+ );
+ }
+}
diff --git a/tests/Alchemy/Tests/Phrasea/Webhook/Processor/FeedEntryProcessorTest.php b/tests/Alchemy/Tests/Phrasea/Webhook/Processor/FeedEntryProcessorTest.php
new file mode 100644
index 0000000000..9a54f3690d
--- /dev/null
+++ b/tests/Alchemy/Tests/Phrasea/Webhook/Processor/FeedEntryProcessorTest.php
@@ -0,0 +1,49 @@
+setData(array(
+ 'feed_id' => 0,
+ 'entry_id' => 0
+ ));
+ $event->setName(WebhookEvent::NEW_FEED_ENTRY);
+ $event->setType(WebhookEvent::FEED_ENTRY_TYPE);
+ $processor = new FeedEntryProcessor($event, self::$DI['app']);
+ $this->assertEquals($processor->process(), null);
+ }
+
+ public function testProcessWithMissingDataProperty()
+ {
+ $event = new WebhookEvent();
+ $event->setData(array(
+ 'feed_id' => 0,
+ ));
+ $event->setName(WebhookEvent::NEW_FEED_ENTRY);
+ $event->setType(WebhookEvent::FEED_ENTRY_TYPE);
+ $processor = new FeedEntryProcessor($event, self::$DI['app']);
+ $this->assertEquals($processor->process(), null);
+ }
+
+
+ public function testProcess()
+ {
+ $event = new WebhookEvent();
+ $event->setData(array(
+ 'feed_id' => self::$DI['feed_public_entry']->getFeed()->getId(),
+ 'entry_id' => self::$DI['feed_public_entry']->getId()
+ ));
+ $event->setName(WebhookEvent::NEW_FEED_ENTRY);
+ $event->setType(WebhookEvent::FEED_ENTRY_TYPE);
+ $processor = new FeedEntryProcessor($event, self::$DI['app']);
+ $this->assertEquals($processor->process(), null);
+ }
+}
diff --git a/tests/classes/PhraseanetTestCase.php b/tests/classes/PhraseanetTestCase.php
index 3bb1dc868a..302089003f 100644
--- a/tests/classes/PhraseanetTestCase.php
+++ b/tests/classes/PhraseanetTestCase.php
@@ -202,6 +202,10 @@ abstract class PhraseanetTestCase extends WebTestCase
return $DI['app']['repo.api-applications']->find(self::$fixtureIds['oauth']['user']);
});
+ self::$DI['webhook-event'] = self::$DI->share(function ($DI) {
+ return $DI['app']['repo.webhook-event']->find(self::$fixtureIds['webhook']['event']);
+ });
+
self::$DI['oauth2-app-user-not-admin'] = self::$DI->share(function ($DI) {
return $DI['app']['repo.api-applications']->find(self::$fixtureIds['oauth']['user-not-admin']);
});