From f4882e144852e6a1d3fe38d48d6b87e42ae24129 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:04:05 +0000 Subject: [PATCH 1/6] feat: generate list of OCP events Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> --- .github/workflows/update-ocp-events.yml | 67 ++++ .gitignore | 3 + build/ocp-events/.php-cs-fixer.dist.php | 15 + build/ocp-events/composer.json | 25 ++ build/ocp-events/lib/EventClass.php | 18 ++ build/ocp-events/lib/EventFinder.php | 122 +++++++ .../lib/ReStructuredTextFormatter.php | 56 ++++ build/ocp-events/update_events.php | 29 ++ .../basics/_available_events_ocp.rst | 305 +++++++++++++++--- 9 files changed, 598 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/update-ocp-events.yml create mode 100644 build/ocp-events/.php-cs-fixer.dist.php create mode 100644 build/ocp-events/composer.json create mode 100644 build/ocp-events/lib/EventClass.php create mode 100644 build/ocp-events/lib/EventFinder.php create mode 100644 build/ocp-events/lib/ReStructuredTextFormatter.php create mode 100644 build/ocp-events/update_events.php diff --git a/.github/workflows/update-ocp-events.yml b/.github/workflows/update-ocp-events.yml new file mode 100644 index 00000000000..74c3af3445a --- /dev/null +++ b/.github/workflows/update-ocp-events.yml @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Update OCP events + +on: + workflow_dispatch: + schedule: + - cron: '5 22 * * 6' + +permissions: + contents: read + +jobs: + update-ocp-events: + runs-on: ubuntu-latest + + if: ${{ github.repository_owner == 'nextcloud' }} + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up php + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0 + with: + php-version: '8.5' + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + working-directory: build/ocp-events + + - name: Update events + run: php build/ocp-events/update_events.php + + - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + id: cpr + with: + token: ${{ secrets.COMMAND_BOT_PAT }} + commit-message: 'chore: update list of OCP events' + committer: GitHub + author: nextcloud-command + signoff: true + branch: 'automated/noid/update-ocp-events' + title: 'Update list of OCP events' + add-paths: developer_manual/basics/_available_events_ocp.rst + + - uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0 + if: steps.cpr.outputs.pull-request-operation == 'created' + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} + + - uses: pascalgn/automerge-action@7961b8b5eec56cc088c140b56d864285eabd3f67 # v0.16.4 + if: steps.cpr.outputs.pull-request-operation == 'created' + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MERGE_LABELS: "" + MERGE_RETRIES: 10 + MERGE_RETRY_SLEEP: 60000 + PULL_REQUEST: ${{ steps.cpr.outputs.pull-request-number }} diff --git a/.gitignore b/.gitignore index f330e9f8bc6..259e2813f95 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ build/index.html build/vendor/ build/server/ build/composer.lock +build/ocp-events/vendor/ +build/ocp-events/composer.lock +build/ocp-events/.php-cs-fixer.cache developer_manual/api/ server/ _build diff --git a/build/ocp-events/.php-cs-fixer.dist.php b/build/ocp-events/.php-cs-fixer.dist.php new file mode 100644 index 00000000000..1b4cdb6f06f --- /dev/null +++ b/build/ocp-events/.php-cs-fixer.dist.php @@ -0,0 +1,15 @@ +getFinder() + ->ignoreVCSIgnored(true) + ->notPath('vendor') + ->in(__DIR__); +return $config; \ No newline at end of file diff --git a/build/ocp-events/composer.json b/build/ocp-events/composer.json new file mode 100644 index 00000000000..2275079184b --- /dev/null +++ b/build/ocp-events/composer.json @@ -0,0 +1,25 @@ +{ + "require": { + "nette/robot-loader": "^4.0", + "nextcloud/ocp": "dev-master", + "phpdocumentor/reflection-docblock": "^6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.43", + "nextcloud/coding-standard": "^1.2" + }, + "autoload": { + "psr-4": { + "": "lib" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + } + }, + "scripts": { + "cs:check": "php-cs-fixer fix --dry-run --diff", + "cs:fix": "php-cs-fixer fix" + } +} diff --git a/build/ocp-events/lib/EventClass.php b/build/ocp-events/lib/EventClass.php new file mode 100644 index 00000000000..a28f1c0b339 --- /dev/null +++ b/build/ocp-events/lib/EventClass.php @@ -0,0 +1,18 @@ +classes as $className => $classPath) { + if (in_array($className, self::SKIP_CLASSES)) { + continue; + } + + try { + $reflection = new ReflectionClass($className); + } catch (ReflectionException $e) { + continue; + } + + if ($reflection->isAbstract()) { + continue; + } + + $isEventClass = false; + $parentClass = $reflection->getParentClass(); + + while ($isEventClass === false && $parentClass !== false) { + $isEventClass = $parentClass->getName() === self::EVENT_CLASS; + $parentClass = $parentClass->getParentClass(); + } + + if ($isEventClass === false) { + continue; + } + + $docBlock = $docBlockFactory->create($reflection->getDocComment() ?: '/** */'); + + $sinceTags = array_map(function (\phpDocumentor\Reflection\DocBlock\Tags\Since $tag) { + $data = [ + 'version' => $tag->getVersion(), + 'description' => '', + ]; + + $description = $tag->getDescription(); + if ($description instanceof \phpDocumentor\Reflection\DocBlock\Description) { + $data['description'] = $description->render(); + } + + return $data; + }, $docBlock->getTagsByName('since')); + + if ($sinceTags === []) { + $sinceTags = $this->sinceFromAttributes($reflection); + } + + $result[] = new EventClass( + $reflection->getName(), + $docBlock->getSummary(), + $docBlock->getDescription()->render(), + $sinceTags + ); + } + + usort($result, static function (EventClass $a, EventClass $b) { + return strcasecmp($a->className, $b->className); + }); + + return $result; + } + + private function sinceFromAttributes(ReflectionClass $reflection): array { + $versions = []; + foreach ([\OCP\AppFramework\Attribute\Listenable::class, \OCP\AppFramework\Attribute\Dispatchable::class] as $attributeClass) { + foreach ($reflection->getAttributes($attributeClass) as $attribute) { + $versions[] = $attribute->newInstance()->getSince(); + } + } + + if ($versions === []) { + return []; + } + + usort($versions, 'version_compare'); + return [['version' => $versions[0], 'description' => '']]; + } +} diff --git a/build/ocp-events/lib/ReStructuredTextFormatter.php b/build/ocp-events/lib/ReStructuredTextFormatter.php new file mode 100644 index 00000000000..07d15d468a2 --- /dev/null +++ b/build/ocp-events/lib/ReStructuredTextFormatter.php @@ -0,0 +1,56 @@ +className . '``' . PHP_EOL; + $output .= str_repeat('*', strlen($event->className) + 4) . PHP_EOL; + $output .= PHP_EOL; + + foreach ($event->sinceTags as $i => $sinceTag) { + if ($i === 0) { + $output .= '.. versionadded:: ' . $this->adjustVersion($sinceTag['version']) . PHP_EOL; + if (strlen($sinceTag['description']) > 0) { + $output .= ' ' . $sinceTag['description'] . PHP_EOL; + } + } else { + $output .= '.. versionchanged:: ' . $sinceTag['version'] . PHP_EOL; + if (strlen($sinceTag['description']) > 0) { + $output .= ' ' . $sinceTag['description'] . PHP_EOL; + } + } + $output .= PHP_EOL; + } + + if (strlen($event->summary) > 0) { + $output .= $event->summary . PHP_EOL; + } + + if (strlen($event->description) > 0) { + $output .= $event->description . PHP_EOL; + } + + $output .= PHP_EOL; + + } + + return $output; + } + + private function adjustVersion(string $version): string { + return preg_replace("/(\.0\.0|\.0)$/", '', $version); + } +} diff --git a/build/ocp-events/update_events.php b/build/ocp-events/update_events.php new file mode 100644 index 00000000000..76b92d7075e --- /dev/null +++ b/build/ocp-events/update_events.php @@ -0,0 +1,29 @@ +addDirectory(__DIR__ . '/vendor/nextcloud/ocp/OCP'); +$loader->setTempDirectory(sys_get_temp_dir()); +$loader->register(); + +$loader->rebuild(); +$classes = $loader->getIndexedClasses(); + +$eventFinder = new EventFinder($classes); +$events = $eventFinder->find(); + +$formatter = new ReStructuredTextFormatter(); +$rest = $formatter->format($events); + +file_put_contents( + __DIR__ . '/../../developer_manual/basics/_available_events_ocp.rst', + $rest +); diff --git a/developer_manual/basics/_available_events_ocp.rst b/developer_manual/basics/_available_events_ocp.rst index 293a09de9de..31ea82a24ae 100644 --- a/developer_manual/basics/_available_events_ocp.rst +++ b/developer_manual/basics/_available_events_ocp.rst @@ -22,6 +22,14 @@ This event is triggered when an app is disabled. This event is triggered when an app is enabled. +``OCP\App\Events\AppsLoadedEvent`` +********************************** + +.. versionadded:: 35 + +This event is triggered when all enabled apps are loaded and available. +So this can be used if your app needs to interact with other apps, which is not guaranteed to be available during the app bootstrapping phase. + ``OCP\App\Events\AppUpdateEvent`` ********************************* @@ -70,7 +78,7 @@ Emitted when the authentication fails, but only if the login name can be associa .. versionadded:: 32 -Emitted when an authentication token is invalidated. +Emitted when an authentication token is invalidated ``OCP\Authentication\TwoFactorAuth\RegistryEvent`` ************************************************** @@ -170,12 +178,6 @@ This can be used to inject a Sabre plugin for example .. versionadded:: 32 -``OCP\Collaboration\AutoComplete\AutoCompleteEvent`` -**************************************************** - -.. versionadded:: 16 - - ``OCP\Collaboration\AutoComplete\AutoCompleteFilterEvent`` ********************************************************** @@ -214,9 +216,38 @@ Class CommentsEntityEvent ****************************** .. versionadded:: 9 + In the future, once the deprecated methods are removed, this class will be abstract. Class CommentsEvent +``OCP\Comments\Events\BeforeCommentUpdatedEvent`` +************************************************* + +.. versionadded:: 33 + +Class BeforeCommentAddedEvent + +``OCP\Comments\Events\CommentAddedEvent`` +***************************************** + +.. versionadded:: 33 + +Class CommentAddedEvent + +``OCP\Comments\Events\CommentDeletedEvent`` +******************************************* + +.. versionadded:: 33 + +Class CommentRemovedEvent + +``OCP\Comments\Events\CommentUpdatedEvent`` +******************************************* + +.. versionadded:: 33 + +Class CommentUpdatedEvent + ``OCP\Config\BeforePreferenceDeletedEvent`` ******************************************* @@ -247,6 +278,12 @@ suggestions in user interfaces. Emitters should add at least one identifier (uid, email, federated cloud ID) of the recipient of the interaction. +``OCP\ContextChat\Events\ContentProviderRegisterEvent`` +******************************************************* + +.. versionadded:: 32 + + ``OCP\DB\Events\AddMissingColumnsEvent`` **************************************** @@ -302,6 +339,14 @@ convenience re-implementation of \Symfony\Component\GenericEvent against .. versionadded:: 22 +``OCP\Files\Cache\CacheEntriesRemovedEvent`` +******************************************** + +.. versionadded:: 34 + +Meta-event wrapping multiple CacheEntryRemovedEvent for when an existing +entry in the cache gets removed. + ``OCP\Files\Cache\CacheEntryInsertedEvent`` ******************************************* @@ -315,6 +360,8 @@ Event for when an existing entry in the cache gets inserted .. versionadded:: 21 Event for when an existing entry in the cache gets removed +Prefer using CacheEntriesRemovedEvent as it is more efficient when deleting +multiple files at the same time. ``OCP\Files\Cache\CacheEntryUpdatedEvent`` ****************************************** @@ -323,38 +370,24 @@ Event for when an existing entry in the cache gets removed Event for when an existing entry in the cache gets updated -``OCP\Files\Cache\CacheInsertEvent`` -************************************ - -.. versionadded:: 16 - -Event for when a new entry gets added to the cache - -``OCP\Files\Cache\CacheUpdateEvent`` -************************************ - -.. versionadded:: 16 - -Event for when an existing entry in the cache gets updated - ``OCP\Files\Config\Event\UserMountAddedEvent`` ********************************************** -.. versionadded:: 31 +.. versionadded:: 32 Event emitted when a user mount was added. ``OCP\Files\Config\Event\UserMountRemovedEvent`` ************************************************ -.. versionadded:: 31 +.. versionadded:: 32 Event emitted when a user mount was removed. ``OCP\Files\Config\Event\UserMountUpdatedEvent`` ************************************************ -.. versionadded:: 31 +.. versionadded:: 32 Event emitted when a user mount was moved. @@ -377,7 +410,7 @@ directly. .. versionadded:: 31 -Event triggered before the file system is setup +Event triggered before the file system is set up. ``OCP\Files\Events\BeforeFolderScannedEvent`` ********************************************* @@ -385,15 +418,28 @@ Event triggered before the file system is setup .. versionadded:: 18 +``OCP\Files\Events\BeforeRemotePropfindEvent`` +********************************************** + +.. versionadded:: 33 + +This event is fired before a PROPFIND request is sent to remote WebDAV storage +Used to extend the list of properties to request additional data from the remote server + ``OCP\Files\Events\BeforeZipCreatedEvent`` ****************************************** .. versionadded:: 25 -This event is triggered before a archive is created when a user requested +This event is triggered before an archive is created when a user requested downloading a folder or multiple files. By setting `successful` to false the tar creation can be aborted and the download denied. +If `allowsPartialArchive` returns true, listeners should only block the +archive creation if access to the entire directory or all files is denied. +Single nodes can be excluded from the archive with `addNodeFilter`. +Listeners can always block the whole archive with `setSuccessful(false)`. + ``OCP\Files\Events\FileCacheUpdated`` ************************************* @@ -486,6 +532,21 @@ Event fired after the filesystem has been torn down .. versionadded:: 20 +``OCP\Files\Events\Node\NodeOpenedEvent`` +***************************************** + +.. versionadded:: 36 + +A user opened a node in a client, as reported by that client. +Unlike {@see \BeforeNodeReadEvent}, this is not dispatched when the content +is read (sync, previews, downloads) but only when a client says the user +actually opened it. + +It is meant for statistics only, like recommending the files a user works +with. Clients report it when they choose to, so it can be missing, repeated +or sent for a node the user never looked at. Never use it for permissions, +access control, auditing, or anything else that has to be reliable. + ``OCP\Files\Events\Node\NodeRenamedEvent`` ****************************************** @@ -527,6 +588,15 @@ Event fired after the filesystem has been torn down .. versionadded:: 28 + +``OCP\Files\Events\UserHomeSetupEvent`` +*************************************** + +.. versionadded:: 34 + +Event triggered after the users home mount has been setup, before any other +mounts are setup. + ``OCP\Files\ObjectStore\Events\BucketCreatedEvent`` *************************************************** @@ -657,6 +727,38 @@ This event is mostly triggered when a registered name is added to the files scan .. versionadded:: 18 +``OCP\Install\Events\InstallationCompletedEvent`` +************************************************* + +.. versionadded:: 33 + +Emitted when the Nextcloud installation has been completed successfully. +This event is dispatched after: +- The database has been configured and migrations have run +- The admin user has been created (if applicable) +- Default apps have been installed +- Background jobs have been configured +- The system has been marked as installed + +Apps can listen to this event to perform additional actions after installation, +such as: +- Sending notification emails +- Triggering external APIs +- Initializing app-specific data +- Setting up integrations + +``OCP\Interaction\RestrictInteractionEvent`` +******************************************** + +.. versionadded:: 34.0.2 + +This event can be emitted to check if a user is allowed to perform an action, such that the receivers would become aware of the resources. +If the resources have a distinction between an owner and a initiator, the event must be emitted for each separately. +Emitters may omit one or multiple properties, if they are not known. +Emitters must call {@see \isInteractionRestricted} instead of dispatching the event manually, to ensure exception handling and audit logging is done correctly. +Listeners may ignore any of the properties and only check the ones that are relevant to them. +Listeners must throw an {@see \InteractionRestrictedException}, if they want to restrict the interaction. + ``OCP\Log\Audit\CriticalActionPerformedEvent`` ********************************************** @@ -684,6 +786,27 @@ Emitted before a system mail is sent. It can be used to alter the message. .. versionadded:: 31 +``OCP\OCM\Events\LocalOCMDiscoveryEvent`` +***************************************** + +.. versionadded:: 33 + +Use this event to register additional resources before the API returns +them in the OCM provider list and capability + +``OCP\OCM\Events\OCMEndpointRequestEvent`` +****************************************** + +.. versionadded:: 33 + +Use this event to catch and manage incoming OCM request + +``OCP\OCM\Events\OCMNotificationReceivedEvent`` +*********************************************** + +.. versionadded:: 35 + + ``OCP\OCM\Events\ResourceTypeRegisterEvent`` ******************************************** @@ -739,10 +862,7 @@ that it is not possible to use the `disallowXYZ` functions. .. versionadded:: 18 -Event to request a secure password to be generated. - -Since Nextcloud 31 this event also provides a ``getContext`` method -allowing to apply different rules for different password context like account passwords or share passwords. +Event to request a secure password to be generated ``OCP\Security\Events\ValidatePasswordPolicyEvent`` *************************************************** @@ -753,9 +873,6 @@ This event can be emitted to request a validation of a password. If a password policy app is installed and the password is invalid, an `\OCP\HintException` will be thrown. -Since Nextcloud 31 this event also provides a ``getContext`` method -allowing to apply different rules for different password context like account passwords or share passwords. - ``OCP\Security\FeaturePolicy\AddFeaturePolicyEvent`` **************************************************** @@ -817,6 +934,18 @@ Event that allows to register a feature policy header to a request. .. versionadded:: 28 +``OCP\Share\Events\ShareMovedEvent`` +************************************ + +.. versionadded:: 33 + + +``OCP\Share\Events\ShareTransferredEvent`` +****************************************** + +.. versionadded:: 33 + + ``OCP\Share\Events\VerifyMountPointEvent`` ****************************************** @@ -828,21 +957,66 @@ Event that allows to register a feature policy header to a request. .. versionadded:: 34.0.2 -Authorization gate for deleting an app-managed share through a share-review app. Dispatched by the app that owns -the share (its ``OCP\Share\ShareReview\IShareReviewSource`` implementation) at the beginning of ``deleteShare()``, -before anything is deleted. The share-review app listens to this event and answers with ``grantAccess()`` or -``denyAccess()`` depending on whether the current user is an authorized share-review operator; apps that merely -expose shares must not listen to it. The event is default-deny: if no listener responds, the share must not be -deleted. Once denied, further grants are ignored and event propagation is stopped. +Authorization gate for deleting an app-managed share through a share-review app. +Background: Apps such as Deck or Tables manage their own shares outside of +the regular sharing backend ({@see \OCP\Share\IManager}). They can expose +those shares to a share-review app — a compliance tool that lets designated +operators audit and revoke shares across all apps — by implementing +{@see \OCP\Share\ShareReview\IShareReviewSource}. When a share-review +operator requests the deletion of such a share, the deletion is executed by +the app that owns the share, not by the share-review app. The owning app +has no way of knowing whether the acting user is actually authorized to +perform share reviews — only the share-review app knows that. This event +closes that gap: it lets the owning app ask "may the current user delete +this share on behalf of a share review?" before deleting anything. + +Dispatched by: the app that owns the share, i.e. the +{@see \OCP\Share\ShareReview\IShareReviewSource} implementation, at the +beginning of its deleteShare() method: + + public function deleteShare(string $shareId): bool { + $event = new ShareReviewAccessCheckEvent('MyApp', $shareId); + $this->dispatcher->dispatchTyped($event); + if (!$event->isHandled() || !$event->isGranted()) { + return false; // default-deny: no listener means no access + } + // ... actually delete the share ... + } + +Listened to by: the share-review app. Its listener decides whether the +current user is an authorized share-review operator (e.g. the app is +enabled for the user) and answers with grantAccess() or denyAccess(): + + public function handle(Event $event): void { + if (!$event instanceof ShareReviewAccessCheckEvent) { + return; + } + if ($this->isShareReviewOperator()) { + $event->grantAccess(); + } else { + $event->denyAccess('User is not a share-review operator.'); + } + } + +Apps that merely expose shares must not listen to this event; answering it +is the responsibility of the share-review app that triggered the deletion. + +Semantics: + - Default-deny: if no listener responds (isHandled() is false, e.g. no + share-review app is installed), the dispatcher must not delete the share. + - Deny wins: once denyAccess() is called, further grantAccess() calls are + ignored and propagation is stopped immediately. + - Multiple grants are harmless; the last listener to deny is authoritative. ``OCP\Share\ShareReview\RegisterShareReviewSourceEvent`` ******************************************************** .. versionadded:: 34.0.2 -Event dispatched by a share-review app to collect share sources from other apps. Listeners register the class name -of their ``OCP\Share\ShareReview\IShareReviewSource`` implementation, whose ``getShares()`` method returns a list of -``OCP\Share\ShareReview\ShareReviewEntry`` objects. +Event dispatched by a share-review app to collect share sources from other +apps. Listeners register the class name of their {@see IShareReviewSource} +implementation; the share-review app resolves it from the dependency +injection container. ``OCP\SpeechToText\Events\TranscriptionFailedEvent`` **************************************************** @@ -858,6 +1032,27 @@ This Event is emitted if a transcription of a media file using a Speech-To-Text This Event is emitted when a transcription of a media file happened successfully +``OCP\SystemTag\Events\TagCreatedEvent`` +**************************************** + +.. versionadded:: 35 + +Event triggered when creating a new tag. + +``OCP\SystemTag\Events\TagDeletedEvent`` +**************************************** + +.. versionadded:: 35 + +Event triggered when deleting a new tag. + +``OCP\SystemTag\Events\TagUpdatedEvent`` +**************************************** + +.. versionadded:: 35 + +Event triggered when updated a tag. + ``OCP\SystemTag\ManagerEvent`` ****************************** @@ -882,6 +1077,20 @@ Class MapperEvent Class SystemTagsEntityEvent +``OCP\SystemTag\TagAssignedEvent`` +********************************** + +.. versionadded:: 32 + +Event class for when a system tag is assigned to an object + +``OCP\SystemTag\TagUnassignedEvent`` +************************************ + +.. versionadded:: 32 + +Event class for when a system tag is unassigned from an object + ``OCP\TaskProcessing\Events\GetTaskProcessingProvidersEvent`` ************************************************************* @@ -1029,6 +1238,12 @@ Emitted when the user password has been updated. .. versionadded:: 18 +``OCP\User\Events\UserConfigChangedEvent`` +****************************************** + +.. versionadded:: 33 + + ``OCP\User\Events\UserCreatedEvent`` ************************************ @@ -1042,6 +1257,12 @@ Emitted when a new user has been created on the back-end. .. versionadded:: 18 +``OCP\User\Events\UserEnumerationFilterEvent`` +********************************************** + +.. versionadded:: 31.0.14 + + ``OCP\User\Events\UserFirstTimeLoggedInEvent`` ********************************************** From 15dfe2804cfd2422870baea17edf1026858a7c52 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:13:21 +0000 Subject: [PATCH 2/6] fixup! feat: generate list of OCP events Signed-off-by: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> --- build/ocp-events/lib/EventFinder.php | 21 ++++++++++++++++--- .../basics/_available_events_ocp.rst | 12 +++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/build/ocp-events/lib/EventFinder.php b/build/ocp-events/lib/EventFinder.php index 759af6aee22..f482ce0b0a6 100644 --- a/build/ocp-events/lib/EventFinder.php +++ b/build/ocp-events/lib/EventFinder.php @@ -69,7 +69,10 @@ public function find(): array { continue; } - $docBlock = $docBlockFactory->create($reflection->getDocComment() ?: '/** */'); + $docBlock = $docBlockFactory->create( + $reflection->getDocComment() ?: '/** */', + (new \phpDocumentor\Reflection\Types\ContextFactory())->createFromReflector($reflection), + ); $sinceTags = array_map(function (\phpDocumentor\Reflection\DocBlock\Tags\Since $tag) { $data = [ @@ -79,7 +82,7 @@ public function find(): array { $description = $tag->getDescription(); if ($description instanceof \phpDocumentor\Reflection\DocBlock\Description) { - $data['description'] = $description->render(); + $data['description'] = $this->renderDescription($description); } return $data; @@ -92,7 +95,7 @@ public function find(): array { $result[] = new EventClass( $reflection->getName(), $docBlock->getSummary(), - $docBlock->getDescription()->render(), + $this->renderDescription($docBlock->getDescription()), $sinceTags ); } @@ -119,4 +122,16 @@ private function sinceFromAttributes(ReflectionClass $reflection): array { usort($versions, 'version_compare'); return [['version' => $versions[0], 'description' => '']]; } + + private function renderDescription(\phpDocumentor\Reflection\DocBlock\Description $description): string { + $tags = array_map(static function (\phpDocumentor\Reflection\DocBlock\Tag $tag) { + if ($tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\See) { + return '``' . ltrim((string)$tag->getReference(), '\\') . '``'; + } + + return '{' . $tag . '}'; + }, $description->getTags()); + + return vsprintf($description->getBodyTemplate(), $tags); + } } diff --git a/developer_manual/basics/_available_events_ocp.rst b/developer_manual/basics/_available_events_ocp.rst index 31ea82a24ae..b6bf43e8ccd 100644 --- a/developer_manual/basics/_available_events_ocp.rst +++ b/developer_manual/basics/_available_events_ocp.rst @@ -538,7 +538,7 @@ Event fired after the filesystem has been torn down .. versionadded:: 36 A user opened a node in a client, as reported by that client. -Unlike {@see \BeforeNodeReadEvent}, this is not dispatched when the content +Unlike ``OCP\Files\Events\Node\BeforeNodeReadEvent``, this is not dispatched when the content is read (sync, previews, downloads) but only when a client says the user actually opened it. @@ -755,9 +755,9 @@ such as: This event can be emitted to check if a user is allowed to perform an action, such that the receivers would become aware of the resources. If the resources have a distinction between an owner and a initiator, the event must be emitted for each separately. Emitters may omit one or multiple properties, if they are not known. -Emitters must call {@see \isInteractionRestricted} instead of dispatching the event manually, to ensure exception handling and audit logging is done correctly. +Emitters must call ``OCP\Interaction\isInteractionRestricted`` instead of dispatching the event manually, to ensure exception handling and audit logging is done correctly. Listeners may ignore any of the properties and only check the ones that are relevant to them. -Listeners must throw an {@see \InteractionRestrictedException}, if they want to restrict the interaction. +Listeners must throw an ``OCP\Interaction\InteractionRestrictedException``, if they want to restrict the interaction. ``OCP\Log\Audit\CriticalActionPerformedEvent`` ********************************************** @@ -959,10 +959,10 @@ Event that allows to register a feature policy header to a request. Authorization gate for deleting an app-managed share through a share-review app. Background: Apps such as Deck or Tables manage their own shares outside of -the regular sharing backend ({@see \OCP\Share\IManager}). They can expose +the regular sharing backend (``OCP\Share\IManager``). They can expose those shares to a share-review app — a compliance tool that lets designated operators audit and revoke shares across all apps — by implementing -{@see \OCP\Share\ShareReview\IShareReviewSource}. When a share-review +``OCP\Share\ShareReview\IShareReviewSource``. When a share-review operator requests the deletion of such a share, the deletion is executed by the app that owns the share, not by the share-review app. The owning app has no way of knowing whether the acting user is actually authorized to @@ -971,7 +971,7 @@ closes that gap: it lets the owning app ask "may the current user delete this share on behalf of a share review?" before deleting anything. Dispatched by: the app that owns the share, i.e. the -{@see \OCP\Share\ShareReview\IShareReviewSource} implementation, at the +``OCP\Share\ShareReview\IShareReviewSource`` implementation, at the beginning of its deleteShare() method: public function deleteShare(string $shareId): bool { From f7f2a299e26ac820a2de62d37ed621c860ae1c39 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:35:13 +0000 Subject: [PATCH 3/6] fixup! feat: generate list of OCP events Signed-off-by: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> --- .../basics/_available_events_ocp.rst | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/developer_manual/basics/_available_events_ocp.rst b/developer_manual/basics/_available_events_ocp.rst index b6bf43e8ccd..70274f74fc9 100644 --- a/developer_manual/basics/_available_events_ocp.rst +++ b/developer_manual/basics/_available_events_ocp.rst @@ -248,17 +248,35 @@ Class CommentRemovedEvent Class CommentUpdatedEvent +``OCP\Config\BeforeAppConfigDeletedEvent`` +****************************************** + +.. versionadded:: 36 + +Emitted before an admin deletes an app config value via occ or the provisioning API. +Listeners can refuse the change with setValid(false). + +``OCP\Config\BeforeAppConfigSetEvent`` +************************************** + +.. versionadded:: 36 + +Emitted before an admin sets an app config value via occ or the provisioning API. +Listeners can refuse the change with setValid(false). + ``OCP\Config\BeforePreferenceDeletedEvent`` ******************************************* .. versionadded:: 25 +Dispatched before a user preference is deleted via the preferences API; the deletion is rejected unless a listener marks it valid. ``OCP\Config\BeforePreferenceSetEvent`` *************************************** .. versionadded:: 25 +Dispatched before a user preference is set via the preferences API; the change is rejected unless a listener marks it valid. ``OCP\Console\ConsoleEvent`` **************************** @@ -755,7 +773,7 @@ such as: This event can be emitted to check if a user is allowed to perform an action, such that the receivers would become aware of the resources. If the resources have a distinction between an owner and a initiator, the event must be emitted for each separately. Emitters may omit one or multiple properties, if they are not known. -Emitters must call ``OCP\Interaction\isInteractionRestricted`` instead of dispatching the event manually, to ensure exception handling and audit logging is done correctly. +Emitters must call ``OCP\Interaction\RestrictInteractionEvent::isInteractionRestricted()`` instead of dispatching the event manually, to ensure exception handling and audit logging is done correctly. Listeners may ignore any of the properties and only check the ones that are relevant to them. Listeners must throw an ``OCP\Interaction\InteractionRestrictedException``, if they want to restrict the interaction. @@ -862,6 +880,9 @@ that it is not possible to use the `disallowXYZ` functions. .. versionadded:: 18 +.. versionchanged:: 31.0.0 + Provides ``getContext`` to apply different rules per password context, e.g. account or share passwords. + Event to request a secure password to be generated ``OCP\Security\Events\ValidatePasswordPolicyEvent`` @@ -869,6 +890,9 @@ Event to request a secure password to be generated .. versionadded:: 18 +.. versionchanged:: 31.0.0 + Provides ``getContext`` to apply different rules per password context, e.g. account or share passwords. + This event can be emitted to request a validation of a password. If a password policy app is installed and the password is invalid, an `\OCP\HintException` will be thrown. @@ -1013,8 +1037,8 @@ Semantics: .. versionadded:: 34.0.2 -Event dispatched by a share-review app to collect share sources from other -apps. Listeners register the class name of their {@see IShareReviewSource} +Event dispatched by a share-review app to collect share sources from other apps. +Listeners register the class name of their ``OCP\Share\ShareReview\IShareReviewSource`` implementation; the share-review app resolves it from the dependency injection container. From 296663dc64b656735ebb874c7878d08677131b84 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:46:30 +0000 Subject: [PATCH 4/6] fixup! feat: generate list of OCP events Signed-off-by: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> --- .../ocp-events/lib/ReStructuredTextFormatter.php | 2 +- .../basics/_available_events_ocp.rst | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build/ocp-events/lib/ReStructuredTextFormatter.php b/build/ocp-events/lib/ReStructuredTextFormatter.php index 07d15d468a2..b76177f76ce 100644 --- a/build/ocp-events/lib/ReStructuredTextFormatter.php +++ b/build/ocp-events/lib/ReStructuredTextFormatter.php @@ -27,7 +27,7 @@ public function format(array $events): string { $output .= ' ' . $sinceTag['description'] . PHP_EOL; } } else { - $output .= '.. versionchanged:: ' . $sinceTag['version'] . PHP_EOL; + $output .= '.. versionchanged:: ' . $this->adjustVersion($sinceTag['version']) . PHP_EOL; if (strlen($sinceTag['description']) > 0) { $output .= ' ' . $sinceTag['description'] . PHP_EOL; } diff --git a/developer_manual/basics/_available_events_ocp.rst b/developer_manual/basics/_available_events_ocp.rst index 70274f74fc9..a1501e41249 100644 --- a/developer_manual/basics/_available_events_ocp.rst +++ b/developer_manual/basics/_available_events_ocp.rst @@ -207,7 +207,7 @@ scripts during page load .. versionadded:: 9.1 -.. versionchanged:: 28.0.0 +.. versionchanged:: 28 Dispatched as a typed event Class CommentsEntityEvent @@ -838,10 +838,10 @@ them in the OCM provider list and capability .. versionadded:: 25.0.1 -.. versionchanged:: 28.0.0 +.. versionchanged:: 28 the constructor arguments ``$width``, ``$height``, ``$crop`` and ``$mode`` are no longer nullable. -.. versionchanged:: 31.0.0 +.. versionchanged:: 31 the constructor arguments ``$mimeType`` was added Emitted before a file preview is being fetched. @@ -880,7 +880,7 @@ that it is not possible to use the `disallowXYZ` functions. .. versionadded:: 18 -.. versionchanged:: 31.0.0 +.. versionchanged:: 31 Provides ``getContext`` to apply different rules per password context, e.g. account or share passwords. Event to request a secure password to be generated @@ -890,7 +890,7 @@ Event to request a secure password to be generated .. versionadded:: 18 -.. versionchanged:: 31.0.0 +.. versionchanged:: 31 Provides ``getContext`` to apply different rules per password context, e.g. account or share passwords. This event can be emitted to request a validation of a password. @@ -996,7 +996,7 @@ this share on behalf of a share review?" before deleting anything. Dispatched by: the app that owns the share, i.e. the ``OCP\Share\ShareReview\IShareReviewSource`` implementation, at the -beginning of its deleteShare() method: +beginning of its deleteShare() method:: public function deleteShare(string $shareId): bool { $event = new ShareReviewAccessCheckEvent('MyApp', $shareId); @@ -1009,7 +1009,7 @@ beginning of its deleteShare() method: Listened to by: the share-review app. Its listener decides whether the current user is an authorized share-review operator (e.g. the app is -enabled for the user) and answers with grantAccess() or denyAccess(): +enabled for the user) and answers with grantAccess() or denyAccess():: public function handle(Event $event): void { if (!$event instanceof ShareReviewAccessCheckEvent) { @@ -1096,7 +1096,7 @@ Class MapperEvent .. versionadded:: 9.1 -.. versionchanged:: 28.0.0 +.. versionchanged:: 28 Dispatched as a typed event Class SystemTagsEntityEvent From c6217df75ab19c882721ad64c8ca7562d49908c7 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:29:45 +0000 Subject: [PATCH 5/6] fixup! feat: generate list of OCP events Signed-off-by: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> --- build/ocp-events/lib/ReStructuredTextFormatter.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build/ocp-events/lib/ReStructuredTextFormatter.php b/build/ocp-events/lib/ReStructuredTextFormatter.php index b76177f76ce..8db81572721 100644 --- a/build/ocp-events/lib/ReStructuredTextFormatter.php +++ b/build/ocp-events/lib/ReStructuredTextFormatter.php @@ -40,7 +40,7 @@ public function format(array $events): string { } if (strlen($event->description) > 0) { - $output .= $event->description . PHP_EOL; + $output .= $this->markdownToRst($event->description) . PHP_EOL; } $output .= PHP_EOL; @@ -50,6 +50,13 @@ public function format(array $events): string { return $output; } + private function markdownToRst(string $markdown): string { + // Indented code block + $rst = preg_replace("/:\n\n(?= {4}|\t)/", "::\n\n", $markdown); + + return $rst; + } + private function adjustVersion(string $version): string { return preg_replace("/(\.0\.0|\.0)$/", '', $version); } From 737674ad6240adeba74338131677a45ef97f7be4 Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:30:15 +0000 Subject: [PATCH 6/6] fixup! feat: generate list of OCP events Signed-off-by: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> --- .../basics/_available_events_ocp.rst | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/developer_manual/basics/_available_events_ocp.rst b/developer_manual/basics/_available_events_ocp.rst index a1501e41249..0373b9bdb38 100644 --- a/developer_manual/basics/_available_events_ocp.rst +++ b/developer_manual/basics/_available_events_ocp.rst @@ -998,29 +998,29 @@ Dispatched by: the app that owns the share, i.e. the ``OCP\Share\ShareReview\IShareReviewSource`` implementation, at the beginning of its deleteShare() method:: - public function deleteShare(string $shareId): bool { - $event = new ShareReviewAccessCheckEvent('MyApp', $shareId); - $this->dispatcher->dispatchTyped($event); - if (!$event->isHandled() || !$event->isGranted()) { - return false; // default-deny: no listener means no access - } - // ... actually delete the share ... - } + public function deleteShare(string $shareId): bool { + $event = new ShareReviewAccessCheckEvent('MyApp', $shareId); + $this->dispatcher->dispatchTyped($event); + if (!$event->isHandled() || !$event->isGranted()) { + return false; // default-deny: no listener means no access + } + // ... actually delete the share ... + } Listened to by: the share-review app. Its listener decides whether the current user is an authorized share-review operator (e.g. the app is enabled for the user) and answers with grantAccess() or denyAccess():: - public function handle(Event $event): void { - if (!$event instanceof ShareReviewAccessCheckEvent) { - return; - } - if ($this->isShareReviewOperator()) { - $event->grantAccess(); - } else { - $event->denyAccess('User is not a share-review operator.'); - } - } + public function handle(Event $event): void { + if (!$event instanceof ShareReviewAccessCheckEvent) { + return; + } + if ($this->isShareReviewOperator()) { + $event->grantAccess(); + } else { + $event->denyAccess('User is not a share-review operator.'); + } + } Apps that merely expose shares must not listen to this event; answering it is the responsibility of the share-review app that triggered the deletion.