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`` **********************************************