From 8d18607f8029606381489dccd6c66efdffb842f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 23 May 2024 11:19:27 +0200 Subject: [PATCH 01/32] feat: Add support for webhook listeners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/composer/composer/autoload_classmap.php | 2 + lib/composer/composer/autoload_static.php | 2 + .../Bootstrap/RegistrationContext.php | 32 ++++++++++ .../WebhookEventListenerRegistration.php | 61 +++++++++++++++++++ lib/private/EventDispatcher/WebhookCaller.php | 53 ++++++++++++++++ .../Bootstrap/IRegistrationContext.php | 16 +++++ 6 files changed, 166 insertions(+) create mode 100644 lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php create mode 100644 lib/private/EventDispatcher/WebhookCaller.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 8e1408e121ef6..7ae7b490f7dce 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -866,6 +866,7 @@ 'OC\\AppFramework\\Bootstrap\\ServiceAliasRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceFactoryRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/ServiceRegistration.php', + 'OC\\AppFramework\\Bootstrap\\WebhookEventListenerRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php', 'OC\\AppFramework\\DependencyInjection\\DIContainer' => $baseDir . '/lib/private/AppFramework/DependencyInjection/DIContainer.php', 'OC\\AppFramework\\Http' => $baseDir . '/lib/private/AppFramework/Http.php', 'OC\\AppFramework\\Http\\Dispatcher' => $baseDir . '/lib/private/AppFramework/Http/Dispatcher.php', @@ -1382,6 +1383,7 @@ 'OC\\Encryption\\Util' => $baseDir . '/lib/private/Encryption/Util.php', 'OC\\EventDispatcher\\EventDispatcher' => $baseDir . '/lib/private/EventDispatcher/EventDispatcher.php', 'OC\\EventDispatcher\\ServiceEventListener' => $baseDir . '/lib/private/EventDispatcher/ServiceEventListener.php', + 'OC\\EventDispatcher\\WebhookCaller' => $baseDir . '/lib/private/EventDispatcher/WebhookCaller.php', 'OC\\EventSource' => $baseDir . '/lib/private/EventSource.php', 'OC\\EventSourceFactory' => $baseDir . '/lib/private/EventSourceFactory.php', 'OC\\Federation\\CloudFederationFactory' => $baseDir . '/lib/private/Federation/CloudFederationFactory.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d6939ae36ce8b..16b148dae5b5a 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -907,6 +907,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\AppFramework\\Bootstrap\\ServiceAliasRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceFactoryRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/ServiceRegistration.php', + 'OC\\AppFramework\\Bootstrap\\WebhookEventListenerRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php', 'OC\\AppFramework\\DependencyInjection\\DIContainer' => __DIR__ . '/../../..' . '/lib/private/AppFramework/DependencyInjection/DIContainer.php', 'OC\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http.php', 'OC\\AppFramework\\Http\\Dispatcher' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Dispatcher.php', @@ -1423,6 +1424,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Encryption\\Util' => __DIR__ . '/../../..' . '/lib/private/Encryption/Util.php', 'OC\\EventDispatcher\\EventDispatcher' => __DIR__ . '/../../..' . '/lib/private/EventDispatcher/EventDispatcher.php', 'OC\\EventDispatcher\\ServiceEventListener' => __DIR__ . '/../../..' . '/lib/private/EventDispatcher/ServiceEventListener.php', + 'OC\\EventDispatcher\\WebhookCaller' => __DIR__ . '/../../..' . '/lib/private/EventDispatcher/WebhookCaller.php', 'OC\\EventSource' => __DIR__ . '/../../..' . '/lib/private/EventSource.php', 'OC\\EventSourceFactory' => __DIR__ . '/../../..' . '/lib/private/EventSourceFactory.php', 'OC\\Federation\\CloudFederationFactory' => __DIR__ . '/../../..' . '/lib/private/Federation/CloudFederationFactory.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index df03d59ebfafe..b82f30e0fb661 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -81,6 +81,9 @@ class RegistrationContext { /** @var EventListenerRegistration[] */ private $eventListeners = []; + /** @var WebhookEventListenerRegistration[] */ + private $webhookEventListeners = []; + /** @var MiddlewareRegistration[] */ private $middlewares = []; @@ -221,6 +224,17 @@ public function registerEventListener(string $event, string $listener, int $prio ); } + public function registerWebhookEventListener(string $event, string $method, string $listenerUri, array $options = [], int $priority = 0): void { + $this->context->registerWebhookEventListener( + $this->appId, + $event, + $method, + $listenerUri, + $options, + $priority, + ); + } + public function registerMiddleware(string $class, bool $global = false): void { $this->context->registerMiddleware( $this->appId, @@ -451,6 +465,10 @@ public function registerEventListener(string $appId, string $event, string $list $this->eventListeners[] = new EventListenerRegistration($appId, $event, $listener, $priority); } + public function registerWebhookEventListener(string $appId, string $event, string $method, string $listenerUri, array $options, int $priority = 0): void { + $this->webhookEventListeners[] = new WebhookEventListenerRegistration($appId, $event, $method, $listenerUri, $options, $priority); + } + /** * @psalm-param class-string $class */ @@ -674,6 +692,20 @@ public function delegateEventListenerRegistrations(IEventDispatcher $eventDispat ]); } } + while (($registration = array_shift($this->webhookEventListeners)) !== null) { + try { + $eventDispatcher->addListener( + $registration->getEvent(), + $registration->getCallable(), + $registration->getPriority() + ); + } catch (Throwable $e) { + $appId = $registration->getAppId(); + $this->logger->error("Error during event webhook listener registration of $appId: " . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } } /** diff --git a/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php b/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php new file mode 100644 index 0000000000000..3b283c6029d39 --- /dev/null +++ b/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php @@ -0,0 +1,61 @@ + + * + * @author Côme Chilliet + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\AppFramework\Bootstrap; + +use OC\EventDispatcher\WebhookCaller; +use OCP\EventDispatcher\Event; +use OCP\Server; + +/** + * @psalm-immutable + */ +class WebhookEventListenerRegistration extends ARegistration { + public function __construct( + string $appId, + private string $event, + private string $method, + private string $uri, + private array $options, + private int $priority, + ) { + parent::__construct($appId); + } + + public function getEvent(): string { + return $this->event; + } + + public function getCallable(): callable { + return function (Event $event) { + Server::get(WebhookCaller::class)->callWebhook($event, $this->method, $this->uri, $this->options); + }; + } + + public function getPriority(): int { + return $this->priority; + } +} diff --git a/lib/private/EventDispatcher/WebhookCaller.php b/lib/private/EventDispatcher/WebhookCaller.php new file mode 100644 index 0000000000000..2f0d1a2ab0617 --- /dev/null +++ b/lib/private/EventDispatcher/WebhookCaller.php @@ -0,0 +1,53 @@ + + * + * @author Côme Chilliet + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\EventDispatcher; + +use OCP\EventDispatcher\Event; +use OCP\Http\Client\IClientService; + +class WebhookCaller { + public function __construct( + private IClientService $clientService, + ) { + } + + public function callWebhook( + Event $event, + string $method, + string $uri, + array $options, + ): void { + $client = $this->clientService->newClient(); + $client->request($method, $uri, $options + ['query' => ['event' => $event::class]]); + + /** + * TODO: + * Serialization of the event + * Timeout or async + */ + } +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index b86f7bcd76d71..17040ccfb4fa2 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -120,6 +120,22 @@ public function registerParameter(string $name, $value): void; */ public function registerEventListener(string $event, string $listener, int $priority = 0): void; + /** + * Register a webhook listener + * + * @psalm-template T of \OCP\EventDispatcher\Event + * @param class-string $event The fully-qualified class name of the Event sub class to listen for + * @param string $method The HTTP method to use (usually 'GET' or 'POST') + * @param string $listenerUri The absolute URI to contact + * @param array $options Additional options for the request, {@see \OCP\Http\Client::request()} + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + * + * @since 30.0.0 + */ + public function registerWebhookEventListener(string $event, string $method, string $listenerUri, array $options = [], int $priority = 0): void; + + /** * @param string $class * @param bool $global load this middleware also for requests of other apps? Added in Nextcloud 26 From d835d23e3a73bb09f46096a93269bb34b51fbb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 23 May 2024 15:41:55 +0200 Subject: [PATCH 02/32] feat: Serialize event data and author userid to webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/private/EventDispatcher/WebhookCaller.php | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/private/EventDispatcher/WebhookCaller.php b/lib/private/EventDispatcher/WebhookCaller.php index 2f0d1a2ab0617..0265ef317b447 100644 --- a/lib/private/EventDispatcher/WebhookCaller.php +++ b/lib/private/EventDispatcher/WebhookCaller.php @@ -28,10 +28,14 @@ use OCP\EventDispatcher\Event; use OCP\Http\Client\IClientService; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; class WebhookCaller { public function __construct( private IClientService $clientService, + private IUserSession $userSession, + private LoggerInterface $logger, ) { } @@ -42,12 +46,47 @@ public function callWebhook( array $options, ): void { $client = $this->clientService->newClient(); - $client->request($method, $uri, $options + ['query' => ['event' => $event::class]]); + if (!isset($options['body'])) { + $options['body'] = json_encode([ + 'event' => $this->serializeEvent($event), + 'userid' => $this->userSession->getUser()?->getUID() ?? null, + ]); + } + try { + $response = $client->request($method, $uri, $options + ['query' => ['event' => $event::class]]); + $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 300) { + $this->logger->warning('Webhook returned unexpected status code '.$statusCode, ['body' => $response->getBody()]); + } else { + $this->logger->debug('Webhook returned status code '.$statusCode, ['body' => $response->getBody()]); + } + } catch (\Exception $e) { + $this->logger->error('Webhook call failed: '.$e->getMessage(), ['exception' => $e]); + } + } - /** - * TODO: - * Serialization of the event - * Timeout or async - */ + private function serializeEvent(Event $event): array|\JsonSerializable { + if ($event instanceof \JsonSerializable) { + return $event; + } else { + /* Event is not serializable, we fallback to reflection to still send something */ + $data = []; + $ref = new \ReflectionClass($event); + foreach ($ref->getMethods() as $method) { + if (str_starts_with($method->getName(), 'get')) { + $key = strtolower(substr($method->getName(), 3)); + $value = $method->invoke($event); + if ($value instanceof \OCP\Files\FileInfo) { + $value = [ + 'id' => $value->getId(), + 'path' => $value->getPath(), + ]; + } + $data[$key] = $value; + } + } + $this->logger->debug('Webhook had to use fallback to serialize event '.$event::class); + return $data; + } } } From aa974a4322170ae3b489607967801ba84ccc63b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 28 May 2024 10:07:04 +0200 Subject: [PATCH 03/32] feat: Add webhooks application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .gitignore | 1 + apps/webhooks/appinfo/info.xml | 25 + apps/webhooks/composer/autoload.php | 25 + apps/webhooks/composer/composer.json | 13 + apps/webhooks/composer/composer.lock | 18 + .../composer/composer/ClassLoader.php | 579 +++++++++++++++ .../composer/composer/InstalledVersions.php | 359 +++++++++ apps/webhooks/composer/composer/LICENSE | 21 + .../composer/composer/autoload_classmap.php | 18 + .../composer/composer/autoload_namespaces.php | 9 + .../composer/composer/autoload_psr4.php | 10 + .../composer/composer/autoload_real.php | 37 + .../composer/composer/autoload_static.php | 44 ++ .../webhooks/composer/composer/installed.json | 5 + apps/webhooks/composer/composer/installed.php | 23 + apps/webhooks/img/app-dark.svg | 1 + apps/webhooks/img/app.svg | 1 + apps/webhooks/lib/AppInfo/Application.php | 55 ++ .../lib/BackgroundJobs/WebhookCall.php | 49 ++ apps/webhooks/lib/Command/Index.php | 83 +++ .../lib/Controller/WebhooksController.php | 193 +++++ apps/webhooks/lib/Db/WebhookListener.php | 60 ++ .../webhooks/lib/Db/WebhookListenerMapper.php | 151 ++++ .../lib/Listener/WebhooksEventListener.php | 64 ++ .../Version1000Date20240527153425.php | 66 ++ apps/webhooks/lib/ResponseDefinitions.php | 22 + apps/webhooks/openapi.json | 691 ++++++++++++++++++ .../tests/Db/WebhookListenerMapperTest.php | 61 ++ tests/enable_all.php | 1 + 29 files changed, 2685 insertions(+) create mode 100644 apps/webhooks/appinfo/info.xml create mode 100644 apps/webhooks/composer/autoload.php create mode 100644 apps/webhooks/composer/composer.json create mode 100644 apps/webhooks/composer/composer.lock create mode 100644 apps/webhooks/composer/composer/ClassLoader.php create mode 100644 apps/webhooks/composer/composer/InstalledVersions.php create mode 100644 apps/webhooks/composer/composer/LICENSE create mode 100644 apps/webhooks/composer/composer/autoload_classmap.php create mode 100644 apps/webhooks/composer/composer/autoload_namespaces.php create mode 100644 apps/webhooks/composer/composer/autoload_psr4.php create mode 100644 apps/webhooks/composer/composer/autoload_real.php create mode 100644 apps/webhooks/composer/composer/autoload_static.php create mode 100644 apps/webhooks/composer/composer/installed.json create mode 100644 apps/webhooks/composer/composer/installed.php create mode 100644 apps/webhooks/img/app-dark.svg create mode 100644 apps/webhooks/img/app.svg create mode 100644 apps/webhooks/lib/AppInfo/Application.php create mode 100644 apps/webhooks/lib/BackgroundJobs/WebhookCall.php create mode 100644 apps/webhooks/lib/Command/Index.php create mode 100644 apps/webhooks/lib/Controller/WebhooksController.php create mode 100644 apps/webhooks/lib/Db/WebhookListener.php create mode 100644 apps/webhooks/lib/Db/WebhookListenerMapper.php create mode 100644 apps/webhooks/lib/Listener/WebhooksEventListener.php create mode 100755 apps/webhooks/lib/Migration/Version1000Date20240527153425.php create mode 100644 apps/webhooks/lib/ResponseDefinitions.php create mode 100644 apps/webhooks/openapi.json create mode 100644 apps/webhooks/tests/Db/WebhookListenerMapperTest.php diff --git a/.gitignore b/.gitignore index 2e73df093dfd4..e9a27c5d68afc 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ !/apps/twofactor_backupcodes !/apps/user_status !/apps/weather_status +!/apps/webhooks !/apps/workflowengine /apps/files_external/3rdparty/irodsphp/PHPUnitTest /apps/files_external/3rdparty/irodsphp/web diff --git a/apps/webhooks/appinfo/info.xml b/apps/webhooks/appinfo/info.xml new file mode 100644 index 0000000000000..8e1d706d06f9f --- /dev/null +++ b/apps/webhooks/appinfo/info.xml @@ -0,0 +1,25 @@ + + + webhooks + Nextcloud webhook support + Nextcloud webhook support + Nextcloud webhook support + 1.0.0-dev + agpl + Côme Chilliet + Webhooks + + + + + + customization + https://github.com/nextcloud/server + https://github.com/nextcloud/server/issues + https://github.com/nextcloud/server.git + + + + + diff --git a/apps/webhooks/composer/autoload.php b/apps/webhooks/composer/autoload.php new file mode 100644 index 0000000000000..81ca354714949 --- /dev/null +++ b/apps/webhooks/composer/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/apps/webhooks/composer/composer/InstalledVersions.php b/apps/webhooks/composer/composer/InstalledVersions.php new file mode 100644 index 0000000000000..51e734a774b3e --- /dev/null +++ b/apps/webhooks/composer/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/apps/webhooks/composer/composer/LICENSE b/apps/webhooks/composer/composer/LICENSE new file mode 100644 index 0000000000000..f27399a042d95 --- /dev/null +++ b/apps/webhooks/composer/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/apps/webhooks/composer/composer/autoload_classmap.php b/apps/webhooks/composer/composer/autoload_classmap.php new file mode 100644 index 0000000000000..64646e93a023c --- /dev/null +++ b/apps/webhooks/composer/composer/autoload_classmap.php @@ -0,0 +1,18 @@ + $vendorDir . '/composer/InstalledVersions.php', + 'OCA\\Webhooks\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php', + 'OCA\\Webhooks\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php', + 'OCA\\Webhooks\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php', + 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php', + 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php', + 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => $baseDir . '/../lib/Migration/Version1000Date20240527153425.php', + 'OCA\\Webhooks\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', +); diff --git a/apps/webhooks/composer/composer/autoload_namespaces.php b/apps/webhooks/composer/composer/autoload_namespaces.php new file mode 100644 index 0000000000000..3f5c929625125 --- /dev/null +++ b/apps/webhooks/composer/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($baseDir . '/../lib'), +); diff --git a/apps/webhooks/composer/composer/autoload_real.php b/apps/webhooks/composer/composer/autoload_real.php new file mode 100644 index 0000000000000..1b1742c422ea5 --- /dev/null +++ b/apps/webhooks/composer/composer/autoload_real.php @@ -0,0 +1,37 @@ +setClassMapAuthoritative(true); + $loader->register(true); + + return $loader; + } +} diff --git a/apps/webhooks/composer/composer/autoload_static.php b/apps/webhooks/composer/composer/autoload_static.php new file mode 100644 index 0000000000000..ef878885cabf7 --- /dev/null +++ b/apps/webhooks/composer/composer/autoload_static.php @@ -0,0 +1,44 @@ + + array ( + 'OCA\\Webhooks\\' => 13, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'OCA\\Webhooks\\' => + array ( + 0 => __DIR__ . '/..' . '/../lib', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'OCA\\Webhooks\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php', + 'OCA\\Webhooks\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php', + 'OCA\\Webhooks\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php', + 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php', + 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php', + 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20240527153425.php', + 'OCA\\Webhooks\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitWebhooks::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitWebhooks::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitWebhooks::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/apps/webhooks/composer/composer/installed.json b/apps/webhooks/composer/composer/installed.json new file mode 100644 index 0000000000000..f20a6c47c6d4f --- /dev/null +++ b/apps/webhooks/composer/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": false, + "dev-package-names": [] +} diff --git a/apps/webhooks/composer/composer/installed.php b/apps/webhooks/composer/composer/installed.php new file mode 100644 index 0000000000000..1a66c7f2416b6 --- /dev/null +++ b/apps/webhooks/composer/composer/installed.php @@ -0,0 +1,23 @@ + array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev' => false, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/apps/webhooks/img/app-dark.svg b/apps/webhooks/img/app-dark.svg new file mode 100644 index 0000000000000..148495ade91db --- /dev/null +++ b/apps/webhooks/img/app-dark.svg @@ -0,0 +1 @@ + diff --git a/apps/webhooks/img/app.svg b/apps/webhooks/img/app.svg new file mode 100644 index 0000000000000..8d98fac4b695f --- /dev/null +++ b/apps/webhooks/img/app.svg @@ -0,0 +1 @@ + diff --git a/apps/webhooks/lib/AppInfo/Application.php b/apps/webhooks/lib/AppInfo/Application.php new file mode 100644 index 0000000000000..a8836a25f5470 --- /dev/null +++ b/apps/webhooks/lib/AppInfo/Application.php @@ -0,0 +1,55 @@ +injectFn($this->registerRuleListeners(...)); + } + + private function registerRuleListeners( + IEventDispatcher $dispatcher, + ContainerInterface $container, + LoggerInterface $logger, + ): void { + /** @var WebhookListenerMapper */ + $mapper = $container->get(WebhookListenerMapper::class); + + /* Listen to all events with at least one webhook configured */ + $configuredEvents = $mapper->getAllConfiguredEvents(); + foreach ($configuredEvents as $eventName) { + // $logger->error($eventName.' '.\OCP\Files\Events\Node\NodeWrittenEvent::class, ['exception' => new \Exception('coucou')]); + $dispatcher->addServiceListener( + $eventName, + WebhooksEventListener::class, + -1, + ); + } + } +} diff --git a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php new file mode 100644 index 0000000000000..9b113a5c1fc09 --- /dev/null +++ b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php @@ -0,0 +1,49 @@ +mapper->getById($webhookId); + $client = $this->clientService->newClient(); + $options = []; + $options['body'] = json_encode([ + 'event' => $event, + 'userid' => $userId, + ]); + try { + $response = $client->request($webhookListener->getHttpMethod(), $webhookListener->getUri(), $options); + $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 300) { + $this->logger->warning('Webhook returned unexpected status code '.$statusCode, ['body' => $response->getBody()]); + } else { + $this->logger->debug('Webhook returned status code '.$statusCode, ['body' => $response->getBody()]); + } + } catch (\Exception $e) { + $this->logger->error('Webhook call failed: '.$e->getMessage(), ['exception' => $e]); + } + } +} diff --git a/apps/webhooks/lib/Command/Index.php b/apps/webhooks/lib/Command/Index.php new file mode 100644 index 0000000000000..4f418b4ffc798 --- /dev/null +++ b/apps/webhooks/lib/Command/Index.php @@ -0,0 +1,83 @@ + + * + * @author Arthur Schiwon + * @author Joas Schilling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\WorkflowEngine\Command; + +use OCA\WorkflowEngine\Helper\ScopeContext; +use OCA\WorkflowEngine\Manager; +use OCP\WorkflowEngine\IManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Index extends Command { + + /** @var Manager */ + private $manager; + + public function __construct(Manager $manager) { + $this->manager = $manager; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('workflows:list') + ->setDescription('Lists configured workflows') + ->addArgument( + 'scope', + InputArgument::OPTIONAL, + 'Lists workflows for "admin", "user"', + 'admin' + ) + ->addArgument( + 'scopeId', + InputArgument::OPTIONAL, + 'User IDs when the scope is "user"', + null + ); + } + + protected function mappedScope(string $scope): int { + static $scopes = [ + 'admin' => IManager::SCOPE_ADMIN, + 'user' => IManager::SCOPE_USER, + ]; + return $scopes[$scope] ?? -1; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $ops = $this->manager->getAllOperations( + new ScopeContext( + $this->mappedScope($input->getArgument('scope')), + $input->getArgument('scopeId') + ) + ); + $output->writeln(\json_encode($ops)); + return 0; + } +} diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php new file mode 100644 index 0000000000000..b5aa14449ecdb --- /dev/null +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -0,0 +1,193 @@ + + * + * 200: Webhook registrations returned + */ + #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks')] + public function index(): DataResponse { + $webhookListeners = $this->mapper->getAll(); + + return new DataResponse($webhookListeners); + } + + /** + * Get details on a registered webhook + * + * @param int $id id of the webhook + * + * @return DataResponse + * + * 200: Webhook registration returned + */ + #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks/{id}')] + public function show(int $id): DataResponse { + return new DataResponse($this->mapper->getById($id)); + } + + /** + * Register a new webhook + * + * @param string $httpMethod HTTP method to use to contact the webhook + * @param string $uri Webhook URI endpoint + * @param string $event Event class name to listen to + * @param ?array $headers Array of headers to send + * @param ?string $authMethod Authentication method to use. TODO + * @param ?array $authData Array of data for authentication + * + * @return DataResponse + * + * 200: Webhook registration returned + * + * @throws OCSBadRequestException Bad request + * @throws OCSForbiddenException Insufficient permissions + * @throws OCSException Other error + */ + #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks')] + public function create( + string $httpMethod, + string $uri, + string $event, + ?array $headers, + ?string $authMethod, + ?array $authData, + ): DataResponse { + try { + $webhookListener = $this->mapper->addWebhookListener( + $this->userId, + $httpMethod, + $uri, + $event, + $headers, + $authMethod, + $authData, + ); + return new DataResponse($webhookListener); + } catch (\UnexpectedValueException $e) { + throw new OCSBadRequestException($e->getMessage(), $e); + } catch (\DomainException $e) { + throw new OCSForbiddenException($e->getMessage(), $e); + } catch (\Exception $e) { + $this->logger->error('Error when inserting webhook', ['exception' => $e]); + throw new OCSException('An internal error occurred', $e->getCode(), $e); + } + } + + /** + * Update an existing webhook registration + * + * @param int $id id of the webhook + * @param string $httpMethod HTTP method to use to contact the webhook + * @param string $uri Webhook URI endpoint + * @param string $event Event class name to listen to + * @param ?array $headers Array of headers to send + * @param ?string $authMethod Authentication method to use. TODO + * @param ?array $authData Array of data for authentication + * + * @return DataResponse + * + * 200: Webhook registration returned + * + * @throws OCSBadRequestException Bad request + * @throws OCSForbiddenException Insufficient permissions + * @throws OCSException Other error + */ + #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks/{id}')] + public function update( + int $id, + string $httpMethod, + string $uri, + string $event, + ?array $headers, + ?string $authMethod, + ?array $authData, + ): DataResponse { + try { + $webhookListener = $this->mapper->updateWebhookListener( + $id, + $this->userId, + $httpMethod, + $uri, + $event, + $headers, + $authMethod, + $authData, + ); + return new DataResponse($webhookListener); + } catch (\UnexpectedValueException $e) { + throw new OCSBadRequestException($e->getMessage(), $e); + } catch (\DomainException $e) { + throw new OCSForbiddenException($e->getMessage(), $e); + } catch (\Exception $e) { + $this->logger->error('Error when updating flow with id ' . $id, ['exception' => $e]); + throw new OCSException('An internal error occurred', $e->getCode(), $e); + } + } + + /** + * Remove an existing webhook registration + * + * @param int $id id of the webhook + * + * @return DataResponse + * + * 200: Boolean returned whether something was deleted FIXME + * + * @throws OCSBadRequestException Bad request + * @throws OCSForbiddenException Insufficient permissions + * @throws OCSException Other error + */ + #[ApiRoute(verb: 'DELETE', url: '/api/v1/webhooks/{id}')] + public function destroy(int $id): DataResponse { + try { + $deleted = $this->mapper->deleteById($id); + return new DataResponse($deleted); + } catch (\UnexpectedValueException $e) { + throw new OCSBadRequestException($e->getMessage(), $e); + } catch (\DomainException $e) { + throw new OCSForbiddenException($e->getMessage(), $e); + } catch (Exception $e) { + $this->logger->error('Error when deleting flow with id ' . $id, ['exception' => $e]); + throw new OCSException('An internal error occurred', $e->getCode(), $e); + } + } +} diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhooks/lib/Db/WebhookListener.php new file mode 100644 index 0000000000000..0d731e72d7f16 --- /dev/null +++ b/apps/webhooks/lib/Db/WebhookListener.php @@ -0,0 +1,60 @@ +addType('userId', 'string'); + $this->addType('httpMethod', 'string'); + $this->addType('uri', 'string'); + $this->addType('event', 'string'); + $this->addType('headers', 'json'); + $this->addType('authMethod', 'string'); + $this->addType('authData', 'json'); + } + + public function jsonSerialize(): array { + $fields = array_keys($this->getFieldTypes()); + return array_combine( + $fields, + array_map( + fn ($field) => $this->getter($field), + $fields + ) + ); + } +} diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php new file mode 100644 index 0000000000000..773358d671c0a --- /dev/null +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -0,0 +1,151 @@ + + */ +class WebhookListenerMapper extends QBMapper { + public const TABLE_NAME = 'webhook_listeners'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::TABLE_NAME, WebhookListener::class); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function getById(int $id): WebhookListener { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity($qb); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + * @return WebhookListener[] + */ + public function getAll(): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()); + + return $this->findEntities($qb); + } + + public function addWebhookListener( + string $userId, + string $httpMethod, + string $uri, + string $event, + ?array $headers, + ?string $authMethod, + ?array $authData, + ) { + $webhookListener = WebhookListener::fromParams( + [ + 'userId' => $userId, + 'httpMethod' => $httpMethod, + 'uri' => $uri, + 'event' => $event, + 'headers' => $headers, + 'authMethod' => $authMethod ?? 'none', + 'authData' => $authData, + ] + ); + return $this->insert($webhookListener); + } + + public function updateWebhookListener( + int $id, + string $userId, + string $httpMethod, + string $uri, + string $event, + ?array $headers, + ?string $authMethod, + ?array $authData, + ) { + $webhookListener = WebhookListener::fromParams( + [ + 'id' => $id, + 'userId' => $userId, + 'httpMethod' => $httpMethod, + 'uri' => $uri, + 'event' => $event, + 'headers' => $headers, + 'authMethod' => $authMethod, + 'authData' => $authData, + ] + ); + return $this->update($webhookListener); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function deleteById(int $id): bool { + $qb = $this->db->getQueryBuilder(); + + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return ($qb->executeStatement() > 0); + } + + /** + * @return list + * TODO cache + */ + public function getAllConfiguredEvents(): array { + $qb = $this->db->getQueryBuilder(); + + $qb->selectDistinct('event') + ->from($this->getTableName()); + + $result = $qb->executeQuery(); + + $configuredEvents = []; + + while (($event = $result->fetchOne()) !== false) { + $configuredEvents[] = $event; + } + + return $configuredEvents; + } + + public function getByEvent(string $event): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('event', $qb->createNamedParameter($event, IQueryBuilder::PARAM_STR))); + + return $this->findEntities($qb); + } +} diff --git a/apps/webhooks/lib/Listener/WebhooksEventListener.php b/apps/webhooks/lib/Listener/WebhooksEventListener.php new file mode 100644 index 0000000000000..8e935f1d0ce69 --- /dev/null +++ b/apps/webhooks/lib/Listener/WebhooksEventListener.php @@ -0,0 +1,64 @@ + + */ +class WebhooksEventListener implements IEventListener { + public function __construct( + private WebhookListenerMapper $mapper, + private IJobList $jobList, + private LoggerInterface $logger, + private ?string $userId, + ) { + } + + public function handle(Event $event): void { + $webhookListeners = $this->mapper->getByEvent($event::class); + + foreach ($webhookListeners as $webhookListener) { + $this->jobList->add(WebhookCall::class, [$this->serializeEvent($event), $this->userId, $webhookListener->getId(), time()]); + } + } + + private function serializeEvent(Event $event): array|\JsonSerializable { + if ($event instanceof \JsonSerializable) { + return $event; + } else { + /* Event is not serializable, we fallback to reflection to still send something */ + $data = ['class' => $event::class]; + $ref = new \ReflectionClass($event); + foreach ($ref->getMethods() as $method) { + if (str_starts_with($method->getName(), 'get')) { + $key = strtolower(substr($method->getName(), 3)); + $value = $method->invoke($event); + if ($value instanceof \OCP\Files\FileInfo) { + $value = [ + 'id' => $value->getId(), + 'path' => $value->getPath(), + ]; + } + $data[$key] = $value; + } + } + $this->logger->debug('Webhook had to use fallback to serialize event '.$event::class); + return $data; + } + } +} diff --git a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php new file mode 100755 index 0000000000000..18f8b545ddfb4 --- /dev/null +++ b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php @@ -0,0 +1,66 @@ +hasTable(WebhookListenerMapper::TABLE_NAME)) { + $table = $schema->createTable(WebhookListenerMapper::TABLE_NAME); + $table->addColumn('id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + ]); + $table->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('http_method', Types::STRING, [ + 'notnull' => true, + 'length' => 32, + ]); + $table->addColumn('uri', Types::STRING, [ + 'notnull' => true, + 'length' => 256, + ]); + $table->addColumn('event', Types::TEXT, [ + 'notnull' => true, + ]); + $table->addColumn('headers', Types::TEXT, [ + 'notnull' => false, + ]); + // TODO decide if string or int with an Enum + $table->addColumn('auth_method', Types::STRING, [ + 'notnull' => true, + 'length' => 16, + 'default' => '', + ]); + $table->addColumn('auth_data', Types::TEXT, [ + 'notnull' => false, + ]); + $table->setPrimaryKey(['id']); + return $schema; + } + return null; + } +} diff --git a/apps/webhooks/lib/ResponseDefinitions.php b/apps/webhooks/lib/ResponseDefinitions.php new file mode 100644 index 0000000000000..d730c0d9e2c86 --- /dev/null +++ b/apps/webhooks/lib/ResponseDefinitions.php @@ -0,0 +1,22 @@ +connection = \OCP\Server::get(IDBConnection::class); + $this->pruneTables(); + + $this->mapper = new WebhookListenerMapper( + $this->connection, + ); + } + + protected function tearDown(): void { + $this->pruneTables(); + parent::tearDown(); + } + + protected function pruneTables() { + $query = $this->connection->getQueryBuilder(); + $query->delete(WebhookListenerMapper::TABLE_NAME)->executeStatement(); + } + + public function testInsertListenerAndGetIt() { + $listener1 = $this->mapper->addWebhookListener( + 'bob', + 'POST', + 'https://webhook.example.com/endpoint', + UserCreatedEvent::class, + null, + null, + null, + ); + + $listener2 = $this->mapper->getById($listener1->getId()); + + $listener1->resetUpdatedFields(); + $this->assertEquals($listener1, $listener2); + } +} diff --git a/tests/enable_all.php b/tests/enable_all.php index b95f00f767e65..35217db079a96 100644 --- a/tests/enable_all.php +++ b/tests/enable_all.php @@ -24,3 +24,4 @@ function enableApp($app) { enableApp('federation'); enableApp('federatedfilesharing'); enableApp('admin_audit'); +enableApp('webhooks'); From cf265592d8ff7993dcbf912500dccf417a24c5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 30 May 2024 12:02:29 +0200 Subject: [PATCH 04/32] Revert "feat: Serialize event data and author userid to webhook" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 45d3bd6e30e112a89ffe2fcfdd3ba4bbdc384346. Signed-off-by: Côme Chilliet --- lib/private/EventDispatcher/WebhookCaller.php | 51 +++---------------- 1 file changed, 6 insertions(+), 45 deletions(-) diff --git a/lib/private/EventDispatcher/WebhookCaller.php b/lib/private/EventDispatcher/WebhookCaller.php index 0265ef317b447..2f0d1a2ab0617 100644 --- a/lib/private/EventDispatcher/WebhookCaller.php +++ b/lib/private/EventDispatcher/WebhookCaller.php @@ -28,14 +28,10 @@ use OCP\EventDispatcher\Event; use OCP\Http\Client\IClientService; -use OCP\IUserSession; -use Psr\Log\LoggerInterface; class WebhookCaller { public function __construct( private IClientService $clientService, - private IUserSession $userSession, - private LoggerInterface $logger, ) { } @@ -46,47 +42,12 @@ public function callWebhook( array $options, ): void { $client = $this->clientService->newClient(); - if (!isset($options['body'])) { - $options['body'] = json_encode([ - 'event' => $this->serializeEvent($event), - 'userid' => $this->userSession->getUser()?->getUID() ?? null, - ]); - } - try { - $response = $client->request($method, $uri, $options + ['query' => ['event' => $event::class]]); - $statusCode = $response->getStatusCode(); - if ($statusCode >= 200 && $statusCode < 300) { - $this->logger->warning('Webhook returned unexpected status code '.$statusCode, ['body' => $response->getBody()]); - } else { - $this->logger->debug('Webhook returned status code '.$statusCode, ['body' => $response->getBody()]); - } - } catch (\Exception $e) { - $this->logger->error('Webhook call failed: '.$e->getMessage(), ['exception' => $e]); - } - } + $client->request($method, $uri, $options + ['query' => ['event' => $event::class]]); - private function serializeEvent(Event $event): array|\JsonSerializable { - if ($event instanceof \JsonSerializable) { - return $event; - } else { - /* Event is not serializable, we fallback to reflection to still send something */ - $data = []; - $ref = new \ReflectionClass($event); - foreach ($ref->getMethods() as $method) { - if (str_starts_with($method->getName(), 'get')) { - $key = strtolower(substr($method->getName(), 3)); - $value = $method->invoke($event); - if ($value instanceof \OCP\Files\FileInfo) { - $value = [ - 'id' => $value->getId(), - 'path' => $value->getPath(), - ]; - } - $data[$key] = $value; - } - } - $this->logger->debug('Webhook had to use fallback to serialize event '.$event::class); - return $data; - } + /** + * TODO: + * Serialization of the event + * Timeout or async + */ } } From c3d4d2aad1539002e9ce31ad35e6df4801d93197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 30 May 2024 12:02:39 +0200 Subject: [PATCH 05/32] Revert "feat: Add support for webhook listeners" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3b790df0127b2bf95e8fe3a8460aa3813e58bef8. Signed-off-by: Côme Chilliet --- lib/composer/composer/autoload_classmap.php | 2 - lib/composer/composer/autoload_static.php | 2 - .../Bootstrap/RegistrationContext.php | 32 ---------- .../WebhookEventListenerRegistration.php | 61 ------------------- lib/private/EventDispatcher/WebhookCaller.php | 53 ---------------- .../Bootstrap/IRegistrationContext.php | 16 ----- 6 files changed, 166 deletions(-) delete mode 100644 lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php delete mode 100644 lib/private/EventDispatcher/WebhookCaller.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 7ae7b490f7dce..8e1408e121ef6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -866,7 +866,6 @@ 'OC\\AppFramework\\Bootstrap\\ServiceAliasRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceFactoryRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/ServiceRegistration.php', - 'OC\\AppFramework\\Bootstrap\\WebhookEventListenerRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php', 'OC\\AppFramework\\DependencyInjection\\DIContainer' => $baseDir . '/lib/private/AppFramework/DependencyInjection/DIContainer.php', 'OC\\AppFramework\\Http' => $baseDir . '/lib/private/AppFramework/Http.php', 'OC\\AppFramework\\Http\\Dispatcher' => $baseDir . '/lib/private/AppFramework/Http/Dispatcher.php', @@ -1383,7 +1382,6 @@ 'OC\\Encryption\\Util' => $baseDir . '/lib/private/Encryption/Util.php', 'OC\\EventDispatcher\\EventDispatcher' => $baseDir . '/lib/private/EventDispatcher/EventDispatcher.php', 'OC\\EventDispatcher\\ServiceEventListener' => $baseDir . '/lib/private/EventDispatcher/ServiceEventListener.php', - 'OC\\EventDispatcher\\WebhookCaller' => $baseDir . '/lib/private/EventDispatcher/WebhookCaller.php', 'OC\\EventSource' => $baseDir . '/lib/private/EventSource.php', 'OC\\EventSourceFactory' => $baseDir . '/lib/private/EventSourceFactory.php', 'OC\\Federation\\CloudFederationFactory' => $baseDir . '/lib/private/Federation/CloudFederationFactory.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 16b148dae5b5a..d6939ae36ce8b 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -907,7 +907,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\AppFramework\\Bootstrap\\ServiceAliasRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/ServiceAliasRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceFactoryRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/ServiceFactoryRegistration.php', 'OC\\AppFramework\\Bootstrap\\ServiceRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/ServiceRegistration.php', - 'OC\\AppFramework\\Bootstrap\\WebhookEventListenerRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php', 'OC\\AppFramework\\DependencyInjection\\DIContainer' => __DIR__ . '/../../..' . '/lib/private/AppFramework/DependencyInjection/DIContainer.php', 'OC\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http.php', 'OC\\AppFramework\\Http\\Dispatcher' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Dispatcher.php', @@ -1424,7 +1423,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Encryption\\Util' => __DIR__ . '/../../..' . '/lib/private/Encryption/Util.php', 'OC\\EventDispatcher\\EventDispatcher' => __DIR__ . '/../../..' . '/lib/private/EventDispatcher/EventDispatcher.php', 'OC\\EventDispatcher\\ServiceEventListener' => __DIR__ . '/../../..' . '/lib/private/EventDispatcher/ServiceEventListener.php', - 'OC\\EventDispatcher\\WebhookCaller' => __DIR__ . '/../../..' . '/lib/private/EventDispatcher/WebhookCaller.php', 'OC\\EventSource' => __DIR__ . '/../../..' . '/lib/private/EventSource.php', 'OC\\EventSourceFactory' => __DIR__ . '/../../..' . '/lib/private/EventSourceFactory.php', 'OC\\Federation\\CloudFederationFactory' => __DIR__ . '/../../..' . '/lib/private/Federation/CloudFederationFactory.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index b82f30e0fb661..df03d59ebfafe 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -81,9 +81,6 @@ class RegistrationContext { /** @var EventListenerRegistration[] */ private $eventListeners = []; - /** @var WebhookEventListenerRegistration[] */ - private $webhookEventListeners = []; - /** @var MiddlewareRegistration[] */ private $middlewares = []; @@ -224,17 +221,6 @@ public function registerEventListener(string $event, string $listener, int $prio ); } - public function registerWebhookEventListener(string $event, string $method, string $listenerUri, array $options = [], int $priority = 0): void { - $this->context->registerWebhookEventListener( - $this->appId, - $event, - $method, - $listenerUri, - $options, - $priority, - ); - } - public function registerMiddleware(string $class, bool $global = false): void { $this->context->registerMiddleware( $this->appId, @@ -465,10 +451,6 @@ public function registerEventListener(string $appId, string $event, string $list $this->eventListeners[] = new EventListenerRegistration($appId, $event, $listener, $priority); } - public function registerWebhookEventListener(string $appId, string $event, string $method, string $listenerUri, array $options, int $priority = 0): void { - $this->webhookEventListeners[] = new WebhookEventListenerRegistration($appId, $event, $method, $listenerUri, $options, $priority); - } - /** * @psalm-param class-string $class */ @@ -692,20 +674,6 @@ public function delegateEventListenerRegistrations(IEventDispatcher $eventDispat ]); } } - while (($registration = array_shift($this->webhookEventListeners)) !== null) { - try { - $eventDispatcher->addListener( - $registration->getEvent(), - $registration->getCallable(), - $registration->getPriority() - ); - } catch (Throwable $e) { - $appId = $registration->getAppId(); - $this->logger->error("Error during event webhook listener registration of $appId: " . $e->getMessage(), [ - 'exception' => $e, - ]); - } - } } /** diff --git a/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php b/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php deleted file mode 100644 index 3b283c6029d39..0000000000000 --- a/lib/private/AppFramework/Bootstrap/WebhookEventListenerRegistration.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * @author Côme Chilliet - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OC\AppFramework\Bootstrap; - -use OC\EventDispatcher\WebhookCaller; -use OCP\EventDispatcher\Event; -use OCP\Server; - -/** - * @psalm-immutable - */ -class WebhookEventListenerRegistration extends ARegistration { - public function __construct( - string $appId, - private string $event, - private string $method, - private string $uri, - private array $options, - private int $priority, - ) { - parent::__construct($appId); - } - - public function getEvent(): string { - return $this->event; - } - - public function getCallable(): callable { - return function (Event $event) { - Server::get(WebhookCaller::class)->callWebhook($event, $this->method, $this->uri, $this->options); - }; - } - - public function getPriority(): int { - return $this->priority; - } -} diff --git a/lib/private/EventDispatcher/WebhookCaller.php b/lib/private/EventDispatcher/WebhookCaller.php deleted file mode 100644 index 2f0d1a2ab0617..0000000000000 --- a/lib/private/EventDispatcher/WebhookCaller.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * @author Côme Chilliet - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OC\EventDispatcher; - -use OCP\EventDispatcher\Event; -use OCP\Http\Client\IClientService; - -class WebhookCaller { - public function __construct( - private IClientService $clientService, - ) { - } - - public function callWebhook( - Event $event, - string $method, - string $uri, - array $options, - ): void { - $client = $this->clientService->newClient(); - $client->request($method, $uri, $options + ['query' => ['event' => $event::class]]); - - /** - * TODO: - * Serialization of the event - * Timeout or async - */ - } -} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 17040ccfb4fa2..b86f7bcd76d71 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -120,22 +120,6 @@ public function registerParameter(string $name, $value): void; */ public function registerEventListener(string $event, string $listener, int $priority = 0): void; - /** - * Register a webhook listener - * - * @psalm-template T of \OCP\EventDispatcher\Event - * @param class-string $event The fully-qualified class name of the Event sub class to listen for - * @param string $method The HTTP method to use (usually 'GET' or 'POST') - * @param string $listenerUri The absolute URI to contact - * @param array $options Additional options for the request, {@see \OCP\Http\Client::request()} - * @param int $priority The higher this value, the earlier an event - * listener will be triggered in the chain (defaults to 0) - * - * @since 30.0.0 - */ - public function registerWebhookEventListener(string $event, string $method, string $listenerUri, array $options = [], int $priority = 0): void; - - /** * @param string $class * @param bool $global load this middleware also for requests of other apps? Added in Nextcloud 26 From 734aad8934cdbe072c7c880482e5caf88b61400b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 30 May 2024 14:32:26 +0200 Subject: [PATCH 06/32] feat: Make node events serializable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/public/EventDispatcher/JsonSerializer.php | 42 +++++++++++++++++++ .../Files/Events/Node/AbstractNodeEvent.php | 13 +++++- .../Files/Events/Node/AbstractNodesEvent.php | 12 ++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 lib/public/EventDispatcher/JsonSerializer.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 8e1408e121ef6..0fe1314644fe6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -281,6 +281,7 @@ 'OCP\\EventDispatcher\\GenericEvent' => $baseDir . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => $baseDir . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => $baseDir . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\EventDispatcher\\JsonSerializer' => $baseDir . '/lib/public/EventDispatcher/JsonSerializer.php', 'OCP\\Exceptions\\AbortedEventException' => $baseDir . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Exceptions\\AppConfigException' => $baseDir . '/lib/public/Exceptions/AppConfigException.php', 'OCP\\Exceptions\\AppConfigIncorrectTypeException' => $baseDir . '/lib/public/Exceptions/AppConfigIncorrectTypeException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d6939ae36ce8b..cf52b9026f11a 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -322,6 +322,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\EventDispatcher\\GenericEvent' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\EventDispatcher\\JsonSerializer' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/JsonSerializer.php', 'OCP\\Exceptions\\AbortedEventException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Exceptions\\AppConfigException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AppConfigException.php', 'OCP\\Exceptions\\AppConfigIncorrectTypeException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AppConfigIncorrectTypeException.php', diff --git a/lib/public/EventDispatcher/JsonSerializer.php b/lib/public/EventDispatcher/JsonSerializer.php new file mode 100644 index 0000000000000..1eb75ed7527e2 --- /dev/null +++ b/lib/public/EventDispatcher/JsonSerializer.php @@ -0,0 +1,42 @@ + $node->getId(), + 'path' => $node->getPath(), + ]; + } + + /** + * @since 30.0.0 + */ + public static function serializeUser(IUser $user): array { + return [ + 'uid' => $user->getUID(), + 'displayName' => $user->getDisplayName(), + ]; + } +} diff --git a/lib/public/Files/Events/Node/AbstractNodeEvent.php b/lib/public/Files/Events/Node/AbstractNodeEvent.php index 1b290578ab940..cc27bdd8cfdb3 100644 --- a/lib/public/Files/Events/Node/AbstractNodeEvent.php +++ b/lib/public/Files/Events/Node/AbstractNodeEvent.php @@ -9,12 +9,13 @@ namespace OCP\Files\Events\Node; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\JsonSerializer; use OCP\Files\Node; /** * @since 20.0.0 */ -abstract class AbstractNodeEvent extends Event { +abstract class AbstractNodeEvent extends Event implements \JsonSerializable { /** * @since 20.0.0 */ @@ -29,4 +30,14 @@ public function __construct( public function getNode(): Node { return $this->node; } + + /** + * @since 30.0.0 + */ + public function jsonSerialize(): array { + return [ + 'class' => static::class, + 'node' => JsonSerializer::serializeFileInfo($this->node), + ]; + } } diff --git a/lib/public/Files/Events/Node/AbstractNodesEvent.php b/lib/public/Files/Events/Node/AbstractNodesEvent.php index a5b058f18f55e..b5fd1b75898f8 100644 --- a/lib/public/Files/Events/Node/AbstractNodesEvent.php +++ b/lib/public/Files/Events/Node/AbstractNodesEvent.php @@ -9,6 +9,7 @@ namespace OCP\Files\Events\Node; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\JsonSerializer; use OCP\Files\Node; /** @@ -37,4 +38,15 @@ public function getSource(): Node { public function getTarget(): Node { return $this->target; } + + /** + * @since 30.0.0 + */ + public function jsonSerialize(): array { + return [ + 'class' => static::class, + 'source' => JsonSerializer::serializeFileInfo($this->source), + 'target' => JsonSerializer::serializeFileInfo($this->target), + ]; + } } From 3b64e525fbbea479887a7b57307c8d53aea3b3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 30 May 2024 14:42:41 +0200 Subject: [PATCH 07/32] feat: Add command to list registered webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/appinfo/info.xml | 4 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/webhooks/lib/Command/Index.php | 83 +++++-------------- 4 files changed, 26 insertions(+), 63 deletions(-) diff --git a/apps/webhooks/appinfo/info.xml b/apps/webhooks/appinfo/info.xml index 8e1d706d06f9f..9261ed646efa3 100644 --- a/apps/webhooks/appinfo/info.xml +++ b/apps/webhooks/appinfo/info.xml @@ -22,4 +22,8 @@ + + + OCA\Webhooks\Command\Index + diff --git a/apps/webhooks/composer/composer/autoload_classmap.php b/apps/webhooks/composer/composer/autoload_classmap.php index 64646e93a023c..f8ea080f69108 100644 --- a/apps/webhooks/composer/composer/autoload_classmap.php +++ b/apps/webhooks/composer/composer/autoload_classmap.php @@ -9,6 +9,7 @@ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\Webhooks\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php', + 'OCA\\Webhooks\\Command\\Index' => $baseDir . '/../lib/Command/Index.php', 'OCA\\Webhooks\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php', 'OCA\\Webhooks\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php', 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php', diff --git a/apps/webhooks/composer/composer/autoload_static.php b/apps/webhooks/composer/composer/autoload_static.php index ef878885cabf7..684e0d96464b7 100644 --- a/apps/webhooks/composer/composer/autoload_static.php +++ b/apps/webhooks/composer/composer/autoload_static.php @@ -24,6 +24,7 @@ class ComposerStaticInitWebhooks 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\Webhooks\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php', + 'OCA\\Webhooks\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php', 'OCA\\Webhooks\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php', 'OCA\\Webhooks\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php', 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php', diff --git a/apps/webhooks/lib/Command/Index.php b/apps/webhooks/lib/Command/Index.php index 4f418b4ffc798..900140c8f964c 100644 --- a/apps/webhooks/lib/Command/Index.php +++ b/apps/webhooks/lib/Command/Index.php @@ -3,81 +3,38 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Arthur Schiwon - * - * @author Arthur Schiwon - * @author Joas Schilling - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\WorkflowEngine\Command; -use OCA\WorkflowEngine\Helper\ScopeContext; -use OCA\WorkflowEngine\Manager; -use OCP\WorkflowEngine\IManager; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; +namespace OCA\Webhooks\Command; + +use OC\Core\Command\Base; +use OCA\Webhooks\Db\WebhookListener; +use OCA\Webhooks\Db\WebhookListenerMapper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class Index extends Command { - - /** @var Manager */ - private $manager; - - public function __construct(Manager $manager) { - $this->manager = $manager; +class Index extends Base { + public function __construct( + private WebhookListenerMapper $mapper, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { + parent::configure(); $this - ->setName('workflows:list') - ->setDescription('Lists configured workflows') - ->addArgument( - 'scope', - InputArgument::OPTIONAL, - 'Lists workflows for "admin", "user"', - 'admin' - ) - ->addArgument( - 'scopeId', - InputArgument::OPTIONAL, - 'User IDs when the scope is "user"', - null - ); - } - - protected function mappedScope(string $scope): int { - static $scopes = [ - 'admin' => IManager::SCOPE_ADMIN, - 'user' => IManager::SCOPE_USER, - ]; - return $scopes[$scope] ?? -1; + ->setName('webhooks:list') + ->setDescription('Lists configured webhooks'); } protected function execute(InputInterface $input, OutputInterface $output): int { - $ops = $this->manager->getAllOperations( - new ScopeContext( - $this->mappedScope($input->getArgument('scope')), - $input->getArgument('scopeId') - ) + $webhookListeners = array_map( + fn (WebhookListener $listener) => $listener->jsonSerialize(), + $this->mapper->getAll() ); - $output->writeln(\json_encode($ops)); - return 0; + $this->writeTableInOutputFormat($input, $output, $webhookListeners); + return static::SUCCESS; } } From 5dd9c2f8e85816f5c588d9539ec33b97d111287e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 30 May 2024 16:46:32 +0200 Subject: [PATCH 08/32] feat: Add admin delegation for webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/appinfo/info.xml | 4 ++ .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/Controller/WebhooksController.php | 6 ++ apps/webhooks/lib/Settings/Admin.php | 60 +++++++++++++++++++ 5 files changed, 72 insertions(+) create mode 100644 apps/webhooks/lib/Settings/Admin.php diff --git a/apps/webhooks/appinfo/info.xml b/apps/webhooks/appinfo/info.xml index 9261ed646efa3..6b9a809dba0df 100644 --- a/apps/webhooks/appinfo/info.xml +++ b/apps/webhooks/appinfo/info.xml @@ -26,4 +26,8 @@ OCA\Webhooks\Command\Index + + + OCA\Webhooks\Settings\Admin + diff --git a/apps/webhooks/composer/composer/autoload_classmap.php b/apps/webhooks/composer/composer/autoload_classmap.php index f8ea080f69108..3411d0b6b961a 100644 --- a/apps/webhooks/composer/composer/autoload_classmap.php +++ b/apps/webhooks/composer/composer/autoload_classmap.php @@ -16,4 +16,5 @@ 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php', 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => $baseDir . '/../lib/Migration/Version1000Date20240527153425.php', 'OCA\\Webhooks\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', + 'OCA\\Webhooks\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', ); diff --git a/apps/webhooks/composer/composer/autoload_static.php b/apps/webhooks/composer/composer/autoload_static.php index 684e0d96464b7..e631fdfb97594 100644 --- a/apps/webhooks/composer/composer/autoload_static.php +++ b/apps/webhooks/composer/composer/autoload_static.php @@ -31,6 +31,7 @@ class ComposerStaticInitWebhooks 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php', 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20240527153425.php', 'OCA\\Webhooks\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', + 'OCA\\Webhooks\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index b5aa14449ecdb..9d4c7c702794e 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -12,6 +12,7 @@ use Doctrine\DBAL\Exception; use OCA\Webhooks\Db\WebhookListenerMapper; use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; @@ -44,6 +45,7 @@ public function __construct( * 200: Webhook registrations returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks')] + #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] public function index(): DataResponse { $webhookListeners = $this->mapper->getAll(); @@ -60,6 +62,7 @@ public function index(): DataResponse { * 200: Webhook registration returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks/{id}')] + #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] public function show(int $id): DataResponse { return new DataResponse($this->mapper->getById($id)); } @@ -83,6 +86,7 @@ public function show(int $id): DataResponse { * @throws OCSException Other error */ #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks')] + #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] public function create( string $httpMethod, string $uri, @@ -132,6 +136,7 @@ public function create( * @throws OCSException Other error */ #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks/{id}')] + #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] public function update( int $id, string $httpMethod, @@ -177,6 +182,7 @@ public function update( * @throws OCSException Other error */ #[ApiRoute(verb: 'DELETE', url: '/api/v1/webhooks/{id}')] + #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] public function destroy(int $id): DataResponse { try { $deleted = $this->mapper->deleteById($id); diff --git a/apps/webhooks/lib/Settings/Admin.php b/apps/webhooks/lib/Settings/Admin.php new file mode 100644 index 0000000000000..748b8536e4f61 --- /dev/null +++ b/apps/webhooks/lib/Settings/Admin.php @@ -0,0 +1,60 @@ +appName, '') extends TemplateResponse { + public function render(): string { + return ''; + } + }; + } + + public function getSection(): ?string { + return 'admindelegation'; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority(): int { + return 0; + } + + public function getName(): string { + return $this->l10n->t('Webhooks'); + } + + public function getAuthorizedAppConfig(): array { + return []; + } +} From 144bdd73f9ca96174d7de8664b4026b65d3bdf07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 30 May 2024 17:26:40 +0200 Subject: [PATCH 09/32] feat: Add event filtering to webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/BackgroundJobs/WebhookCall.php | 7 +- apps/webhooks/lib/Command/Index.php | 6 +- .../lib/Controller/WebhooksController.php | 4 + apps/webhooks/lib/Db/WebhookListener.php | 1 + .../webhooks/lib/Db/WebhookListenerMapper.php | 4 + .../lib/Listener/WebhooksEventListener.php | 30 +- .../Version1000Date20240527153425.php | 3 + apps/webhooks/lib/Service/PHPMongoQuery.php | 340 ++++++++++++++++++ .../tests/Db/WebhookListenerMapperTest.php | 1 + 11 files changed, 390 insertions(+), 8 deletions(-) create mode 100644 apps/webhooks/lib/Service/PHPMongoQuery.php diff --git a/apps/webhooks/composer/composer/autoload_classmap.php b/apps/webhooks/composer/composer/autoload_classmap.php index 3411d0b6b961a..efd4a43d58ae3 100644 --- a/apps/webhooks/composer/composer/autoload_classmap.php +++ b/apps/webhooks/composer/composer/autoload_classmap.php @@ -16,5 +16,6 @@ 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php', 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => $baseDir . '/../lib/Migration/Version1000Date20240527153425.php', 'OCA\\Webhooks\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', + 'OCA\\Webhooks\\Service\\PHPMongoQuery' => $baseDir . '/../lib/Service/PHPMongoQuery.php', 'OCA\\Webhooks\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', ); diff --git a/apps/webhooks/composer/composer/autoload_static.php b/apps/webhooks/composer/composer/autoload_static.php index e631fdfb97594..75182423ae6ee 100644 --- a/apps/webhooks/composer/composer/autoload_static.php +++ b/apps/webhooks/composer/composer/autoload_static.php @@ -31,6 +31,7 @@ class ComposerStaticInitWebhooks 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php', 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20240527153425.php', 'OCA\\Webhooks\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', + 'OCA\\Webhooks\\Service\\PHPMongoQuery' => __DIR__ . '/..' . '/../lib/Service/PHPMongoQuery.php', 'OCA\\Webhooks\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', ); diff --git a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php index 9b113a5c1fc09..469b554a886d4 100644 --- a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php +++ b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php @@ -26,14 +26,11 @@ public function __construct( } protected function run($argument): void { - [$event, $userId, $webhookId] = $argument; + [$data, $webhookId] = $argument; $webhookListener = $this->mapper->getById($webhookId); $client = $this->clientService->newClient(); $options = []; - $options['body'] = json_encode([ - 'event' => $event, - 'userid' => $userId, - ]); + $options['body'] = json_encode($data); try { $response = $client->request($webhookListener->getHttpMethod(), $webhookListener->getUri(), $options); $statusCode = $response->getStatusCode(); diff --git a/apps/webhooks/lib/Command/Index.php b/apps/webhooks/lib/Command/Index.php index 900140c8f964c..78feda3ec68ec 100644 --- a/apps/webhooks/lib/Command/Index.php +++ b/apps/webhooks/lib/Command/Index.php @@ -31,7 +31,11 @@ protected function configure(): void { protected function execute(InputInterface $input, OutputInterface $output): int { $webhookListeners = array_map( - fn (WebhookListener $listener) => $listener->jsonSerialize(), + function (WebhookListener $listener): array { + $data = $listener->jsonSerialize(); + $data['eventFilter'] = json_encode($data['eventFilter']); + return $data; + }, $this->mapper->getAll() ); $this->writeTableInOutputFormat($input, $output, $webhookListeners); diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index 9d4c7c702794e..46da32fd6e276 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -91,6 +91,7 @@ public function create( string $httpMethod, string $uri, string $event, + ?array $eventFilter, ?array $headers, ?string $authMethod, ?array $authData, @@ -101,6 +102,7 @@ public function create( $httpMethod, $uri, $event, + $eventFilter, $headers, $authMethod, $authData, @@ -142,6 +144,7 @@ public function update( string $httpMethod, string $uri, string $event, + ?array $eventFilter, ?array $headers, ?string $authMethod, ?array $authData, @@ -153,6 +156,7 @@ public function update( $httpMethod, $uri, $event, + $eventFilter, $headers, $authMethod, $authData, diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhooks/lib/Db/WebhookListener.php index 0d731e72d7f16..a52042aa0a2d1 100644 --- a/apps/webhooks/lib/Db/WebhookListener.php +++ b/apps/webhooks/lib/Db/WebhookListener.php @@ -42,6 +42,7 @@ public function __construct() { $this->addType('httpMethod', 'string'); $this->addType('uri', 'string'); $this->addType('event', 'string'); + $this->addType('eventFilter', 'json'); $this->addType('headers', 'json'); $this->addType('authMethod', 'string'); $this->addType('authData', 'json'); diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index 773358d671c0a..5ce824893abcf 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -61,6 +61,7 @@ public function addWebhookListener( string $httpMethod, string $uri, string $event, + ?array $eventFilter, ?array $headers, ?string $authMethod, ?array $authData, @@ -71,6 +72,7 @@ public function addWebhookListener( 'httpMethod' => $httpMethod, 'uri' => $uri, 'event' => $event, + 'eventFilter' => $eventFilter ?? [], 'headers' => $headers, 'authMethod' => $authMethod ?? 'none', 'authData' => $authData, @@ -85,6 +87,7 @@ public function updateWebhookListener( string $httpMethod, string $uri, string $event, + ?array $eventFilter, ?array $headers, ?string $authMethod, ?array $authData, @@ -96,6 +99,7 @@ public function updateWebhookListener( 'httpMethod' => $httpMethod, 'uri' => $uri, 'event' => $event, + 'eventFilter' => $eventFilter ?? [], 'headers' => $headers, 'authMethod' => $authMethod, 'authData' => $authData, diff --git a/apps/webhooks/lib/Listener/WebhooksEventListener.php b/apps/webhooks/lib/Listener/WebhooksEventListener.php index 8e935f1d0ce69..5ea0012d4e9cd 100644 --- a/apps/webhooks/lib/Listener/WebhooksEventListener.php +++ b/apps/webhooks/lib/Listener/WebhooksEventListener.php @@ -11,9 +11,12 @@ use OCA\Webhooks\BackgroundJobs\WebhookCall; use OCA\Webhooks\Db\WebhookListenerMapper; +use OCA\Webhooks\Service\PHPMongoQuery; use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\EventDispatcher\JsonSerializer; +use OCP\IUserSession; use Psr\Log\LoggerInterface; /** @@ -25,15 +28,31 @@ public function __construct( private WebhookListenerMapper $mapper, private IJobList $jobList, private LoggerInterface $logger, - private ?string $userId, + private IUserSession $userSession, ) { } public function handle(Event $event): void { $webhookListeners = $this->mapper->getByEvent($event::class); + /** @var IUser */ + $user = $this->userSession->getUser(); foreach ($webhookListeners as $webhookListener) { - $this->jobList->add(WebhookCall::class, [$this->serializeEvent($event), $this->userId, $webhookListener->getId(), time()]); + // TODO add group membership to be able to filter on it + $data = [ + 'event' => $this->serializeEvent($event), + 'user' => JsonSerializer::serializeUser($user), + 'time' => time(), + ]; + if ($this->filterMatch($webhookListener->getEventFilter(), $data)) { + $this->jobList->add( + WebhookCall::class, + [ + $data, + $webhookListener->getId(), + ] + ); + } } } @@ -61,4 +80,11 @@ private function serializeEvent(Event $event): array|\JsonSerializable { return $data; } } + + private function filterMatch(array $filter, array $data): bool { + if ($filter === []) { + return true; + } + return PHPMongoQuery::executeQuery($filter, $data); + } } diff --git a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php index 18f8b545ddfb4..b6c345a22e2a9 100755 --- a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php +++ b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php @@ -46,6 +46,9 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('event', Types::TEXT, [ 'notnull' => true, ]); + $table->addColumn('event_filter', Types::TEXT, [ + 'notnull' => false, + ]); $table->addColumn('headers', Types::TEXT, [ 'notnull' => false, ]); diff --git a/apps/webhooks/lib/Service/PHPMongoQuery.php b/apps/webhooks/lib/Service/PHPMongoQuery.php new file mode 100644 index 0000000000000..65ba57757634a --- /dev/null +++ b/apps/webhooks/lib/Service/PHPMongoQuery.php @@ -0,0 +1,340 @@ +debug('executeQuery called', ['query' => $query, 'document' => $document, 'options' => $options]); + } + + if(!is_array($query)) { + return (bool)$query; + } + + return self::_executeQuery($query, $document, $options); + } + + /** + * Internal execute query + * + * This expects an array from the query and has an additional logical operator (for the root query object the logical operator is always $and so this is not required) + * + * @throws Exception + */ + private static function _executeQuery(array $query, array &$document, array $options = [], string $logicalOperator = '$and'): bool { + if($logicalOperator !== '$and' && (!count($query) || !isset($query[0]))) { + throw new Exception($logicalOperator.' requires nonempty array'); + } + if($options['_debug'] && $options['_shouldLog']) { + $options['logger']->debug('_executeQuery called', ['query' => $query, 'document' => $document, 'logicalOperator' => $logicalOperator]); + } + + // for the purpose of querying documents, we are going to specify that an indexed array is an array which + // only contains numeric keys, is sequential, the first key is zero, and not empty. This will allow us + // to detect an array of key->vals that have numeric IDs vs an array of queries (where keys were not specified) + $queryIsIndexedArray = !empty($query) && array_is_list($query); + + foreach($query as $k => $q) { + $pass = true; + if(is_string($k) && substr($k, 0, 1) === '$') { + // key is an operator at this level, except $not, which can be at any level + if($k === '$not') { + $pass = !self::_executeQuery($q, $document, $options); + } else { + $pass = self::_executeQuery($q, $document, $options, $k); + } + } elseif($logicalOperator === '$and') { // special case for $and + if($queryIsIndexedArray) { // $q is an array of query objects + $pass = self::_executeQuery($q, $document, $options); + } elseif(is_array($q)) { // query is array, run all queries on field. All queries must match. e.g { 'age': { $gt: 24, $lt: 52 } } + $pass = self::_executeQueryOnElement($q, $k, $document, $options); + } else { + // key value means equality + $pass = self::_executeOperatorOnElement('$e', $q, $k, $document, $options); + } + } else { // $q is array of query objects e.g '$or' => [{'fullName' => 'Nick'}] + $pass = self::_executeQuery($q, $document, $options, '$and'); + } + switch($logicalOperator) { + case '$and': // if any fail, query fails + if(!$pass) { + return false; + } + break; + case '$or': // if one succeeds, query succeeds + if($pass) { + return true; + } + break; + case '$nor': // if one succeeds, query fails + if($pass) { + return false; + } + break; + default: + if($options['_shouldLog']) { + $options['logger']->warning('_executeQuery could not find logical operator', ['query' => $query, 'document' => $document, 'logicalOperator' => $logicalOperator]); + } + return false; + } + } + switch($logicalOperator) { + case '$and': // all succeeded, query succeeds + return true; + case '$or': // all failed, query fails + return false; + case '$nor': // all failed, query succeeded + return true; + default: + if($options['_shouldLog']) { + $options['logger']->warning('_executeQuery could not find logical operator', ['query' => $query, 'document' => $document, 'logicalOperator' => $logicalOperator]); + } + return false; + } + } + + /** + * Execute a query object on an element + * + * @throws Exception + */ + private static function _executeQueryOnElement(array $query, string $element, array &$document, array $options = []): bool { + if($options['_debug'] && $options['_shouldLog']) { + $options['logger']->debug('_executeQueryOnElement called', ['query' => $query, 'element' => $element, 'document' => $document]); + } + // iterate through query operators + foreach($query as $op => $opVal) { + if(!self::_executeOperatorOnElement($op, $opVal, $element, $document, $options)) { + return false; + } + } + return true; + } + + /** + * Check if an operator is equal to a value + * + * Equality includes direct equality, regular expression match, and checking if the operator value is one of the values in an array value + * + * @param mixed $v + * @param mixed $operatorValue + */ + private static function _isEqual($v, $operatorValue): bool { + if (is_array($v) && is_array($operatorValue)) { + return $v == $operatorValue; + } + if(is_array($v)) { + return in_array($operatorValue, $v); + } + if(is_string($operatorValue) && preg_match('/^\/(.*?)\/([a-z]*)$/i', $operatorValue, $matches)) { + return (bool)preg_match('/'.$matches[1].'/'.$matches[2], $v); + } + return $operatorValue === $v; + } + + /** + * Execute a Mongo Operator on an element + * + * @param string $operator The operator to perform + * @param mixed $operatorValue The value to provide the operator + * @param string $element The target element. Can be an object path eg price.shoes + * @param array $document The document in which to find the element + * @param array $options Options + * @throws Exception Exceptions on invalid operators, invalid unknown operator callback, and invalid operator values + */ + private static function _executeOperatorOnElement(string $operator, $operatorValue, string $element, array &$document, array $options = []): bool { + if($options['_debug'] && $options['_shouldLog']) { + $options['logger']->debug('_executeOperatorOnElement called', ['operator' => $operator, 'operatorValue' => $operatorValue, 'element' => $element, 'document' => $document]); + } + + if($operator === '$not') { + return !self::_executeQueryOnElement($operatorValue, $element, $document, $options); + } + + $elementSpecifier = explode('.', $element); + $v = & $document; + $exists = true; + foreach($elementSpecifier as $index => $es) { + if(empty($v)) { + $exists = false; + break; + } + if(isset($v[0])) { + // value from document is an array, so we need to iterate through array and test the query on all elements of the array + // if any elements match, then return true + $newSpecifier = implode('.', array_slice($elementSpecifier, $index)); + foreach($v as $item) { + if(self::_executeOperatorOnElement($operator, $operatorValue, $newSpecifier, $item, $options)) { + return true; + } + } + return false; + } + if(isset($v[$es])) { + $v = & $v[$es]; + } else { + $exists = false; + break; + } + } + + switch($operator) { + case '$all': + if(!$exists) { + return false; + } + if(!is_array($operatorValue)) { + throw new Exception('$all requires array'); + } + if(count($operatorValue) === 0) { + return false; + } + if(!is_array($v)) { + if(count($operatorValue) === 1) { + return $v === $operatorValue[0]; + } + return false; + } + return count(array_intersect($v, $operatorValue)) === count($operatorValue); + case '$e': + if(!$exists) { + return false; + } + return self::_isEqual($v, $operatorValue); + case '$in': + if(!$exists) { + return false; + } + if(!is_array($operatorValue)) { + throw new Exception('$in requires array'); + } + if(count($operatorValue) === 0) { + return false; + } + if(is_array($v)) { + return count(array_intersect($v, $operatorValue)) > 0; + } + return in_array($v, $operatorValue); + case '$lt': return $exists && $v < $operatorValue; + case '$lte': return $exists && $v <= $operatorValue; + case '$gt': return $exists && $v > $operatorValue; + case '$gte': return $exists && $v >= $operatorValue; + case '$ne': return (!$exists && $operatorValue !== null) || ($exists && !self::_isEqual($v, $operatorValue)); + case '$nin': + if(!$exists) { + return true; + } + if(!is_array($operatorValue)) { + throw new Exception('$nin requires array'); + } + if(count($operatorValue) === 0) { + return true; + } + if(is_array($v)) { + return count(array_intersect($v, $operatorValue)) === 0; + } + return !in_array($v, $operatorValue); + + case '$exists': return ($operatorValue && $exists) || (!$operatorValue && !$exists); + case '$mod': + if(!$exists) { + return false; + } + if(!is_array($operatorValue)) { + throw new Exception('$mod requires array'); + } + if(count($operatorValue) !== 2) { + throw new Exception('$mod requires two parameters in array: divisor and remainder'); + } + return $v % $operatorValue[0] === $operatorValue[1]; + + default: + if(empty($options['unknownOperatorCallback']) || !is_callable($options['unknownOperatorCallback'])) { + throw new Exception('Operator '.$operator.' is unknown'); + } + + $res = call_user_func($options['unknownOperatorCallback'], $operator, $operatorValue, $element, $document); + if($res === null) { + throw new Exception('Operator '.$operator.' is unknown'); + } + if(!is_bool($res)) { + throw new Exception('Return value of unknownOperatorCallback must be boolean, actual value '.$res); + } + return $res; + } + throw new Exception('Didn\'t return in switch'); + } + + /** + * Get the fields this query depends on + * + * @param array query The query to analyse + * @return array An array of fields this query depends on + */ + public static function getDependentFields(array $query) { + $fields = []; + foreach($query as $k => $v) { + if(is_array($v)) { + $fields = array_merge($fields, static::getDependentFields($v)); + } + if(is_int($k) || $k[0] === '$') { + continue; + } + $fields[] = $k; + } + return array_unique($fields); + } +} diff --git a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php index eca6b750ae0f8..4481eb6661eca 100644 --- a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php +++ b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php @@ -51,6 +51,7 @@ public function testInsertListenerAndGetIt() { null, null, null, + null, ); $listener2 = $this->mapper->getById($listener1->getId()); From 7fe3f1cc7052e69acb3be6d53645f0c7736ee214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 30 May 2024 18:23:09 +0200 Subject: [PATCH 10/32] fix: Add event filter to openapi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Somehow this does not work in the ocs_api_viewer app, so we should look into better specifying format for eventFilter. Ideally it should also be set as in body, not query. Signed-off-by: Côme Chilliet --- .../lib/Controller/WebhooksController.php | 2 ++ apps/webhooks/lib/Db/WebhookListener.php | 3 +++ apps/webhooks/lib/ResponseDefinitions.php | 4 ++++ apps/webhooks/openapi.json | 23 ++++++++++++++++++- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index 46da32fd6e276..21a06f5e563c6 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -73,6 +73,7 @@ public function show(int $id): DataResponse { * @param string $httpMethod HTTP method to use to contact the webhook * @param string $uri Webhook URI endpoint * @param string $event Event class name to listen to + * @param ?array $eventFilter Mongo filter to apply to the serialized data to decide if firing * @param ?array $headers Array of headers to send * @param ?string $authMethod Authentication method to use. TODO * @param ?array $authData Array of data for authentication @@ -125,6 +126,7 @@ public function create( * @param string $httpMethod HTTP method to use to contact the webhook * @param string $uri Webhook URI endpoint * @param string $event Event class name to listen to + * @param ?array $eventFilter Mongo filter to apply to the serialized data to decide if firing * @param ?array $headers Array of headers to send * @param ?string $authMethod Authentication method to use. TODO * @param ?array $authData Array of data for authentication diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhooks/lib/Db/WebhookListener.php index a52042aa0a2d1..8e16249b678ab 100644 --- a/apps/webhooks/lib/Db/WebhookListener.php +++ b/apps/webhooks/lib/Db/WebhookListener.php @@ -28,6 +28,9 @@ class WebhookListener extends Entity implements \JsonSerializable { /** @var string */ protected $event; + /** @var array */ + protected $eventFilter; + /** @var ?string */ protected $headers; diff --git a/apps/webhooks/lib/ResponseDefinitions.php b/apps/webhooks/lib/ResponseDefinitions.php index d730c0d9e2c86..a8c188d2a5917 100644 --- a/apps/webhooks/lib/ResponseDefinitions.php +++ b/apps/webhooks/lib/ResponseDefinitions.php @@ -16,6 +16,10 @@ * httpMethod: string, * uri: string, * event?: string, + * eventFilter?: array, + * headers?: array, + * authMethod: string, + * authData?: array, * } */ class ResponseDefinitions { diff --git a/apps/webhooks/openapi.json b/apps/webhooks/openapi.json index ada0d7fb9c7cf..eeed54d780569 100644 --- a/apps/webhooks/openapi.json +++ b/apps/webhooks/openapi.json @@ -180,6 +180,18 @@ "type": "string" } }, + { + "name": "eventFilter[]", + "in": "query", + "description": "Mongo filter to apply to the serialized data to decide if firing", + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "object" + } + } + }, { "name": "headers", "in": "query", @@ -422,6 +434,15 @@ "type": "string" } }, + { + "name": "eventFilter", + "in": "query", + "description": "Mongo filter to apply to the serialized data to decide if firing", + "schema": { + "type": "string", + "nullable": true + } + }, { "name": "headers", "in": "query", @@ -688,4 +709,4 @@ } }, "tags": [] -} \ No newline at end of file +} From 75b2ed4c79f187114d66a299f11266b325f6baa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 3 Jun 2024 15:32:26 +0200 Subject: [PATCH 11/32] chore: Add tests for the PHPMongoQuery class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It appears that it does not match Mongo current documentation exactly so we should look into adapting it. Having equality autodetect regex is a bit weird. Signed-off-by: Côme Chilliet --- .../tests/Service/PHPMongoQueryTest.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 apps/webhooks/tests/Service/PHPMongoQueryTest.php diff --git a/apps/webhooks/tests/Service/PHPMongoQueryTest.php b/apps/webhooks/tests/Service/PHPMongoQueryTest.php new file mode 100644 index 0000000000000..06658b718f003 --- /dev/null +++ b/apps/webhooks/tests/Service/PHPMongoQueryTest.php @@ -0,0 +1,47 @@ + [ + 'class' => NodeWrittenEvent::class, + 'node' => [ + 'id' => 23, + 'path' => '/tmp/file.txt', + ], + ], + 'user' => [ + 'uid' => 'bob', + ], + ]; + return [ + [[], [], true], + [[], $event, true], + [['event.class' => NodeWrittenEvent::class], $event, true], + [['event.class' => NodeWrittenEvent::class, 'user.uid' => 'bob'], $event, true], + [['event.node.path' => '/.txt$/'], $event, true], + [['event.node.id' => ['$gte' => 22]], $event, true], + [['event.class' => 'SomethingElse'], $event, false], + ]; + } + + /** + * @dataProvider dataExecuteQuery + */ + public function testExecuteQuery(array $query, array $document, bool $matches) { + $this->assertEquals($matches, PHPMongoQuery::executeQuery($query, $document)); + } +} From 261f08e631f0442f4077c0936c4b784e257830b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 3 Jun 2024 16:10:55 +0200 Subject: [PATCH 12/32] feat: Add app_api app id to saved information about webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../lib/Controller/WebhooksController.php | 12 +++++++++ apps/webhooks/lib/Db/WebhookListener.php | 6 ++++- .../webhooks/lib/Db/WebhookListenerMapper.php | 4 +++ .../Version1000Date20240527153425.php | 4 +++ apps/webhooks/openapi.json | 26 +++++++++++++++++-- .../tests/Db/WebhookListenerMapperTest.php | 1 + 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index 21a06f5e563c6..0893743cdb951 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -20,6 +20,7 @@ use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCSController; use OCP\IRequest; +use OCP\ISession; use Psr\Log\LoggerInterface; /** @@ -33,6 +34,7 @@ public function __construct( private LoggerInterface $logger, private WebhookListenerMapper $mapper, private ?string $userId, + private ISession $session, ) { parent::__construct($appName, $request); } @@ -97,8 +99,13 @@ public function create( ?string $authMethod, ?array $authData, ): DataResponse { + $appId = null; + if ($this->session->get('app_api') === true) { + $appId = $this->request->getHeader('EX-APP-ID'); + } try { $webhookListener = $this->mapper->addWebhookListener( + $appId, $this->userId, $httpMethod, $uri, @@ -151,9 +158,14 @@ public function update( ?string $authMethod, ?array $authData, ): DataResponse { + $appId = null; + if ($this->session->get('app_api') === true) { + $appId = $this->request->getHeader('EX-APP-ID'); + } try { $webhookListener = $this->mapper->updateWebhookListener( $id, + $appId, $this->userId, $httpMethod, $uri, diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhooks/lib/Db/WebhookListener.php index 8e16249b678ab..20d00959f8c9a 100644 --- a/apps/webhooks/lib/Db/WebhookListener.php +++ b/apps/webhooks/lib/Db/WebhookListener.php @@ -16,7 +16,10 @@ * @method string getUserId() */ class WebhookListener extends Entity implements \JsonSerializable { - /** @var string id of the user who added the webhook listener */ + /** @var ?string id of the app_api application who added the webhook listener */ + protected $appId; + + /** @var string id of the user who added the webhook listener */ protected $userId; /** @var string */ @@ -41,6 +44,7 @@ class WebhookListener extends Entity implements \JsonSerializable { protected $authData; public function __construct() { + $this->addType('appId', 'string'); $this->addType('userId', 'string'); $this->addType('httpMethod', 'string'); $this->addType('uri', 'string'); diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index 5ce824893abcf..3b472231e37eb 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -57,6 +57,7 @@ public function getAll(): array { } public function addWebhookListener( + ?string $appId, string $userId, string $httpMethod, string $uri, @@ -68,6 +69,7 @@ public function addWebhookListener( ) { $webhookListener = WebhookListener::fromParams( [ + 'appId' => $appId, 'userId' => $userId, 'httpMethod' => $httpMethod, 'uri' => $uri, @@ -83,6 +85,7 @@ public function addWebhookListener( public function updateWebhookListener( int $id, + ?string $appId, string $userId, string $httpMethod, string $uri, @@ -95,6 +98,7 @@ public function updateWebhookListener( $webhookListener = WebhookListener::fromParams( [ 'id' => $id, + 'appId' => $appId, 'userId' => $userId, 'httpMethod' => $httpMethod, 'uri' => $uri, diff --git a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php index b6c345a22e2a9..1b06414353382 100755 --- a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php +++ b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php @@ -31,6 +31,10 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'notnull' => true, 'length' => 4, ]); + $table->addColumn('app_id', Types::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); $table->addColumn('user_id', Types::STRING, [ 'notnull' => true, 'length' => 64, diff --git a/apps/webhooks/openapi.json b/apps/webhooks/openapi.json index eeed54d780569..3a2c641b0a638 100644 --- a/apps/webhooks/openapi.json +++ b/apps/webhooks/openapi.json @@ -26,7 +26,8 @@ "id", "userId", "httpMethod", - "uri" + "uri", + "authMethod" ], "properties": { "id": { @@ -43,6 +44,27 @@ }, "event": { "type": "string" + }, + "eventFilter": { + "type": "array", + "items": { + "type": "object" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "authMethod": { + "type": "string" + }, + "authData": { + "type": "object", + "additionalProperties": { + "type": "object" + } } } }, @@ -709,4 +731,4 @@ } }, "tags": [] -} +} \ No newline at end of file diff --git a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php index 4481eb6661eca..c9f6e39b31f25 100644 --- a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php +++ b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php @@ -44,6 +44,7 @@ protected function pruneTables() { public function testInsertListenerAndGetIt() { $listener1 = $this->mapper->addWebhookListener( + null, 'bob', 'POST', 'https://webhook.example.com/endpoint', From 85e0407aad3fc5cafbffc9ceefe7953dfa1194e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 3 Jun 2024 17:11:52 +0200 Subject: [PATCH 13/32] feat: Add support for headers and authentication headers in webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/BackgroundJobs/WebhookCall.php | 10 ++++++ apps/webhooks/lib/Command/Index.php | 9 +++-- .../lib/Controller/WebhooksController.php | 5 +-- apps/webhooks/lib/Db/AuthMethod.php | 15 ++++++++ apps/webhooks/lib/Db/WebhookListener.php | 34 +++++++++++++++++-- .../webhooks/lib/Db/WebhookListenerMapper.php | 12 +++---- .../tests/Db/WebhookListenerMapperTest.php | 20 +++++++++++ .../tests/Service/PHPMongoQueryTest.php | 2 +- 10 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 apps/webhooks/lib/Db/AuthMethod.php diff --git a/apps/webhooks/composer/composer/autoload_classmap.php b/apps/webhooks/composer/composer/autoload_classmap.php index efd4a43d58ae3..3ac7eb7e7024e 100644 --- a/apps/webhooks/composer/composer/autoload_classmap.php +++ b/apps/webhooks/composer/composer/autoload_classmap.php @@ -11,6 +11,7 @@ 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php', 'OCA\\Webhooks\\Command\\Index' => $baseDir . '/../lib/Command/Index.php', 'OCA\\Webhooks\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php', + 'OCA\\Webhooks\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php', 'OCA\\Webhooks\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php', 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php', 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php', diff --git a/apps/webhooks/composer/composer/autoload_static.php b/apps/webhooks/composer/composer/autoload_static.php index 75182423ae6ee..788787644d726 100644 --- a/apps/webhooks/composer/composer/autoload_static.php +++ b/apps/webhooks/composer/composer/autoload_static.php @@ -26,6 +26,7 @@ class ComposerStaticInitWebhooks 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php', 'OCA\\Webhooks\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php', 'OCA\\Webhooks\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php', + 'OCA\\Webhooks\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php', 'OCA\\Webhooks\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php', 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php', 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php', diff --git a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php index 469b554a886d4..4c0def3f69de5 100644 --- a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php +++ b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php @@ -9,6 +9,7 @@ namespace OCA\Webhooks\BackgroundJobs; +use OCA\Webhooks\Db\AuthMethod; use OCA\Webhooks\Db\WebhookListenerMapper; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\QueuedJob; @@ -31,7 +32,16 @@ protected function run($argument): void { $client = $this->clientService->newClient(); $options = []; $options['body'] = json_encode($data); + $options['headers'] = $webhookListener->getHeaders(); try { + switch ($webhookListener->getAuthMethodEnum()) { + case AuthMethod::None: + break; + case AuthMethod::Header: + $authHeaders = $webhookListener->getAuthDataClear(); + $options['headers'] = array_merge($options['headers'], $authHeaders); + break; + } $response = $client->request($webhookListener->getHttpMethod(), $webhookListener->getUri(), $options); $statusCode = $response->getStatusCode(); if ($statusCode >= 200 && $statusCode < 300) { diff --git a/apps/webhooks/lib/Command/Index.php b/apps/webhooks/lib/Command/Index.php index 78feda3ec68ec..4457e95c1962f 100644 --- a/apps/webhooks/lib/Command/Index.php +++ b/apps/webhooks/lib/Command/Index.php @@ -31,11 +31,10 @@ protected function configure(): void { protected function execute(InputInterface $input, OutputInterface $output): int { $webhookListeners = array_map( - function (WebhookListener $listener): array { - $data = $listener->jsonSerialize(); - $data['eventFilter'] = json_encode($data['eventFilter']); - return $data; - }, + fn (WebhookListener $listener): array => array_map( + fn (string|array|null $value): ?string => (is_array($value) ? json_encode($value) : $value), + $listener->jsonSerialize() + ), $this->mapper->getAll() ); $this->writeTableInOutputFormat($input, $output, $webhookListeners); diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index 0893743cdb951..040e076be6595 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -10,6 +10,7 @@ namespace OCA\Webhooks\Controller; use Doctrine\DBAL\Exception; +use OCA\Webhooks\Db\AuthMethod; use OCA\Webhooks\Db\WebhookListenerMapper; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; @@ -112,7 +113,7 @@ public function create( $event, $eventFilter, $headers, - $authMethod, + AuthMethod::from($authMethod ?? AuthMethod::None->value), $authData, ); return new DataResponse($webhookListener); @@ -172,7 +173,7 @@ public function update( $event, $eventFilter, $headers, - $authMethod, + AuthMethod::from($authMethod ?? AuthMethod::None->value), $authData, ); return new DataResponse($webhookListener); diff --git a/apps/webhooks/lib/Db/AuthMethod.php b/apps/webhooks/lib/Db/AuthMethod.php new file mode 100644 index 0000000000000..4fe06ef34fba8 --- /dev/null +++ b/apps/webhooks/lib/Db/AuthMethod.php @@ -0,0 +1,15 @@ +crypto = $crypto; $this->addType('appId', 'string'); $this->addType('userId', 'string'); $this->addType('httpMethod', 'string'); @@ -52,7 +61,26 @@ public function __construct() { $this->addType('eventFilter', 'json'); $this->addType('headers', 'json'); $this->addType('authMethod', 'string'); - $this->addType('authData', 'json'); + $this->addType('authData', 'string'); + } + + public function getAuthMethodEnum(): AuthMethod { + return AuthMethod::from(parent::getAuthMethod()); + } + + public function getAuthDataClear(): array { + if ($this->authData === null) { + return []; + } + return json_decode($this->crypto->decrypt($this->getAuthData()), associative:true, flags:JSON_THROW_ON_ERROR); + } + + public function setAuthDataClear(?array $data): void { + if ($data === null) { + $this->setAuthData(null); + return; + } + $this->setAuthData($this->crypto->encrypt(json_encode($data))); } public function jsonSerialize(): array { diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index 3b472231e37eb..8f07a413c7f7a 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -64,7 +64,7 @@ public function addWebhookListener( string $event, ?array $eventFilter, ?array $headers, - ?string $authMethod, + AuthMethod $authMethod, ?array $authData, ) { $webhookListener = WebhookListener::fromParams( @@ -76,10 +76,10 @@ public function addWebhookListener( 'event' => $event, 'eventFilter' => $eventFilter ?? [], 'headers' => $headers, - 'authMethod' => $authMethod ?? 'none', - 'authData' => $authData, + 'authMethod' => $authMethod->value, ] ); + $webhookListener->setAuthDataClear($authData); return $this->insert($webhookListener); } @@ -92,7 +92,7 @@ public function updateWebhookListener( string $event, ?array $eventFilter, ?array $headers, - ?string $authMethod, + AuthMethod $authMethod, ?array $authData, ) { $webhookListener = WebhookListener::fromParams( @@ -105,10 +105,10 @@ public function updateWebhookListener( 'event' => $event, 'eventFilter' => $eventFilter ?? [], 'headers' => $headers, - 'authMethod' => $authMethod, - 'authData' => $authData, + 'authMethod' => $authMethod->value, ] ); + $webhookListener->setAuthDataClear($authData); return $this->update($webhookListener); } diff --git a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php index c9f6e39b31f25..a30c07280baeb 100644 --- a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php +++ b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php @@ -9,6 +9,7 @@ namespace OCA\Webhooks\Tests\Db; +use OCA\Webhooks\Db\AuthMethod; use OCA\Webhooks\Db\WebhookListenerMapper; use OCP\IDBConnection; use OCP\User\Events\UserCreatedEvent; @@ -51,8 +52,27 @@ public function testInsertListenerAndGetIt() { UserCreatedEvent::class, null, null, + AuthMethod::None, + null, + ); + + $listener2 = $this->mapper->getById($listener1->getId()); + + $listener1->resetUpdatedFields(); + $this->assertEquals($listener1, $listener2); + } + + public function testInsertListenerAndGetItWithAuthData() { + $listener1 = $this->mapper->addWebhookListener( + null, + 'bob', + 'POST', + 'https://webhook.example.com/endpoint', + UserCreatedEvent::class, null, null, + AuthMethod::Header, + ['secretHeader' => 'header'], ); $listener2 = $this->mapper->getById($listener1->getId()); diff --git a/apps/webhooks/tests/Service/PHPMongoQueryTest.php b/apps/webhooks/tests/Service/PHPMongoQueryTest.php index 06658b718f003..51684bb8e34fb 100644 --- a/apps/webhooks/tests/Service/PHPMongoQueryTest.php +++ b/apps/webhooks/tests/Service/PHPMongoQueryTest.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Tests\Db; +namespace OCA\Webhooks\Tests\Service; use OCA\Webhooks\Service\PHPMongoQuery; use OCP\Files\Events\Node\NodeWrittenEvent; From 8aae03c66c98b58fe308f09d18663c4ad0d6663c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 4 Jun 2024 14:21:10 +0200 Subject: [PATCH 14/32] fix: Fix TODOs about authentication data, list possible auth methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../lib/Controller/WebhooksController.php | 6 ++--- .../Version1000Date20240527153425.php | 1 - apps/webhooks/openapi.json | 25 +++++++++++-------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index 040e076be6595..832f22727340b 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -76,9 +76,9 @@ public function show(int $id): DataResponse { * @param string $httpMethod HTTP method to use to contact the webhook * @param string $uri Webhook URI endpoint * @param string $event Event class name to listen to - * @param ?array $eventFilter Mongo filter to apply to the serialized data to decide if firing + * @param ?array $eventFilter Mongo filter to apply to the serialized data to decide if firing * @param ?array $headers Array of headers to send - * @param ?string $authMethod Authentication method to use. TODO + * @param "none"|"headers"|null $authMethod Authentication method to use * @param ?array $authData Array of data for authentication * * @return DataResponse @@ -136,7 +136,7 @@ public function create( * @param string $event Event class name to listen to * @param ?array $eventFilter Mongo filter to apply to the serialized data to decide if firing * @param ?array $headers Array of headers to send - * @param ?string $authMethod Authentication method to use. TODO + * @param "none"|"headers"|null $authMethod Authentication method to use * @param ?array $authData Array of data for authentication * * @return DataResponse diff --git a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php index 1b06414353382..f2d722687de98 100755 --- a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php +++ b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php @@ -56,7 +56,6 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('headers', Types::TEXT, [ 'notnull' => false, ]); - // TODO decide if string or int with an Enum $table->addColumn('auth_method', Types::STRING, [ 'notnull' => true, 'length' => 16, diff --git a/apps/webhooks/openapi.json b/apps/webhooks/openapi.json index 3a2c641b0a638..13bc1a7fb5fea 100644 --- a/apps/webhooks/openapi.json +++ b/apps/webhooks/openapi.json @@ -203,15 +203,12 @@ } }, { - "name": "eventFilter[]", + "name": "eventFilter", "in": "query", "description": "Mongo filter to apply to the serialized data to decide if firing", "schema": { - "type": "array", - "nullable": true, - "items": { - "type": "object" - } + "type": "string", + "nullable": true } }, { @@ -226,10 +223,14 @@ { "name": "authMethod", "in": "query", - "description": "Authentication method to use. TODO", + "description": "Authentication method to use", "schema": { "type": "string", - "nullable": true + "nullable": true, + "enum": [ + "none", + "headers" + ] } }, { @@ -477,10 +478,14 @@ { "name": "authMethod", "in": "query", - "description": "Authentication method to use. TODO", + "description": "Authentication method to use", "schema": { "type": "string", - "nullable": true + "nullable": true, + "enum": [ + "none", + "headers" + ] } }, { From d3c06d5dea703d4bf53d8018e60cb109f65d8139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 4 Jun 2024 14:46:44 +0200 Subject: [PATCH 15/32] fix: Fix eventFilter definition in ResponseDefinitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/lib/ResponseDefinitions.php | 2 +- apps/webhooks/openapi.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webhooks/lib/ResponseDefinitions.php b/apps/webhooks/lib/ResponseDefinitions.php index a8c188d2a5917..ea31de931acbb 100644 --- a/apps/webhooks/lib/ResponseDefinitions.php +++ b/apps/webhooks/lib/ResponseDefinitions.php @@ -16,7 +16,7 @@ * httpMethod: string, * uri: string, * event?: string, - * eventFilter?: array, + * eventFilter?: array, * headers?: array, * authMethod: string, * authData?: array, diff --git a/apps/webhooks/openapi.json b/apps/webhooks/openapi.json index 13bc1a7fb5fea..bdcc8007270b4 100644 --- a/apps/webhooks/openapi.json +++ b/apps/webhooks/openapi.json @@ -46,8 +46,8 @@ "type": "string" }, "eventFilter": { - "type": "array", - "items": { + "type": "object", + "additionalProperties": { "type": "object" } }, From cccda66c7dc7929c0c4f251c7d5a33e69ae824d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 6 Jun 2024 10:59:21 +0200 Subject: [PATCH 16/32] fix: Fix errors spotted by reviewers, fix `@throws` annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../lib/BackgroundJobs/WebhookCall.php | 4 ++-- .../lib/Controller/WebhooksController.php | 4 ++-- apps/webhooks/lib/Db/WebhookListenerMapper.php | 18 ++++++++++++------ .../lib/Listener/WebhooksEventListener.php | 3 +-- .../Version1000Date20240527153425.php | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php index 4c0def3f69de5..06c3a4cf5d72a 100644 --- a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php +++ b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php @@ -45,9 +45,9 @@ protected function run($argument): void { $response = $client->request($webhookListener->getHttpMethod(), $webhookListener->getUri(), $options); $statusCode = $response->getStatusCode(); if ($statusCode >= 200 && $statusCode < 300) { - $this->logger->warning('Webhook returned unexpected status code '.$statusCode, ['body' => $response->getBody()]); - } else { $this->logger->debug('Webhook returned status code '.$statusCode, ['body' => $response->getBody()]); + } else { + $this->logger->warning('Webhook returned unexpected status code '.$statusCode, ['body' => $response->getBody()]); } } catch (\Exception $e) { $this->logger->error('Webhook call failed: '.$e->getMessage(), ['exception' => $e]); diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index 832f22727340b..f61e458430597 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -9,9 +9,9 @@ namespace OCA\Webhooks\Controller; -use Doctrine\DBAL\Exception; use OCA\Webhooks\Db\AuthMethod; use OCA\Webhooks\Db\WebhookListenerMapper; +use OCA\Webhooks\ResponseDefinitions; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\Attribute\OpenAPI; @@ -210,7 +210,7 @@ public function destroy(int $id): DataResponse { throw new OCSBadRequestException($e->getMessage(), $e); } catch (\DomainException $e) { throw new OCSForbiddenException($e->getMessage(), $e); - } catch (Exception $e) { + } catch (\Exception $e) { $this->logger->error('Error when deleting flow with id ' . $id, ['exception' => $e]); throw new OCSException('An internal error occurred', $e->getCode(), $e); } diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index 8f07a413c7f7a..a4a43a882f592 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -42,8 +42,6 @@ public function getById(int $id): WebhookListener { } /** - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException * @throws Exception * @return WebhookListener[] */ @@ -56,6 +54,9 @@ public function getAll(): array { return $this->findEntities($qb); } + /** + * @throws Exception + */ public function addWebhookListener( ?string $appId, string $userId, @@ -66,7 +67,7 @@ public function addWebhookListener( ?array $headers, AuthMethod $authMethod, ?array $authData, - ) { + ): WebhookListener { $webhookListener = WebhookListener::fromParams( [ 'appId' => $appId, @@ -83,6 +84,9 @@ public function addWebhookListener( return $this->insert($webhookListener); } + /** + * @throws Exception + */ public function updateWebhookListener( int $id, ?string $appId, @@ -94,7 +98,7 @@ public function updateWebhookListener( ?array $headers, AuthMethod $authMethod, ?array $authData, - ) { + ): WebhookListener { $webhookListener = WebhookListener::fromParams( [ 'id' => $id, @@ -113,8 +117,6 @@ public function updateWebhookListener( } /** - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException * @throws Exception */ public function deleteById(int $id): bool { @@ -127,6 +129,7 @@ public function deleteById(int $id): bool { } /** + * @throws Exception * @return list * TODO cache */ @@ -147,6 +150,9 @@ public function getAllConfiguredEvents(): array { return $configuredEvents; } + /** + * @throws Exception + */ public function getByEvent(string $event): array { $qb = $this->db->getQueryBuilder(); diff --git a/apps/webhooks/lib/Listener/WebhooksEventListener.php b/apps/webhooks/lib/Listener/WebhooksEventListener.php index 5ea0012d4e9cd..afe53595e75dc 100644 --- a/apps/webhooks/lib/Listener/WebhooksEventListener.php +++ b/apps/webhooks/lib/Listener/WebhooksEventListener.php @@ -34,14 +34,13 @@ public function __construct( public function handle(Event $event): void { $webhookListeners = $this->mapper->getByEvent($event::class); - /** @var IUser */ $user = $this->userSession->getUser(); foreach ($webhookListeners as $webhookListener) { // TODO add group membership to be able to filter on it $data = [ 'event' => $this->serializeEvent($event), - 'user' => JsonSerializer::serializeUser($user), + 'user' => (is_null($user) ? null : JsonSerializer::serializeUser($user)), 'time' => time(), ]; if ($this->filterMatch($webhookListener->getEventFilter(), $data)) { diff --git a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php index f2d722687de98..7c6f66314e468 100755 --- a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php +++ b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php @@ -26,7 +26,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if (!$schema->hasTable(WebhookListenerMapper::TABLE_NAME)) { $table = $schema->createTable(WebhookListenerMapper::TABLE_NAME); - $table->addColumn('id', Types::INTEGER, [ + $table->addColumn('id', Types::BIGINT, [ 'autoincrement' => true, 'notnull' => true, 'length' => 4, @@ -45,7 +45,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); $table->addColumn('uri', Types::STRING, [ 'notnull' => true, - 'length' => 256, + 'length' => 4096, ]); $table->addColumn('event', Types::TEXT, [ 'notnull' => true, From 35d5d90dc01b6525fb8aabb4b71aeaa9e7b7a249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 6 Jun 2024 11:06:58 +0200 Subject: [PATCH 17/32] chore: rename Index.php to ListWebhooks.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/appinfo/info.xml | 2 +- apps/webhooks/composer/composer/autoload_classmap.php | 2 +- apps/webhooks/composer/composer/autoload_static.php | 2 +- apps/webhooks/lib/Command/{Index.php => ListWebhooks.php} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename apps/webhooks/lib/Command/{Index.php => ListWebhooks.php} (97%) diff --git a/apps/webhooks/appinfo/info.xml b/apps/webhooks/appinfo/info.xml index 6b9a809dba0df..4bc0a99931071 100644 --- a/apps/webhooks/appinfo/info.xml +++ b/apps/webhooks/appinfo/info.xml @@ -24,7 +24,7 @@ - OCA\Webhooks\Command\Index + OCA\Webhooks\Command\ListWebhooks diff --git a/apps/webhooks/composer/composer/autoload_classmap.php b/apps/webhooks/composer/composer/autoload_classmap.php index 3ac7eb7e7024e..bc6625e3c93a9 100644 --- a/apps/webhooks/composer/composer/autoload_classmap.php +++ b/apps/webhooks/composer/composer/autoload_classmap.php @@ -9,7 +9,7 @@ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\Webhooks\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php', - 'OCA\\Webhooks\\Command\\Index' => $baseDir . '/../lib/Command/Index.php', + 'OCA\\Webhooks\\Command\\ListWebhooks' => $baseDir . '/../lib/Command/ListWebhooks.php', 'OCA\\Webhooks\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php', 'OCA\\Webhooks\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php', 'OCA\\Webhooks\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php', diff --git a/apps/webhooks/composer/composer/autoload_static.php b/apps/webhooks/composer/composer/autoload_static.php index 788787644d726..d06810391ec16 100644 --- a/apps/webhooks/composer/composer/autoload_static.php +++ b/apps/webhooks/composer/composer/autoload_static.php @@ -24,7 +24,7 @@ class ComposerStaticInitWebhooks 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\Webhooks\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php', - 'OCA\\Webhooks\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php', + 'OCA\\Webhooks\\Command\\ListWebhooks' => __DIR__ . '/..' . '/../lib/Command/ListWebhooks.php', 'OCA\\Webhooks\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php', 'OCA\\Webhooks\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php', 'OCA\\Webhooks\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php', diff --git a/apps/webhooks/lib/Command/Index.php b/apps/webhooks/lib/Command/ListWebhooks.php similarity index 97% rename from apps/webhooks/lib/Command/Index.php rename to apps/webhooks/lib/Command/ListWebhooks.php index 4457e95c1962f..c8cca6f860994 100644 --- a/apps/webhooks/lib/Command/Index.php +++ b/apps/webhooks/lib/Command/ListWebhooks.php @@ -15,7 +15,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class Index extends Base { +class ListWebhooks extends Base { public function __construct( private WebhookListenerMapper $mapper, ) { From e111d2e26cbc50fb252c940980574a8579ababde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 6 Jun 2024 16:12:36 +0200 Subject: [PATCH 18/32] fix: Make webhook event serialization opt-in with a new interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../webhooks/lib/Db/WebhookListenerMapper.php | 7 +++++ .../lib/Listener/WebhooksEventListener.php | 30 ++++--------------- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../IWebhookCompatibleEvent.php | 24 +++++++++++++++ .../Files/Events/Node/AbstractNodeEvent.php | 6 ++-- .../Files/Events/Node/AbstractNodesEvent.php | 6 ++-- 7 files changed, 45 insertions(+), 30 deletions(-) create mode 100644 lib/public/EventDispatcher/IWebhookCompatibleEvent.php diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index a4a43a882f592..85c167b0c9225 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -14,6 +14,7 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IWebhookCompatibleEvent; use OCP\IDBConnection; /** @@ -68,6 +69,9 @@ public function addWebhookListener( AuthMethod $authMethod, ?array $authData, ): WebhookListener { + if (!class_exists($event) || !is_a($event, IWebhookCompatibleEvent::class, true)) { + throw new \UnexpectedValueException("$event is not an event class compatible with webhooks"); + } $webhookListener = WebhookListener::fromParams( [ 'appId' => $appId, @@ -99,6 +103,9 @@ public function updateWebhookListener( AuthMethod $authMethod, ?array $authData, ): WebhookListener { + if (!class_exists($event) || !is_a($event, IWebhookCompatibleEvent::class, true)) { + throw new \UnexpectedValueException("$event is not an event class compatible with webhooks"); + } $webhookListener = WebhookListener::fromParams( [ 'id' => $id, diff --git a/apps/webhooks/lib/Listener/WebhooksEventListener.php b/apps/webhooks/lib/Listener/WebhooksEventListener.php index afe53595e75dc..37d6863553a48 100644 --- a/apps/webhooks/lib/Listener/WebhooksEventListener.php +++ b/apps/webhooks/lib/Listener/WebhooksEventListener.php @@ -15,13 +15,14 @@ use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\EventDispatcher\IWebhookCompatibleEvent; use OCP\EventDispatcher\JsonSerializer; use OCP\IUserSession; use Psr\Log\LoggerInterface; /** * The class to handle the share events - * @template-implements IEventListener + * @template-implements IEventListener */ class WebhooksEventListener implements IEventListener { public function __construct( @@ -55,29 +56,10 @@ public function handle(Event $event): void { } } - private function serializeEvent(Event $event): array|\JsonSerializable { - if ($event instanceof \JsonSerializable) { - return $event; - } else { - /* Event is not serializable, we fallback to reflection to still send something */ - $data = ['class' => $event::class]; - $ref = new \ReflectionClass($event); - foreach ($ref->getMethods() as $method) { - if (str_starts_with($method->getName(), 'get')) { - $key = strtolower(substr($method->getName(), 3)); - $value = $method->invoke($event); - if ($value instanceof \OCP\Files\FileInfo) { - $value = [ - 'id' => $value->getId(), - 'path' => $value->getPath(), - ]; - } - $data[$key] = $value; - } - } - $this->logger->debug('Webhook had to use fallback to serialize event '.$event::class); - return $data; - } + private function serializeEvent(IWebhookCompatibleEvent $event): array { + $data = $event->getWebhookSerializable(); + $data['class'] = $event::class; + return $data; } private function filterMatch(array $filter, array $data): bool { diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 0fe1314644fe6..dbd9ebc66ab58 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -281,6 +281,7 @@ 'OCP\\EventDispatcher\\GenericEvent' => $baseDir . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => $baseDir . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => $baseDir . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\EventDispatcher\\IWebhookCompatibleEvent' => $baseDir . '/lib/public/EventDispatcher/IWebhookCompatibleEvent.php', 'OCP\\EventDispatcher\\JsonSerializer' => $baseDir . '/lib/public/EventDispatcher/JsonSerializer.php', 'OCP\\Exceptions\\AbortedEventException' => $baseDir . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Exceptions\\AppConfigException' => $baseDir . '/lib/public/Exceptions/AppConfigException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index cf52b9026f11a..9f3b289cdfc5a 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -322,6 +322,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\EventDispatcher\\GenericEvent' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\EventDispatcher\\IWebhookCompatibleEvent' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IWebhookCompatibleEvent.php', 'OCP\\EventDispatcher\\JsonSerializer' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/JsonSerializer.php', 'OCP\\Exceptions\\AbortedEventException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Exceptions\\AppConfigException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AppConfigException.php', diff --git a/lib/public/EventDispatcher/IWebhookCompatibleEvent.php b/lib/public/EventDispatcher/IWebhookCompatibleEvent.php new file mode 100644 index 0000000000000..b13c35c187bbc --- /dev/null +++ b/lib/public/EventDispatcher/IWebhookCompatibleEvent.php @@ -0,0 +1,24 @@ + static::class, 'node' => JsonSerializer::serializeFileInfo($this->node), ]; } diff --git a/lib/public/Files/Events/Node/AbstractNodesEvent.php b/lib/public/Files/Events/Node/AbstractNodesEvent.php index b5fd1b75898f8..7941a9e596a80 100644 --- a/lib/public/Files/Events/Node/AbstractNodesEvent.php +++ b/lib/public/Files/Events/Node/AbstractNodesEvent.php @@ -9,13 +9,14 @@ namespace OCP\Files\Events\Node; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IWebhookCompatibleEvent; use OCP\EventDispatcher\JsonSerializer; use OCP\Files\Node; /** * @since 20.0.0 */ -abstract class AbstractNodesEvent extends Event { +abstract class AbstractNodesEvent extends Event implements IWebhookCompatibleEvent { /** * @since 20.0.0 */ @@ -42,9 +43,8 @@ public function getTarget(): Node { /** * @since 30.0.0 */ - public function jsonSerialize(): array { + public function getWebhookSerializable(): array { return [ - 'class' => static::class, 'source' => JsonSerializer::serializeFileInfo($this->source), 'target' => JsonSerializer::serializeFileInfo($this->target), ]; From 98f3ea657c4ec7b1b0c15230fbbfbd06a978a6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 6 Jun 2024 16:23:28 +0200 Subject: [PATCH 19/32] fix: Cache webhooks listened events for 5min MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/lib/AppInfo/Application.php | 35 +++++++++++++++---- .../webhooks/lib/Db/WebhookListenerMapper.php | 1 - 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/webhooks/lib/AppInfo/Application.php b/apps/webhooks/lib/AppInfo/Application.php index a8836a25f5470..cbbacd3ed4881 100644 --- a/apps/webhooks/lib/AppInfo/Application.php +++ b/apps/webhooks/lib/AppInfo/Application.php @@ -16,14 +16,25 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICache; +use OCP\ICacheFactory; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; class Application extends App implements IBootstrap { public const APP_ID = 'webhooks'; - public function __construct() { + private ?ICache $cache = null; + + private const CACHE_KEY = 'eventsUsedInWebhooks'; + + public function __construct( + ICacheFactory $cacheFactory, + ) { parent::__construct(self::APP_ID); + if ($cacheFactory->isAvailable()) { + $this->cache = $cacheFactory->createDistributed(); + } } public function register(IRegistrationContext $context): void { @@ -38,13 +49,10 @@ private function registerRuleListeners( ContainerInterface $container, LoggerInterface $logger, ): void { - /** @var WebhookListenerMapper */ - $mapper = $container->get(WebhookListenerMapper::class); - /* Listen to all events with at least one webhook configured */ - $configuredEvents = $mapper->getAllConfiguredEvents(); + $configuredEvents = $this->getAllConfiguredEvents($container); foreach ($configuredEvents as $eventName) { - // $logger->error($eventName.' '.\OCP\Files\Events\Node\NodeWrittenEvent::class, ['exception' => new \Exception('coucou')]); + $logger->debug("Listening to {$eventName}"); $dispatcher->addServiceListener( $eventName, WebhooksEventListener::class, @@ -52,4 +60,19 @@ private function registerRuleListeners( ); } } + + /** + * List all events with at least one webhook configured, with cache + */ + private function getAllConfiguredEvents(ContainerInterface $container) { + $events = $this->cache?->get(self::CACHE_KEY); + if ($events !== null) { + return json_decode($events); + } + /** @var WebhookListenerMapper */ + $mapper = $container->get(WebhookListenerMapper::class); + $events = $mapper->getAllConfiguredEvents(); + // cache for 5 minutes + $this->cache?->set(self::CACHE_KEY, json_encode($events), 300); + } } diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index 85c167b0c9225..a8985ae6e88b7 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -138,7 +138,6 @@ public function deleteById(int $id): bool { /** * @throws Exception * @return list - * TODO cache */ public function getAllConfiguredEvents(): array { $qb = $this->db->getQueryBuilder(); From 3bc43b2a34b4f1a4da2fdca7c93e72a62445ac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 6 Jun 2024 17:00:45 +0200 Subject: [PATCH 20/32] fix: Move caching to the mapper instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application class cannot use DI, and having the cache in the mapper allows for invalidating it when inserting or updating a webhook registration. Signed-off-by: Côme Chilliet --- apps/webhooks/lib/AppInfo/Application.php | 33 +++--------------- .../webhooks/lib/Db/WebhookListenerMapper.php | 34 +++++++++++++++++-- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/apps/webhooks/lib/AppInfo/Application.php b/apps/webhooks/lib/AppInfo/Application.php index cbbacd3ed4881..22abec747927f 100644 --- a/apps/webhooks/lib/AppInfo/Application.php +++ b/apps/webhooks/lib/AppInfo/Application.php @@ -16,25 +16,14 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\EventDispatcher\IEventDispatcher; -use OCP\ICache; -use OCP\ICacheFactory; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; class Application extends App implements IBootstrap { public const APP_ID = 'webhooks'; - private ?ICache $cache = null; - - private const CACHE_KEY = 'eventsUsedInWebhooks'; - - public function __construct( - ICacheFactory $cacheFactory, - ) { + public function __construct() { parent::__construct(self::APP_ID); - if ($cacheFactory->isAvailable()) { - $this->cache = $cacheFactory->createDistributed(); - } } public function register(IRegistrationContext $context): void { @@ -49,8 +38,11 @@ private function registerRuleListeners( ContainerInterface $container, LoggerInterface $logger, ): void { + /** @var WebhookListenerMapper */ + $mapper = $container->get(WebhookListenerMapper::class); + /* Listen to all events with at least one webhook configured */ - $configuredEvents = $this->getAllConfiguredEvents($container); + $configuredEvents = $mapper->getAllConfiguredEvents(); foreach ($configuredEvents as $eventName) { $logger->debug("Listening to {$eventName}"); $dispatcher->addServiceListener( @@ -60,19 +52,4 @@ private function registerRuleListeners( ); } } - - /** - * List all events with at least one webhook configured, with cache - */ - private function getAllConfiguredEvents(ContainerInterface $container) { - $events = $this->cache?->get(self::CACHE_KEY); - if ($events !== null) { - return json_decode($events); - } - /** @var WebhookListenerMapper */ - $mapper = $container->get(WebhookListenerMapper::class); - $events = $mapper->getAllConfiguredEvents(); - // cache for 5 minutes - $this->cache?->set(self::CACHE_KEY, json_encode($events), 300); - } } diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index a8985ae6e88b7..78c3ea6ec4fa2 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -15,6 +15,8 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IWebhookCompatibleEvent; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IDBConnection; /** @@ -23,8 +25,18 @@ class WebhookListenerMapper extends QBMapper { public const TABLE_NAME = 'webhook_listeners'; - public function __construct(IDBConnection $db) { + private const EVENTS_CACHE_KEY = 'eventsUsedInWebhooks'; + + private ?ICache $cache = null; + + public function __construct( + IDBConnection $db, + ICacheFactory $cacheFactory, + ) { parent::__construct($db, self::TABLE_NAME, WebhookListener::class); + if ($cacheFactory->isAvailable()) { + $this->cache = $cacheFactory->createDistributed(); + } } /** @@ -85,6 +97,7 @@ public function addWebhookListener( ] ); $webhookListener->setAuthDataClear($authData); + $this->cache?->remove(self::EVENTS_CACHE_KEY); return $this->insert($webhookListener); } @@ -120,6 +133,7 @@ public function updateWebhookListener( ] ); $webhookListener->setAuthDataClear($authData); + $this->cache?->remove(self::EVENTS_CACHE_KEY); return $this->update($webhookListener); } @@ -139,7 +153,7 @@ public function deleteById(int $id): bool { * @throws Exception * @return list */ - public function getAllConfiguredEvents(): array { + private function getAllConfiguredEventsFromDatabase(): array { $qb = $this->db->getQueryBuilder(); $qb->selectDistinct('event') @@ -156,6 +170,22 @@ public function getAllConfiguredEvents(): array { return $configuredEvents; } + /** + * List all events with at least one webhook configured, with cache + * @throws Exception + * @return list + */ + public function getAllConfiguredEvents(): array { + $events = $this->cache?->get(self::EVENTS_CACHE_KEY); + if ($events !== null) { + return json_decode($events); + } + $events = $this->getAllConfiguredEventsFromDatabase(); + // cache for 5 minutes + $this->cache?->set(self::EVENTS_CACHE_KEY, json_encode($events), 300); + return $events; + } + /** * @throws Exception */ From bb2cb45340225e0e72540ecaf42a31c58376f5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 16:25:12 +0200 Subject: [PATCH 21/32] fix: Remove superfluous antislash from event name to avoid problems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/lib/Db/WebhookListenerMapper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index 78c3ea6ec4fa2..7d7360431dc42 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -81,6 +81,8 @@ public function addWebhookListener( AuthMethod $authMethod, ?array $authData, ): WebhookListener { + /* Remove any superfluous antislash */ + $event = ltrim($event, '\\'); if (!class_exists($event) || !is_a($event, IWebhookCompatibleEvent::class, true)) { throw new \UnexpectedValueException("$event is not an event class compatible with webhooks"); } @@ -116,6 +118,8 @@ public function updateWebhookListener( AuthMethod $authMethod, ?array $authData, ): WebhookListener { + /* Remove any superfluous antislash */ + $event = ltrim($event, '\\'); if (!class_exists($event) || !is_a($event, IWebhookCompatibleEvent::class, true)) { throw new \UnexpectedValueException("$event is not an event class compatible with webhooks"); } From 621c068680bb4710d45c86764c718bb8edb46506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 16:26:59 +0200 Subject: [PATCH 22/32] fix(webhooks): fix crash when headers is null with header auth method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/lib/BackgroundJobs/WebhookCall.php | 2 +- apps/webhooks/lib/Db/WebhookListener.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php index 06c3a4cf5d72a..1d5eeafdc75bd 100644 --- a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php +++ b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php @@ -32,7 +32,7 @@ protected function run($argument): void { $client = $this->clientService->newClient(); $options = []; $options['body'] = json_encode($data); - $options['headers'] = $webhookListener->getHeaders(); + $options['headers'] = $webhookListener->getHeaders() ?? []; try { switch ($webhookListener->getAuthMethodEnum()) { case AuthMethod::None: diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhooks/lib/Db/WebhookListener.php index fafda909f6009..a949d93c9ea41 100644 --- a/apps/webhooks/lib/Db/WebhookListener.php +++ b/apps/webhooks/lib/Db/WebhookListener.php @@ -15,6 +15,7 @@ /** * @method void setUserId(string $userId) * @method string getUserId() + * @method ?array getHeaders() */ class WebhookListener extends Entity implements \JsonSerializable { /** @var ?string id of the app_api application who added the webhook listener */ From 74e5812c26dad3aa102088af9b525a4ead2c78d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 16:33:44 +0200 Subject: [PATCH 23/32] fix(webhooks): Refuse webhooks registration if auth header is used without auth data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/lib/Db/WebhookListener.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhooks/lib/Db/WebhookListener.php index a949d93c9ea41..4781454fc1a97 100644 --- a/apps/webhooks/lib/Db/WebhookListener.php +++ b/apps/webhooks/lib/Db/WebhookListener.php @@ -78,6 +78,9 @@ public function getAuthDataClear(): array { public function setAuthDataClear(?array $data): void { if ($data === null) { + if ($this->getAuthMethodEnum() === AuthMethod::Header) { + throw new \UnexpectedValueException('Header auth method needs an associative array of headers as auth data'); + } $this->setAuthData(null); return; } From 6c7931b8466a377eef5e8be36d50e7cd20cda045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 16:34:15 +0200 Subject: [PATCH 24/32] fix(webhooks): Add Nextcloud certificate bundle to client options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This way importing a certificate with occ security:certificate:import will allow to use it for webhooks. Signed-off-by: Côme Chilliet --- apps/webhooks/lib/BackgroundJobs/WebhookCall.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php index 1d5eeafdc75bd..4edc76fbea999 100644 --- a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php +++ b/apps/webhooks/lib/BackgroundJobs/WebhookCall.php @@ -14,11 +14,13 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\QueuedJob; use OCP\Http\Client\IClientService; +use OCP\ICertificateManager; use Psr\Log\LoggerInterface; class WebhookCall extends QueuedJob { public function __construct( private IClientService $clientService, + private ICertificateManager $certificateManager, private WebhookListenerMapper $mapper, private LoggerInterface $logger, ITimeFactory $timeFactory, @@ -30,9 +32,11 @@ protected function run($argument): void { [$data, $webhookId] = $argument; $webhookListener = $this->mapper->getById($webhookId); $client = $this->clientService->newClient(); - $options = []; - $options['body'] = json_encode($data); - $options['headers'] = $webhookListener->getHeaders() ?? []; + $options = [ + 'verify' => $this->certificateManager->getAbsoluteBundlePath(), + 'headers' => $webhookListener->getHeaders() ?? [], + 'body' => json_encode($data), + ]; try { switch ($webhookListener->getAuthMethodEnum()) { case AuthMethod::None: From c940ba015cb842407f9d503e2e4e74675896af03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 17:07:07 +0200 Subject: [PATCH 25/32] fix(webhooks): Fix migration uri length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/lib/Migration/Version1000Date20240527153425.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php index 7c6f66314e468..a54702de6c061 100755 --- a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php +++ b/apps/webhooks/lib/Migration/Version1000Date20240527153425.php @@ -45,7 +45,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); $table->addColumn('uri', Types::STRING, [ 'notnull' => true, - 'length' => 4096, + 'length' => 4000, ]); $table->addColumn('event', Types::TEXT, [ 'notnull' => true, From 6e38b48534e87fbaac5d93c819993fb124bdb54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 17:07:44 +0200 Subject: [PATCH 26/32] feat(webhooks): Add \SensitiveValue attribute to authData parameter in all classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/webhooks/lib/Controller/WebhooksController.php | 2 ++ apps/webhooks/lib/Db/WebhookListener.php | 5 ++++- apps/webhooks/lib/Db/WebhookListenerMapper.php | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhooks/lib/Controller/WebhooksController.php index f61e458430597..e3df0897bbe52 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhooks/lib/Controller/WebhooksController.php @@ -98,6 +98,7 @@ public function create( ?array $eventFilter, ?array $headers, ?string $authMethod, + #[\SensitiveParameter] ?array $authData, ): DataResponse { $appId = null; @@ -157,6 +158,7 @@ public function update( ?array $eventFilter, ?array $headers, ?string $authMethod, + #[\SensitiveParameter] ?array $authData, ): DataResponse { $appId = null; diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhooks/lib/Db/WebhookListener.php index 4781454fc1a97..c4053b5ba7b92 100644 --- a/apps/webhooks/lib/Db/WebhookListener.php +++ b/apps/webhooks/lib/Db/WebhookListener.php @@ -76,7 +76,10 @@ public function getAuthDataClear(): array { return json_decode($this->crypto->decrypt($this->getAuthData()), associative:true, flags:JSON_THROW_ON_ERROR); } - public function setAuthDataClear(?array $data): void { + public function setAuthDataClear( + #[\SensitiveParameter] + ?array $data + ): void { if ($data === null) { if ($this->getAuthMethodEnum() === AuthMethod::Header) { throw new \UnexpectedValueException('Header auth method needs an associative array of headers as auth data'); diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhooks/lib/Db/WebhookListenerMapper.php index 7d7360431dc42..4094b5e267967 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhooks/lib/Db/WebhookListenerMapper.php @@ -79,6 +79,7 @@ public function addWebhookListener( ?array $eventFilter, ?array $headers, AuthMethod $authMethod, + #[\SensitiveParameter] ?array $authData, ): WebhookListener { /* Remove any superfluous antislash */ @@ -116,6 +117,7 @@ public function updateWebhookListener( ?array $eventFilter, ?array $headers, AuthMethod $authMethod, + #[\SensitiveParameter] ?array $authData, ): WebhookListener { /* Remove any superfluous antislash */ From 9449f6438d1ef3127f57601ebdd9d5d24fa6d16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 17:13:41 +0200 Subject: [PATCH 27/32] fix(webhooks): Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../tests/Db/WebhookListenerMapperTest.php | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php index a30c07280baeb..76e58f0d2b726 100644 --- a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php +++ b/apps/webhooks/tests/Db/WebhookListenerMapperTest.php @@ -11,6 +11,8 @@ use OCA\Webhooks\Db\AuthMethod; use OCA\Webhooks\Db\WebhookListenerMapper; +use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\ICacheFactory; use OCP\IDBConnection; use OCP\User\Events\UserCreatedEvent; use Test\TestCase; @@ -21,15 +23,18 @@ class WebhookListenerMapperTest extends TestCase { private IDBConnection $connection; private WebhookListenerMapper $mapper; + private ICacheFactory $cacheFactory; protected function setUp(): void { parent::setUp(); $this->connection = \OCP\Server::get(IDBConnection::class); + $this->cacheFactory = \OCP\Server::get(ICacheFactory::class); $this->pruneTables(); $this->mapper = new WebhookListenerMapper( $this->connection, + $this->cacheFactory, ); } @@ -43,7 +48,8 @@ protected function pruneTables() { $query->delete(WebhookListenerMapper::TABLE_NAME)->executeStatement(); } - public function testInsertListenerAndGetIt() { + public function testInsertListenerWithNotSupportedEvent() { + $this->expectException(\UnexpectedValueException::class); $listener1 = $this->mapper->addWebhookListener( null, 'bob', @@ -55,6 +61,20 @@ public function testInsertListenerAndGetIt() { AuthMethod::None, null, ); + } + + public function testInsertListenerAndGetIt() { + $listener1 = $this->mapper->addWebhookListener( + null, + 'bob', + 'POST', + 'https://webhook.example.com/endpoint', + NodeWrittenEvent::class, + null, + null, + AuthMethod::None, + null, + ); $listener2 = $this->mapper->getById($listener1->getId()); @@ -68,7 +88,7 @@ public function testInsertListenerAndGetItWithAuthData() { 'bob', 'POST', 'https://webhook.example.com/endpoint', - UserCreatedEvent::class, + NodeWrittenEvent::class, null, null, AuthMethod::Header, From 19bc3ed1e3f52a9d9cd0a540e7e754a2fa16eb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 17:35:07 +0200 Subject: [PATCH 28/32] chore(webhooks): Rename webhooks application to webhook_listeners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is already a webhooks application in the appstore Signed-off-by: Côme Chilliet --- .gitignore | 2 +- .../dav/composer/composer/autoload_static.php | 2 +- .../appinfo/info.xml | 8 ++-- .../composer/autoload.php | 2 +- .../composer/composer.json | 4 +- .../composer/composer.lock | 0 .../composer/composer/ClassLoader.php | 0 .../composer/composer/InstalledVersions.php | 0 .../composer/composer/LICENSE | 0 .../composer/composer/autoload_classmap.php | 22 +++++++++ .../composer/composer/autoload_namespaces.php | 0 .../composer/composer/autoload_psr4.php | 2 +- .../composer/composer/autoload_real.php | 8 ++-- .../composer/composer/autoload_static.php | 48 +++++++++++++++++++ .../composer/composer/installed.json | 0 .../composer/composer/installed.php | 0 .../img/app-dark.svg | 0 .../img/app.svg | 0 .../lib/AppInfo/Application.php | 8 ++-- .../lib/BackgroundJobs/WebhookCall.php | 6 +-- .../lib/Command/ListWebhooks.php | 10 ++-- .../lib/Controller/WebhooksController.php | 18 +++---- .../lib/Db/AuthMethod.php | 2 +- .../lib/Db/WebhookListener.php | 2 +- .../lib/Db/WebhookListenerMapper.php | 2 +- .../lib/Listener/WebhooksEventListener.php | 8 ++-- .../Version1000Date20240527153425.php | 4 +- .../lib/ResponseDefinitions.php | 2 +- .../lib/Service/PHPMongoQuery.php | 2 +- .../lib/Settings/Admin.php | 2 +- .../openapi.json | 2 +- .../tests/Db/WebhookListenerMapperTest.php | 6 +-- .../tests/Service/PHPMongoQueryTest.php | 4 +- .../composer/composer/autoload_classmap.php | 22 --------- .../composer/composer/autoload_static.php | 48 ------------------- lib/composer/composer/autoload_psr4.php | 1 + psalm.xml | 1 + tests/enable_all.php | 2 +- 38 files changed, 126 insertions(+), 124 deletions(-) rename apps/{webhooks => webhook_listeners}/appinfo/info.xml (81%) rename apps/{webhooks => webhook_listeners}/composer/autoload.php (92%) rename apps/{webhooks => webhook_listeners}/composer/composer.json (65%) rename apps/{webhooks => webhook_listeners}/composer/composer.lock (100%) rename apps/{webhooks => webhook_listeners}/composer/composer/ClassLoader.php (100%) rename apps/{webhooks => webhook_listeners}/composer/composer/InstalledVersions.php (100%) rename apps/{webhooks => webhook_listeners}/composer/composer/LICENSE (100%) create mode 100644 apps/webhook_listeners/composer/composer/autoload_classmap.php rename apps/{webhooks => webhook_listeners}/composer/composer/autoload_namespaces.php (100%) rename apps/{webhooks => webhook_listeners}/composer/composer/autoload_psr4.php (66%) rename apps/{webhooks => webhook_listeners}/composer/composer/autoload_real.php (84%) create mode 100644 apps/webhook_listeners/composer/composer/autoload_static.php rename apps/{webhooks => webhook_listeners}/composer/composer/installed.json (100%) rename apps/{webhooks => webhook_listeners}/composer/composer/installed.php (100%) rename apps/{webhooks => webhook_listeners}/img/app-dark.svg (100%) rename apps/{webhooks => webhook_listeners}/img/app.svg (100%) rename apps/{webhooks => webhook_listeners}/lib/AppInfo/Application.php (87%) rename apps/{webhooks => webhook_listeners}/lib/BackgroundJobs/WebhookCall.php (93%) rename apps/{webhooks => webhook_listeners}/lib/Command/ListWebhooks.php (80%) rename apps/{webhooks => webhook_listeners}/lib/Controller/WebhooksController.php (92%) rename apps/{webhooks => webhook_listeners}/lib/Db/AuthMethod.php (87%) rename apps/{webhooks => webhook_listeners}/lib/Db/WebhookListener.php (98%) rename apps/{webhooks => webhook_listeners}/lib/Db/WebhookListenerMapper.php (99%) rename apps/{webhooks => webhook_listeners}/lib/Listener/WebhooksEventListener.php (89%) rename apps/{webhooks => webhook_listeners}/lib/Migration/Version1000Date20240527153425.php (94%) rename apps/{webhooks => webhook_listeners}/lib/ResponseDefinitions.php (94%) rename apps/{webhooks => webhook_listeners}/lib/Service/PHPMongoQuery.php (99%) rename apps/{webhooks => webhook_listeners}/lib/Settings/Admin.php (96%) rename apps/{webhooks => webhook_listeners}/openapi.json (99%) rename apps/{webhooks => webhook_listeners}/tests/Db/WebhookListenerMapperTest.php (94%) rename apps/{webhooks => webhook_listeners}/tests/Service/PHPMongoQueryTest.php (92%) delete mode 100644 apps/webhooks/composer/composer/autoload_classmap.php delete mode 100644 apps/webhooks/composer/composer/autoload_static.php diff --git a/.gitignore b/.gitignore index e9a27c5d68afc..a3e6f7872ca17 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,7 @@ !/apps/twofactor_backupcodes !/apps/user_status !/apps/weather_status -!/apps/webhooks +!/apps/webhook_listeners !/apps/workflowengine /apps/files_external/3rdparty/irodsphp/PHPUnitTest /apps/files_external/3rdparty/irodsphp/web diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index caa6cc8d5e6c3..94961e04837bc 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -150,13 +150,13 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/MultiGetExportPlugin.php', 'OCA\\DAV\\CardDAV\\PhotoCache' => __DIR__ . '/..' . '/../lib/CardDAV/PhotoCache.php', 'OCA\\DAV\\CardDAV\\Plugin' => __DIR__ . '/..' . '/../lib/CardDAV/Plugin.php', + 'OCA\\DAV\\CardDAV\\Security\\CardDavRateLimitingPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/Security/CardDavRateLimitingPlugin.php', 'OCA\\DAV\\CardDAV\\Sharing\\Backend' => __DIR__ . '/..' . '/../lib/CardDAV/Sharing/Backend.php', 'OCA\\DAV\\CardDAV\\Sharing\\Service' => __DIR__ . '/..' . '/../lib/CardDAV/Sharing/Service.php', 'OCA\\DAV\\CardDAV\\SyncService' => __DIR__ . '/..' . '/../lib/CardDAV/SyncService.php', 'OCA\\DAV\\CardDAV\\SystemAddressbook' => __DIR__ . '/..' . '/../lib/CardDAV/SystemAddressbook.php', 'OCA\\DAV\\CardDAV\\UserAddressBooks' => __DIR__ . '/..' . '/../lib/CardDAV/UserAddressBooks.php', 'OCA\\DAV\\CardDAV\\Xml\\Groups' => __DIR__ . '/..' . '/../lib/CardDAV/Xml/Groups.php', - 'OCA\\DAV\\CardDAV\\Security\\CardDavRateLimitingPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/Security/CardDavRateLimitingPlugin.php', 'OCA\\DAV\\Command\\CreateAddressBook' => __DIR__ . '/..' . '/../lib/Command/CreateAddressBook.php', 'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php', 'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php', diff --git a/apps/webhooks/appinfo/info.xml b/apps/webhook_listeners/appinfo/info.xml similarity index 81% rename from apps/webhooks/appinfo/info.xml rename to apps/webhook_listeners/appinfo/info.xml index 4bc0a99931071..a8cec901ec3ed 100644 --- a/apps/webhooks/appinfo/info.xml +++ b/apps/webhook_listeners/appinfo/info.xml @@ -1,14 +1,14 @@ - webhooks + webhook_listeners Nextcloud webhook support Nextcloud webhook support Nextcloud webhook support 1.0.0-dev agpl Côme Chilliet - Webhooks + WebhookListeners @@ -24,10 +24,10 @@ - OCA\Webhooks\Command\ListWebhooks + OCA\WebhookListeners\Command\ListWebhooks - OCA\Webhooks\Settings\Admin + OCA\WebhookListeners\Settings\Admin diff --git a/apps/webhooks/composer/autoload.php b/apps/webhook_listeners/composer/autoload.php similarity index 92% rename from apps/webhooks/composer/autoload.php rename to apps/webhook_listeners/composer/autoload.php index 81ca354714949..fa45003779ed7 100644 --- a/apps/webhooks/composer/autoload.php +++ b/apps/webhook_listeners/composer/autoload.php @@ -22,4 +22,4 @@ require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInitWebhooks::getLoader(); +return ComposerAutoloaderInitWebhookListeners::getLoader(); diff --git a/apps/webhooks/composer/composer.json b/apps/webhook_listeners/composer/composer.json similarity index 65% rename from apps/webhooks/composer/composer.json rename to apps/webhook_listeners/composer/composer.json index 5511396d89607..b9eb07e50ab09 100644 --- a/apps/webhooks/composer/composer.json +++ b/apps/webhook_listeners/composer/composer.json @@ -3,11 +3,11 @@ "vendor-dir": ".", "optimize-autoloader": true, "classmap-authoritative": true, - "autoloader-suffix": "Webhooks" + "autoloader-suffix": "WebhookListeners" }, "autoload" : { "psr-4": { - "OCA\\Webhooks\\": "../lib/" + "OCA\\WebhookListeners\\": "../lib/" } } } diff --git a/apps/webhooks/composer/composer.lock b/apps/webhook_listeners/composer/composer.lock similarity index 100% rename from apps/webhooks/composer/composer.lock rename to apps/webhook_listeners/composer/composer.lock diff --git a/apps/webhooks/composer/composer/ClassLoader.php b/apps/webhook_listeners/composer/composer/ClassLoader.php similarity index 100% rename from apps/webhooks/composer/composer/ClassLoader.php rename to apps/webhook_listeners/composer/composer/ClassLoader.php diff --git a/apps/webhooks/composer/composer/InstalledVersions.php b/apps/webhook_listeners/composer/composer/InstalledVersions.php similarity index 100% rename from apps/webhooks/composer/composer/InstalledVersions.php rename to apps/webhook_listeners/composer/composer/InstalledVersions.php diff --git a/apps/webhooks/composer/composer/LICENSE b/apps/webhook_listeners/composer/composer/LICENSE similarity index 100% rename from apps/webhooks/composer/composer/LICENSE rename to apps/webhook_listeners/composer/composer/LICENSE diff --git a/apps/webhook_listeners/composer/composer/autoload_classmap.php b/apps/webhook_listeners/composer/composer/autoload_classmap.php new file mode 100644 index 0000000000000..0501a86df2ce8 --- /dev/null +++ b/apps/webhook_listeners/composer/composer/autoload_classmap.php @@ -0,0 +1,22 @@ + $vendorDir . '/composer/InstalledVersions.php', + 'OCA\\WebhookListeners\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php', + 'OCA\\WebhookListeners\\Command\\ListWebhooks' => $baseDir . '/../lib/Command/ListWebhooks.php', + 'OCA\\WebhookListeners\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php', + 'OCA\\WebhookListeners\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php', + 'OCA\\WebhookListeners\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php', + 'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php', + 'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php', + 'OCA\\WebhookListeners\\Migration\\Version1000Date20240527153425' => $baseDir . '/../lib/Migration/Version1000Date20240527153425.php', + 'OCA\\WebhookListeners\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', + 'OCA\\WebhookListeners\\Service\\PHPMongoQuery' => $baseDir . '/../lib/Service/PHPMongoQuery.php', + 'OCA\\WebhookListeners\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', +); diff --git a/apps/webhooks/composer/composer/autoload_namespaces.php b/apps/webhook_listeners/composer/composer/autoload_namespaces.php similarity index 100% rename from apps/webhooks/composer/composer/autoload_namespaces.php rename to apps/webhook_listeners/composer/composer/autoload_namespaces.php diff --git a/apps/webhooks/composer/composer/autoload_psr4.php b/apps/webhook_listeners/composer/composer/autoload_psr4.php similarity index 66% rename from apps/webhooks/composer/composer/autoload_psr4.php rename to apps/webhook_listeners/composer/composer/autoload_psr4.php index 987a95f989d4d..85f48c55bdd5c 100644 --- a/apps/webhooks/composer/composer/autoload_psr4.php +++ b/apps/webhook_listeners/composer/composer/autoload_psr4.php @@ -6,5 +6,5 @@ $baseDir = $vendorDir; return array( - 'OCA\\Webhooks\\' => array($baseDir . '/../lib'), + 'OCA\\WebhookListeners\\' => array($baseDir . '/../lib'), ); diff --git a/apps/webhooks/composer/composer/autoload_real.php b/apps/webhook_listeners/composer/composer/autoload_real.php similarity index 84% rename from apps/webhooks/composer/composer/autoload_real.php rename to apps/webhook_listeners/composer/composer/autoload_real.php index 1b1742c422ea5..336058c2e24cd 100644 --- a/apps/webhooks/composer/composer/autoload_real.php +++ b/apps/webhook_listeners/composer/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInitWebhooks +class ComposerAutoloaderInitWebhookListeners { private static $loader; @@ -22,12 +22,12 @@ public static function getLoader() return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInitWebhooks', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInitWebhookListeners', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); - spl_autoload_unregister(array('ComposerAutoloaderInitWebhooks', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInitWebhookListeners', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInitWebhooks::getInitializer($loader)); + call_user_func(\Composer\Autoload\ComposerStaticInitWebhookListeners::getInitializer($loader)); $loader->setClassMapAuthoritative(true); $loader->register(true); diff --git a/apps/webhook_listeners/composer/composer/autoload_static.php b/apps/webhook_listeners/composer/composer/autoload_static.php new file mode 100644 index 0000000000000..43a9b4779d9c6 --- /dev/null +++ b/apps/webhook_listeners/composer/composer/autoload_static.php @@ -0,0 +1,48 @@ + + array ( + 'OCA\\WebhookListeners\\' => 21, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'OCA\\WebhookListeners\\' => + array ( + 0 => __DIR__ . '/..' . '/../lib', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'OCA\\WebhookListeners\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php', + 'OCA\\WebhookListeners\\Command\\ListWebhooks' => __DIR__ . '/..' . '/../lib/Command/ListWebhooks.php', + 'OCA\\WebhookListeners\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php', + 'OCA\\WebhookListeners\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php', + 'OCA\\WebhookListeners\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php', + 'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php', + 'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php', + 'OCA\\WebhookListeners\\Migration\\Version1000Date20240527153425' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20240527153425.php', + 'OCA\\WebhookListeners\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', + 'OCA\\WebhookListeners\\Service\\PHPMongoQuery' => __DIR__ . '/..' . '/../lib/Service/PHPMongoQuery.php', + 'OCA\\WebhookListeners\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitWebhookListeners::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitWebhookListeners::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitWebhookListeners::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/apps/webhooks/composer/composer/installed.json b/apps/webhook_listeners/composer/composer/installed.json similarity index 100% rename from apps/webhooks/composer/composer/installed.json rename to apps/webhook_listeners/composer/composer/installed.json diff --git a/apps/webhooks/composer/composer/installed.php b/apps/webhook_listeners/composer/composer/installed.php similarity index 100% rename from apps/webhooks/composer/composer/installed.php rename to apps/webhook_listeners/composer/composer/installed.php diff --git a/apps/webhooks/img/app-dark.svg b/apps/webhook_listeners/img/app-dark.svg similarity index 100% rename from apps/webhooks/img/app-dark.svg rename to apps/webhook_listeners/img/app-dark.svg diff --git a/apps/webhooks/img/app.svg b/apps/webhook_listeners/img/app.svg similarity index 100% rename from apps/webhooks/img/app.svg rename to apps/webhook_listeners/img/app.svg diff --git a/apps/webhooks/lib/AppInfo/Application.php b/apps/webhook_listeners/lib/AppInfo/Application.php similarity index 87% rename from apps/webhooks/lib/AppInfo/Application.php rename to apps/webhook_listeners/lib/AppInfo/Application.php index 22abec747927f..d1ffa5db49ba9 100644 --- a/apps/webhooks/lib/AppInfo/Application.php +++ b/apps/webhook_listeners/lib/AppInfo/Application.php @@ -7,10 +7,10 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\AppInfo; +namespace OCA\WebhookListeners\AppInfo; -use OCA\Webhooks\Db\WebhookListenerMapper; -use OCA\Webhooks\Listener\WebhooksEventListener; +use OCA\WebhookListeners\Db\WebhookListenerMapper; +use OCA\WebhookListeners\Listener\WebhooksEventListener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -20,7 +20,7 @@ use Psr\Log\LoggerInterface; class Application extends App implements IBootstrap { - public const APP_ID = 'webhooks'; + public const APP_ID = 'webhook_listeners'; public function __construct() { parent::__construct(self::APP_ID); diff --git a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php b/apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php similarity index 93% rename from apps/webhooks/lib/BackgroundJobs/WebhookCall.php rename to apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php index 4edc76fbea999..9689d4cb58550 100644 --- a/apps/webhooks/lib/BackgroundJobs/WebhookCall.php +++ b/apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php @@ -7,10 +7,10 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\BackgroundJobs; +namespace OCA\WebhookListeners\BackgroundJobs; -use OCA\Webhooks\Db\AuthMethod; -use OCA\Webhooks\Db\WebhookListenerMapper; +use OCA\WebhookListeners\Db\AuthMethod; +use OCA\WebhookListeners\Db\WebhookListenerMapper; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\QueuedJob; use OCP\Http\Client\IClientService; diff --git a/apps/webhooks/lib/Command/ListWebhooks.php b/apps/webhook_listeners/lib/Command/ListWebhooks.php similarity index 80% rename from apps/webhooks/lib/Command/ListWebhooks.php rename to apps/webhook_listeners/lib/Command/ListWebhooks.php index c8cca6f860994..157097f3f1540 100644 --- a/apps/webhooks/lib/Command/ListWebhooks.php +++ b/apps/webhook_listeners/lib/Command/ListWebhooks.php @@ -7,11 +7,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Command; +namespace OCA\WebhookListeners\Command; use OC\Core\Command\Base; -use OCA\Webhooks\Db\WebhookListener; -use OCA\Webhooks\Db\WebhookListenerMapper; +use OCA\WebhookListeners\Db\WebhookListener; +use OCA\WebhookListeners\Db\WebhookListenerMapper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -25,8 +25,8 @@ public function __construct( protected function configure(): void { parent::configure(); $this - ->setName('webhooks:list') - ->setDescription('Lists configured webhooks'); + ->setName('webhook_listeners:list') + ->setDescription('Lists configured webhook listeners'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/apps/webhooks/lib/Controller/WebhooksController.php b/apps/webhook_listeners/lib/Controller/WebhooksController.php similarity index 92% rename from apps/webhooks/lib/Controller/WebhooksController.php rename to apps/webhook_listeners/lib/Controller/WebhooksController.php index e3df0897bbe52..88a6e473d8526 100644 --- a/apps/webhooks/lib/Controller/WebhooksController.php +++ b/apps/webhook_listeners/lib/Controller/WebhooksController.php @@ -7,11 +7,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Controller; +namespace OCA\WebhookListeners\Controller; -use OCA\Webhooks\Db\AuthMethod; -use OCA\Webhooks\Db\WebhookListenerMapper; -use OCA\Webhooks\ResponseDefinitions; +use OCA\WebhookListeners\Db\AuthMethod; +use OCA\WebhookListeners\Db\WebhookListenerMapper; +use OCA\WebhookListeners\ResponseDefinitions; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\Attribute\OpenAPI; @@ -48,7 +48,7 @@ public function __construct( * 200: Webhook registrations returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks')] - #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] + #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] public function index(): DataResponse { $webhookListeners = $this->mapper->getAll(); @@ -65,7 +65,7 @@ public function index(): DataResponse { * 200: Webhook registration returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks/{id}')] - #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] + #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] public function show(int $id): DataResponse { return new DataResponse($this->mapper->getById($id)); } @@ -90,7 +90,7 @@ public function show(int $id): DataResponse { * @throws OCSException Other error */ #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks')] - #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] + #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] public function create( string $httpMethod, string $uri, @@ -149,7 +149,7 @@ public function create( * @throws OCSException Other error */ #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks/{id}')] - #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] + #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] public function update( int $id, string $httpMethod, @@ -203,7 +203,7 @@ public function update( * @throws OCSException Other error */ #[ApiRoute(verb: 'DELETE', url: '/api/v1/webhooks/{id}')] - #[AuthorizedAdminSetting(settings:'OCA\Webhooks\Settings\Admin')] + #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] public function destroy(int $id): DataResponse { try { $deleted = $this->mapper->deleteById($id); diff --git a/apps/webhooks/lib/Db/AuthMethod.php b/apps/webhook_listeners/lib/Db/AuthMethod.php similarity index 87% rename from apps/webhooks/lib/Db/AuthMethod.php rename to apps/webhook_listeners/lib/Db/AuthMethod.php index 4fe06ef34fba8..ab8bff76eb7f6 100644 --- a/apps/webhooks/lib/Db/AuthMethod.php +++ b/apps/webhook_listeners/lib/Db/AuthMethod.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Db; +namespace OCA\WebhookListeners\Db; enum AuthMethod: string { case None = 'none'; diff --git a/apps/webhooks/lib/Db/WebhookListener.php b/apps/webhook_listeners/lib/Db/WebhookListener.php similarity index 98% rename from apps/webhooks/lib/Db/WebhookListener.php rename to apps/webhook_listeners/lib/Db/WebhookListener.php index c4053b5ba7b92..0d08082666f96 100644 --- a/apps/webhooks/lib/Db/WebhookListener.php +++ b/apps/webhook_listeners/lib/Db/WebhookListener.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Db; +namespace OCA\WebhookListeners\Db; use OCP\AppFramework\Db\Entity; use OCP\Security\ICrypto; diff --git a/apps/webhooks/lib/Db/WebhookListenerMapper.php b/apps/webhook_listeners/lib/Db/WebhookListenerMapper.php similarity index 99% rename from apps/webhooks/lib/Db/WebhookListenerMapper.php rename to apps/webhook_listeners/lib/Db/WebhookListenerMapper.php index 4094b5e267967..97e01062f2f86 100644 --- a/apps/webhooks/lib/Db/WebhookListenerMapper.php +++ b/apps/webhook_listeners/lib/Db/WebhookListenerMapper.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Db; +namespace OCA\WebhookListeners\Db; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; diff --git a/apps/webhooks/lib/Listener/WebhooksEventListener.php b/apps/webhook_listeners/lib/Listener/WebhooksEventListener.php similarity index 89% rename from apps/webhooks/lib/Listener/WebhooksEventListener.php rename to apps/webhook_listeners/lib/Listener/WebhooksEventListener.php index 37d6863553a48..72d48d790e19e 100644 --- a/apps/webhooks/lib/Listener/WebhooksEventListener.php +++ b/apps/webhook_listeners/lib/Listener/WebhooksEventListener.php @@ -7,11 +7,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Listener; +namespace OCA\WebhookListeners\Listener; -use OCA\Webhooks\BackgroundJobs\WebhookCall; -use OCA\Webhooks\Db\WebhookListenerMapper; -use OCA\Webhooks\Service\PHPMongoQuery; +use OCA\WebhookListeners\BackgroundJobs\WebhookCall; +use OCA\WebhookListeners\Db\WebhookListenerMapper; +use OCA\WebhookListeners\Service\PHPMongoQuery; use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; diff --git a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php b/apps/webhook_listeners/lib/Migration/Version1000Date20240527153425.php similarity index 94% rename from apps/webhooks/lib/Migration/Version1000Date20240527153425.php rename to apps/webhook_listeners/lib/Migration/Version1000Date20240527153425.php index a54702de6c061..44f2476dd44c5 100755 --- a/apps/webhooks/lib/Migration/Version1000Date20240527153425.php +++ b/apps/webhook_listeners/lib/Migration/Version1000Date20240527153425.php @@ -7,10 +7,10 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Migration; +namespace OCA\WebhookListeners\Migration; use Closure; -use OCA\Webhooks\Db\WebhookListenerMapper; +use OCA\WebhookListeners\Db\WebhookListenerMapper; use OCP\DB\ISchemaWrapper; use OCP\DB\Types; use OCP\Migration\IOutput; diff --git a/apps/webhooks/lib/ResponseDefinitions.php b/apps/webhook_listeners/lib/ResponseDefinitions.php similarity index 94% rename from apps/webhooks/lib/ResponseDefinitions.php rename to apps/webhook_listeners/lib/ResponseDefinitions.php index ea31de931acbb..3b9965c20a31f 100644 --- a/apps/webhooks/lib/ResponseDefinitions.php +++ b/apps/webhook_listeners/lib/ResponseDefinitions.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks; +namespace OCA\WebhookListeners; /** * @psalm-type WebhooksListenerInfo = array{ diff --git a/apps/webhooks/lib/Service/PHPMongoQuery.php b/apps/webhook_listeners/lib/Service/PHPMongoQuery.php similarity index 99% rename from apps/webhooks/lib/Service/PHPMongoQuery.php rename to apps/webhook_listeners/lib/Service/PHPMongoQuery.php index 65ba57757634a..e8e52615008b3 100644 --- a/apps/webhooks/lib/Service/PHPMongoQuery.php +++ b/apps/webhook_listeners/lib/Service/PHPMongoQuery.php @@ -6,7 +6,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Service; +namespace OCA\WebhookListeners\Service; use Exception; diff --git a/apps/webhooks/lib/Settings/Admin.php b/apps/webhook_listeners/lib/Settings/Admin.php similarity index 96% rename from apps/webhooks/lib/Settings/Admin.php rename to apps/webhook_listeners/lib/Settings/Admin.php index 748b8536e4f61..e5e0d00221c20 100644 --- a/apps/webhooks/lib/Settings/Admin.php +++ b/apps/webhook_listeners/lib/Settings/Admin.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Settings; +namespace OCA\WebhookListeners\Settings; use OCP\AppFramework\Http\TemplateResponse; use OCP\IL10N; diff --git a/apps/webhooks/openapi.json b/apps/webhook_listeners/openapi.json similarity index 99% rename from apps/webhooks/openapi.json rename to apps/webhook_listeners/openapi.json index bdcc8007270b4..6d8a10fcdea0a 100644 --- a/apps/webhooks/openapi.json +++ b/apps/webhook_listeners/openapi.json @@ -736,4 +736,4 @@ } }, "tags": [] -} \ No newline at end of file +} diff --git a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php b/apps/webhook_listeners/tests/Db/WebhookListenerMapperTest.php similarity index 94% rename from apps/webhooks/tests/Db/WebhookListenerMapperTest.php rename to apps/webhook_listeners/tests/Db/WebhookListenerMapperTest.php index 76e58f0d2b726..b385cff1228f4 100644 --- a/apps/webhooks/tests/Db/WebhookListenerMapperTest.php +++ b/apps/webhook_listeners/tests/Db/WebhookListenerMapperTest.php @@ -7,10 +7,10 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Tests\Db; +namespace OCA\WebhookListeners\Tests\Db; -use OCA\Webhooks\Db\AuthMethod; -use OCA\Webhooks\Db\WebhookListenerMapper; +use OCA\WebhookListeners\Db\AuthMethod; +use OCA\WebhookListeners\Db\WebhookListenerMapper; use OCP\Files\Events\Node\NodeWrittenEvent; use OCP\ICacheFactory; use OCP\IDBConnection; diff --git a/apps/webhooks/tests/Service/PHPMongoQueryTest.php b/apps/webhook_listeners/tests/Service/PHPMongoQueryTest.php similarity index 92% rename from apps/webhooks/tests/Service/PHPMongoQueryTest.php rename to apps/webhook_listeners/tests/Service/PHPMongoQueryTest.php index 51684bb8e34fb..071330a79e319 100644 --- a/apps/webhooks/tests/Service/PHPMongoQueryTest.php +++ b/apps/webhook_listeners/tests/Service/PHPMongoQueryTest.php @@ -7,9 +7,9 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OCA\Webhooks\Tests\Service; +namespace OCA\WebhookListeners\Tests\Service; -use OCA\Webhooks\Service\PHPMongoQuery; +use OCA\WebhookListeners\Service\PHPMongoQuery; use OCP\Files\Events\Node\NodeWrittenEvent; use Test\TestCase; diff --git a/apps/webhooks/composer/composer/autoload_classmap.php b/apps/webhooks/composer/composer/autoload_classmap.php deleted file mode 100644 index bc6625e3c93a9..0000000000000 --- a/apps/webhooks/composer/composer/autoload_classmap.php +++ /dev/null @@ -1,22 +0,0 @@ - $vendorDir . '/composer/InstalledVersions.php', - 'OCA\\Webhooks\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', - 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php', - 'OCA\\Webhooks\\Command\\ListWebhooks' => $baseDir . '/../lib/Command/ListWebhooks.php', - 'OCA\\Webhooks\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php', - 'OCA\\Webhooks\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php', - 'OCA\\Webhooks\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php', - 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php', - 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php', - 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => $baseDir . '/../lib/Migration/Version1000Date20240527153425.php', - 'OCA\\Webhooks\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', - 'OCA\\Webhooks\\Service\\PHPMongoQuery' => $baseDir . '/../lib/Service/PHPMongoQuery.php', - 'OCA\\Webhooks\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', -); diff --git a/apps/webhooks/composer/composer/autoload_static.php b/apps/webhooks/composer/composer/autoload_static.php deleted file mode 100644 index d06810391ec16..0000000000000 --- a/apps/webhooks/composer/composer/autoload_static.php +++ /dev/null @@ -1,48 +0,0 @@ - - array ( - 'OCA\\Webhooks\\' => 13, - ), - ); - - public static $prefixDirsPsr4 = array ( - 'OCA\\Webhooks\\' => - array ( - 0 => __DIR__ . '/..' . '/../lib', - ), - ); - - public static $classMap = array ( - 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', - 'OCA\\Webhooks\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', - 'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php', - 'OCA\\Webhooks\\Command\\ListWebhooks' => __DIR__ . '/..' . '/../lib/Command/ListWebhooks.php', - 'OCA\\Webhooks\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php', - 'OCA\\Webhooks\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php', - 'OCA\\Webhooks\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php', - 'OCA\\Webhooks\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php', - 'OCA\\Webhooks\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php', - 'OCA\\Webhooks\\Migration\\Version1000Date20240527153425' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20240527153425.php', - 'OCA\\Webhooks\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', - 'OCA\\Webhooks\\Service\\PHPMongoQuery' => __DIR__ . '/..' . '/../lib/Service/PHPMongoQuery.php', - 'OCA\\Webhooks\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', - ); - - public static function getInitializer(ClassLoader $loader) - { - return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInitWebhooks::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInitWebhooks::$prefixDirsPsr4; - $loader->classMap = ComposerStaticInitWebhooks::$classMap; - - }, null, ClassLoader::class); - } -} diff --git a/lib/composer/composer/autoload_psr4.php b/lib/composer/composer/autoload_psr4.php index 74e48cf69ae28..7bf40f7a6b581 100644 --- a/lib/composer/composer/autoload_psr4.php +++ b/lib/composer/composer/autoload_psr4.php @@ -9,5 +9,6 @@ 'OC\\Core\\' => array($baseDir . '/core'), 'OC\\' => array($baseDir . '/lib/private'), 'OCP\\' => array($baseDir . '/lib/public'), + 'Bamarni\\Composer\\Bin\\' => array($vendorDir . '/bamarni/composer-bin-plugin/src'), '' => array($baseDir . '/lib/private/legacy'), ); diff --git a/psalm.xml b/psalm.xml index 2f1e226b5cf7a..f2aed4b382b2e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -45,6 +45,7 @@ + diff --git a/tests/enable_all.php b/tests/enable_all.php index 35217db079a96..db01de6ec4110 100644 --- a/tests/enable_all.php +++ b/tests/enable_all.php @@ -24,4 +24,4 @@ function enableApp($app) { enableApp('federation'); enableApp('federatedfilesharing'); enableApp('admin_audit'); -enableApp('webhooks'); +enableApp('webhook_listeners'); From f09c7815db6511ea24b0b551316c8ba07732f6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 10 Jun 2024 18:12:41 +0200 Subject: [PATCH 29/32] fix(webhooks): Fix a few of psalm errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../lib/Controller/WebhooksController.php | 1 + apps/webhook_listeners/lib/Db/WebhookListener.php | 7 ++++++- .../lib/Listener/WebhooksEventListener.php | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/webhook_listeners/lib/Controller/WebhooksController.php b/apps/webhook_listeners/lib/Controller/WebhooksController.php index 88a6e473d8526..c09da53d5f2c4 100644 --- a/apps/webhook_listeners/lib/Controller/WebhooksController.php +++ b/apps/webhook_listeners/lib/Controller/WebhooksController.php @@ -12,6 +12,7 @@ use OCA\WebhookListeners\Db\AuthMethod; use OCA\WebhookListeners\Db\WebhookListenerMapper; use OCA\WebhookListeners\ResponseDefinitions; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\Attribute\OpenAPI; diff --git a/apps/webhook_listeners/lib/Db/WebhookListener.php b/apps/webhook_listeners/lib/Db/WebhookListener.php index 0d08082666f96..222e6b981bd91 100644 --- a/apps/webhook_listeners/lib/Db/WebhookListener.php +++ b/apps/webhook_listeners/lib/Db/WebhookListener.php @@ -15,7 +15,12 @@ /** * @method void setUserId(string $userId) * @method string getUserId() + * @method string getHttpMethod() + * @method string getUri() * @method ?array getHeaders() + * @method ?string getAuthData() + * @method void setAuthData(?string $data) + * @method ?string getAuthMethod() */ class WebhookListener extends Entity implements \JsonSerializable { /** @var ?string id of the app_api application who added the webhook listener */ @@ -66,7 +71,7 @@ public function __construct( } public function getAuthMethodEnum(): AuthMethod { - return AuthMethod::from(parent::getAuthMethod()); + return AuthMethod::from($this->getAuthMethod()); } public function getAuthDataClear(): array { diff --git a/apps/webhook_listeners/lib/Listener/WebhooksEventListener.php b/apps/webhook_listeners/lib/Listener/WebhooksEventListener.php index 72d48d790e19e..5ea4d531c9f0e 100644 --- a/apps/webhook_listeners/lib/Listener/WebhooksEventListener.php +++ b/apps/webhook_listeners/lib/Listener/WebhooksEventListener.php @@ -22,7 +22,7 @@ /** * The class to handle the share events - * @template-implements IEventListener + * @template-implements IEventListener */ class WebhooksEventListener implements IEventListener { public function __construct( From d5b53be449bb00f78232c7948c8cfbcb2303a38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 11 Jun 2024 09:53:56 +0200 Subject: [PATCH 30/32] fix(webhooks): Fix openapi reponse definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../lib/Controller/WebhooksController.php | 10 +-- .../lib/ResponseDefinitions.php | 2 +- apps/webhook_listeners/openapi.json | 66 +++++++++---------- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/apps/webhook_listeners/lib/Controller/WebhooksController.php b/apps/webhook_listeners/lib/Controller/WebhooksController.php index c09da53d5f2c4..42870825877b7 100644 --- a/apps/webhook_listeners/lib/Controller/WebhooksController.php +++ b/apps/webhook_listeners/lib/Controller/WebhooksController.php @@ -26,7 +26,7 @@ use Psr\Log\LoggerInterface; /** - * @psalm-import-type WebhooksListenerInfo from ResponseDefinitions + * @psalm-import-type WebhookListenersWebhookInfo from ResponseDefinitions */ #[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION)] class WebhooksController extends OCSController { @@ -44,7 +44,7 @@ public function __construct( /** * List registered webhooks * - * @return DataResponse + * @return DataResponse * * 200: Webhook registrations returned */ @@ -61,7 +61,7 @@ public function index(): DataResponse { * * @param int $id id of the webhook * - * @return DataResponse + * @return DataResponse * * 200: Webhook registration returned */ @@ -82,7 +82,7 @@ public function show(int $id): DataResponse { * @param "none"|"headers"|null $authMethod Authentication method to use * @param ?array $authData Array of data for authentication * - * @return DataResponse + * @return DataResponse * * 200: Webhook registration returned * @@ -141,7 +141,7 @@ public function create( * @param "none"|"headers"|null $authMethod Authentication method to use * @param ?array $authData Array of data for authentication * - * @return DataResponse + * @return DataResponse * * 200: Webhook registration returned * diff --git a/apps/webhook_listeners/lib/ResponseDefinitions.php b/apps/webhook_listeners/lib/ResponseDefinitions.php index 3b9965c20a31f..cb33f93e8ffda 100644 --- a/apps/webhook_listeners/lib/ResponseDefinitions.php +++ b/apps/webhook_listeners/lib/ResponseDefinitions.php @@ -10,7 +10,7 @@ namespace OCA\WebhookListeners; /** - * @psalm-type WebhooksListenerInfo = array{ + * @psalm-type WebhookListenersWebhookInfo = array{ * id: string, * userId: string, * httpMethod: string, diff --git a/apps/webhook_listeners/openapi.json b/apps/webhook_listeners/openapi.json index 6d8a10fcdea0a..308f0e8b11df4 100644 --- a/apps/webhook_listeners/openapi.json +++ b/apps/webhook_listeners/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.3", "info": { - "title": "webhooks", + "title": "webhook_listeners", "version": "0.0.1", "description": "Nextcloud webhook support", "license": { @@ -20,7 +20,31 @@ } }, "schemas": { - "ListenerInfo": { + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + }, + "WebhookInfo": { "type": "object", "required": [ "id", @@ -67,35 +91,11 @@ } } } - }, - "OCSMeta": { - "type": "object", - "required": [ - "status", - "statuscode" - ], - "properties": { - "status": { - "type": "string" - }, - "statuscode": { - "type": "integer" - }, - "message": { - "type": "string" - }, - "totalitems": { - "type": "string" - }, - "itemsperpage": { - "type": "string" - } - } } } }, "paths": { - "/ocs/v2.php/apps/webhooks/api/v1/webhooks": { + "/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks": { "get": { "operationId": "webhooks-index", "summary": "List registered webhooks", @@ -147,7 +147,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ListenerInfo" + "$ref": "#/components/schemas/WebhookInfo" } } } @@ -275,7 +275,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/ListenerInfo" + "$ref": "#/components/schemas/WebhookInfo" } } } @@ -343,7 +343,7 @@ } } }, - "/ocs/v2.php/apps/webhooks/api/v1/webhooks/{id}": { + "/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/{id}": { "get": { "operationId": "webhooks-show", "summary": "Get details on a registered webhook", @@ -403,7 +403,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/ListenerInfo" + "$ref": "#/components/schemas/WebhookInfo" } } } @@ -540,7 +540,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/ListenerInfo" + "$ref": "#/components/schemas/WebhookInfo" } } } @@ -736,4 +736,4 @@ } }, "tags": [] -} +} \ No newline at end of file From bff7d3c76958740419074d8436416a0b8cc25daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 11 Jun 2024 11:03:45 +0200 Subject: [PATCH 31/32] fix(webhooks): Fix last psalm and openapi problems with the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../lib/Controller/WebhooksController.php | 53 +++++++++++++++---- apps/webhook_listeners/lib/Settings/Admin.php | 4 +- apps/webhook_listeners/openapi.json | 28 ++++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/apps/webhook_listeners/lib/Controller/WebhooksController.php b/apps/webhook_listeners/lib/Controller/WebhooksController.php index 42870825877b7..e7a05f0983ea7 100644 --- a/apps/webhook_listeners/lib/Controller/WebhooksController.php +++ b/apps/webhook_listeners/lib/Controller/WebhooksController.php @@ -10,8 +10,10 @@ namespace OCA\WebhookListeners\Controller; use OCA\WebhookListeners\Db\AuthMethod; +use OCA\WebhookListeners\Db\WebhookListener; use OCA\WebhookListeners\Db\WebhookListenerMapper; use OCA\WebhookListeners\ResponseDefinitions; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; @@ -20,6 +22,7 @@ use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\ISession; @@ -45,15 +48,26 @@ public function __construct( * List registered webhooks * * @return DataResponse + * @throws OCSException Other internal error * * 200: Webhook registrations returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks')] #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] public function index(): DataResponse { - $webhookListeners = $this->mapper->getAll(); + try { + $webhookListeners = $this->mapper->getAll(); - return new DataResponse($webhookListeners); + return new DataResponse( + array_map( + fn (WebhookListener $listener): array => $listener->jsonSerialize(), + $webhookListeners + ) + ); + } catch (\Exception $e) { + $this->logger->error('Error when listing webhooks', ['exception' => $e]); + throw new OCSException('An internal error occurred', Http::STATUS_INTERNAL_SERVER_ERROR, $e); + } } /** @@ -62,13 +76,22 @@ public function index(): DataResponse { * @param int $id id of the webhook * * @return DataResponse + * @throws OCSNotFoundException Webhook not found + * @throws OCSException Other internal error * * 200: Webhook registration returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks/{id}')] #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] public function show(int $id): DataResponse { - return new DataResponse($this->mapper->getById($id)); + try { + return new DataResponse($this->mapper->getById($id)->jsonSerialize()); + } catch (DoesNotExistException $e) { + throw new OCSNotFoundException($e->getMessage(), $e); + } catch (\Exception $e) { + $this->logger->error('Error when getting webhook', ['exception' => $e]); + throw new OCSException('An internal error occurred', Http::STATUS_INTERNAL_SERVER_ERROR, $e); + } } /** @@ -106,6 +129,11 @@ public function create( if ($this->session->get('app_api') === true) { $appId = $this->request->getHeader('EX-APP-ID'); } + try { + $authMethod = AuthMethod::from($authMethod ?? AuthMethod::None->value); + } catch (\ValueError $e) { + throw new OCSBadRequestException('This auth method does not exist'); + } try { $webhookListener = $this->mapper->addWebhookListener( $appId, @@ -115,17 +143,17 @@ public function create( $event, $eventFilter, $headers, - AuthMethod::from($authMethod ?? AuthMethod::None->value), + $authMethod, $authData, ); - return new DataResponse($webhookListener); + return new DataResponse($webhookListener->jsonSerialize()); } catch (\UnexpectedValueException $e) { throw new OCSBadRequestException($e->getMessage(), $e); } catch (\DomainException $e) { throw new OCSForbiddenException($e->getMessage(), $e); } catch (\Exception $e) { $this->logger->error('Error when inserting webhook', ['exception' => $e]); - throw new OCSException('An internal error occurred', $e->getCode(), $e); + throw new OCSException('An internal error occurred', Http::STATUS_INTERNAL_SERVER_ERROR, $e); } } @@ -166,6 +194,11 @@ public function update( if ($this->session->get('app_api') === true) { $appId = $this->request->getHeader('EX-APP-ID'); } + try { + $authMethod = AuthMethod::from($authMethod ?? AuthMethod::None->value); + } catch (\ValueError $e) { + throw new OCSBadRequestException('This auth method does not exist'); + } try { $webhookListener = $this->mapper->updateWebhookListener( $id, @@ -176,17 +209,17 @@ public function update( $event, $eventFilter, $headers, - AuthMethod::from($authMethod ?? AuthMethod::None->value), + $authMethod, $authData, ); - return new DataResponse($webhookListener); + return new DataResponse($webhookListener->jsonSerialize()); } catch (\UnexpectedValueException $e) { throw new OCSBadRequestException($e->getMessage(), $e); } catch (\DomainException $e) { throw new OCSForbiddenException($e->getMessage(), $e); } catch (\Exception $e) { $this->logger->error('Error when updating flow with id ' . $id, ['exception' => $e]); - throw new OCSException('An internal error occurred', $e->getCode(), $e); + throw new OCSException('An internal error occurred', Http::STATUS_INTERNAL_SERVER_ERROR, $e); } } @@ -215,7 +248,7 @@ public function destroy(int $id): DataResponse { throw new OCSForbiddenException($e->getMessage(), $e); } catch (\Exception $e) { $this->logger->error('Error when deleting flow with id ' . $id, ['exception' => $e]); - throw new OCSException('An internal error occurred', $e->getCode(), $e); + throw new OCSException('An internal error occurred', Http::STATUS_INTERNAL_SERVER_ERROR, $e); } } } diff --git a/apps/webhook_listeners/lib/Settings/Admin.php b/apps/webhook_listeners/lib/Settings/Admin.php index e5e0d00221c20..5ef7656ca3e5e 100644 --- a/apps/webhook_listeners/lib/Settings/Admin.php +++ b/apps/webhook_listeners/lib/Settings/Admin.php @@ -9,6 +9,7 @@ namespace OCA\WebhookListeners\Settings; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\TemplateResponse; use OCP\IL10N; use OCP\Settings\IDelegatedSettings; @@ -28,7 +29,8 @@ public function __construct( * Empty template response */ public function getForm(): TemplateResponse { - return new class($this->appName, '') extends TemplateResponse { + + return new /** @template-extends TemplateResponse */ class($this->appName, '') extends TemplateResponse { public function render(): string { return ''; } diff --git a/apps/webhook_listeners/openapi.json b/apps/webhook_listeners/openapi.json index 308f0e8b11df4..8488ce8e11758 100644 --- a/apps/webhook_listeners/openapi.json +++ b/apps/webhook_listeners/openapi.json @@ -411,6 +411,34 @@ } } } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } } } }, From e0b9ff4fa255b4dd4c1e3881e26dff18053e129a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 11 Jun 2024 14:40:29 +0200 Subject: [PATCH 32/32] fix(webhooks): Fix a few more psalm notices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../lib/BackgroundJobs/WebhookCall.php | 3 + .../lib/Controller/WebhooksController.php | 15 +++-- .../lib/Db/WebhookListener.php | 58 ++++++++++++++----- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php b/apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php index 9689d4cb58550..9c9a4bb6dbe37 100644 --- a/apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php +++ b/apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php @@ -28,6 +28,9 @@ public function __construct( parent::__construct($timeFactory); } + /** + * @param array $argument + */ protected function run($argument): void { [$data, $webhookId] = $argument; $webhookListener = $this->mapper->getById($webhookId); diff --git a/apps/webhook_listeners/lib/Controller/WebhooksController.php b/apps/webhook_listeners/lib/Controller/WebhooksController.php index e7a05f0983ea7..1c4306eabb8a2 100644 --- a/apps/webhook_listeners/lib/Controller/WebhooksController.php +++ b/apps/webhook_listeners/lib/Controller/WebhooksController.php @@ -13,6 +13,7 @@ use OCA\WebhookListeners\Db\WebhookListener; use OCA\WebhookListeners\Db\WebhookListenerMapper; use OCA\WebhookListeners\ResponseDefinitions; +use OCA\WebhookListeners\Settings\Admin; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -53,7 +54,7 @@ public function __construct( * 200: Webhook registrations returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks')] - #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] + #[AuthorizedAdminSetting(settings:Admin::class)] public function index(): DataResponse { try { $webhookListeners = $this->mapper->getAll(); @@ -82,7 +83,7 @@ public function index(): DataResponse { * 200: Webhook registration returned */ #[ApiRoute(verb: 'GET', url: '/api/v1/webhooks/{id}')] - #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] + #[AuthorizedAdminSetting(settings:Admin::class)] public function show(int $id): DataResponse { try { return new DataResponse($this->mapper->getById($id)->jsonSerialize()); @@ -114,7 +115,7 @@ public function show(int $id): DataResponse { * @throws OCSException Other error */ #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks')] - #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] + #[AuthorizedAdminSetting(settings:Admin::class)] public function create( string $httpMethod, string $uri, @@ -135,6 +136,8 @@ public function create( throw new OCSBadRequestException('This auth method does not exist'); } try { + /* We can never reach here without a user in session */ + assert(is_string($this->userId)); $webhookListener = $this->mapper->addWebhookListener( $appId, $this->userId, @@ -178,7 +181,7 @@ public function create( * @throws OCSException Other error */ #[ApiRoute(verb: 'POST', url: '/api/v1/webhooks/{id}')] - #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] + #[AuthorizedAdminSetting(settings:Admin::class)] public function update( int $id, string $httpMethod, @@ -200,6 +203,8 @@ public function update( throw new OCSBadRequestException('This auth method does not exist'); } try { + /* We can never reach here without a user in session */ + assert(is_string($this->userId)); $webhookListener = $this->mapper->updateWebhookListener( $id, $appId, @@ -237,7 +242,7 @@ public function update( * @throws OCSException Other error */ #[ApiRoute(verb: 'DELETE', url: '/api/v1/webhooks/{id}')] - #[AuthorizedAdminSetting(settings:'OCA\WebhookListeners\Settings\Admin')] + #[AuthorizedAdminSetting(settings:Admin::class)] public function destroy(int $id): DataResponse { try { $deleted = $this->mapper->deleteById($id); diff --git a/apps/webhook_listeners/lib/Db/WebhookListener.php b/apps/webhook_listeners/lib/Db/WebhookListener.php index 222e6b981bd91..e9b63e0147245 100644 --- a/apps/webhook_listeners/lib/Db/WebhookListener.php +++ b/apps/webhook_listeners/lib/Db/WebhookListener.php @@ -20,35 +20,60 @@ * @method ?array getHeaders() * @method ?string getAuthData() * @method void setAuthData(?string $data) - * @method ?string getAuthMethod() + * @method string getAuthMethod() + * @psalm-suppress PropertyNotSetInConstructor */ class WebhookListener extends Entity implements \JsonSerializable { - /** @var ?string id of the app_api application who added the webhook listener */ - protected $appId; - - /** @var string id of the user who added the webhook listener */ + /** + * @var ?string id of the app_api application who added the webhook listener + */ + protected $appId = null; + + /** + * @var string id of the user who added the webhook listener + * @psalm-suppress PropertyNotSetInConstructor + */ protected $userId; - /** @var string */ + /** + * @var string + * @psalm-suppress PropertyNotSetInConstructor + */ protected $httpMethod; - /** @var string */ + /** + * @var string + * @psalm-suppress PropertyNotSetInConstructor + */ protected $uri; - /** @var string */ + /** + * @var string + * @psalm-suppress PropertyNotSetInConstructor + */ protected $event; - /** @var array */ + /** + * @var array + * @psalm-suppress PropertyNotSetInConstructor + */ protected $eventFilter; - /** @var ?array */ - protected $headers; + /** + * @var ?array + */ + protected $headers = null; - /** @var ?string */ + /** + * @var string + * @psalm-suppress PropertyNotSetInConstructor + */ protected $authMethod; - /** @var ?string */ - protected $authData; + /** + * @var ?string + */ + protected $authData = null; private ICrypto $crypto; @@ -75,10 +100,11 @@ public function getAuthMethodEnum(): AuthMethod { } public function getAuthDataClear(): array { - if ($this->authData === null) { + $authData = $this->getAuthData(); + if ($authData === null) { return []; } - return json_decode($this->crypto->decrypt($this->getAuthData()), associative:true, flags:JSON_THROW_ON_ERROR); + return json_decode($this->crypto->decrypt($authData), associative:true, flags:JSON_THROW_ON_ERROR); } public function setAuthDataClear(