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']); });