123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- <?php
- /**
- * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
- namespace OCA\Files\Activity;
- use OCP\Activity\Exceptions\UnknownActivityException;
- use OCP\Activity\IEvent;
- use OCP\Activity\IEventMerger;
- use OCP\Activity\IManager;
- use OCP\Activity\IProvider;
- use OCP\Contacts\IManager as IContactsManager;
- use OCP\Federation\ICloudIdManager;
- use OCP\Files\Folder;
- use OCP\Files\InvalidPathException;
- use OCP\Files\IRootFolder;
- use OCP\Files\Node;
- use OCP\Files\NotFoundException;
- use OCP\IL10N;
- use OCP\IURLGenerator;
- use OCP\IUserManager;
- use OCP\L10N\IFactory;
- class Provider implements IProvider {
- /** @var IL10N */
- protected $l;
- /** @var IL10N */
- protected $activityLang;
- /** @var string[] cached displayNames - key is the cloud id and value the displayname */
- protected $displayNames = [];
- protected $fileIsEncrypted = false;
- public function __construct(
- protected IFactory $languageFactory,
- protected IURLGenerator $url,
- protected IManager $activityManager,
- protected IUserManager $userManager,
- protected IRootFolder $rootFolder,
- protected ICloudIdManager $cloudIdManager,
- protected IContactsManager $contactsManager,
- protected IEventMerger $eventMerger,
- ) {
- }
- /**
- * @param string $language
- * @param IEvent $event
- * @param IEvent|null $previousEvent
- * @return IEvent
- * @throws UnknownActivityException
- * @since 11.0.0
- */
- public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
- if ($event->getApp() !== 'files') {
- throw new UnknownActivityException();
- }
- $this->l = $this->languageFactory->get('files', $language);
- $this->activityLang = $this->languageFactory->get('activity', $language);
- if ($this->activityManager->isFormattingFilteredObject()) {
- try {
- return $this->parseShortVersion($event, $previousEvent);
- } catch (UnknownActivityException) {
- // Ignore and simply use the long version...
- }
- }
- return $this->parseLongVersion($event, $previousEvent);
- }
- protected function setIcon(IEvent $event, string $icon, string $app = 'files') {
- if ($this->activityManager->getRequirePNG()) {
- $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath($app, $icon . '.png')));
- } else {
- $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath($app, $icon . '.svg')));
- }
- }
- /**
- * @param IEvent $event
- * @param IEvent|null $previousEvent
- * @return IEvent
- * @throws UnknownActivityException
- * @since 11.0.0
- */
- public function parseShortVersion(IEvent $event, ?IEvent $previousEvent = null): IEvent {
- $parsedParameters = $this->getParameters($event);
- if ($event->getSubject() === 'created_by') {
- $subject = $this->l->t('Created by {user}');
- $this->setIcon($event, 'add-color');
- } elseif ($event->getSubject() === 'changed_by') {
- $subject = $this->l->t('Changed by {user}');
- $this->setIcon($event, 'change');
- } elseif ($event->getSubject() === 'deleted_by') {
- $subject = $this->l->t('Deleted by {user}');
- $this->setIcon($event, 'delete-color');
- } elseif ($event->getSubject() === 'restored_by') {
- $subject = $this->l->t('Restored by {user}');
- $this->setIcon($event, 'actions/history', 'core');
- } elseif ($event->getSubject() === 'renamed_by') {
- $subject = $this->l->t('Renamed by {user}');
- $this->setIcon($event, 'change');
- } elseif ($event->getSubject() === 'moved_by') {
- $subject = $this->l->t('Moved by {user}');
- $this->setIcon($event, 'change');
- } else {
- throw new UnknownActivityException();
- }
- if (!isset($parsedParameters['user'])) {
- // External user via public link share
- $subject = str_replace('{user}', $this->activityLang->t('"remote account"'), $subject);
- }
- $this->setSubjects($event, $subject, $parsedParameters);
- return $this->eventMerger->mergeEvents('user', $event, $previousEvent);
- }
- /**
- * @param IEvent $event
- * @param IEvent|null $previousEvent
- * @return IEvent
- * @throws UnknownActivityException
- * @since 11.0.0
- */
- public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null): IEvent {
- $this->fileIsEncrypted = false;
- $parsedParameters = $this->getParameters($event);
- if ($event->getSubject() === 'created_self') {
- $subject = $this->l->t('You created {file}');
- if ($this->fileIsEncrypted) {
- $subject = $this->l->t('You created an encrypted file in {file}');
- }
- $this->setIcon($event, 'add-color');
- } elseif ($event->getSubject() === 'created_by') {
- $subject = $this->l->t('{user} created {file}');
- if ($this->fileIsEncrypted) {
- $subject = $this->l->t('{user} created an encrypted file in {file}');
- }
- $this->setIcon($event, 'add-color');
- } elseif ($event->getSubject() === 'created_public') {
- $subject = $this->l->t('{file} was created in a public folder');
- $this->setIcon($event, 'add-color');
- } elseif ($event->getSubject() === 'changed_self') {
- $subject = $this->l->t('You changed {file}');
- if ($this->fileIsEncrypted) {
- $subject = $this->l->t('You changed an encrypted file in {file}');
- }
- $this->setIcon($event, 'change');
- } elseif ($event->getSubject() === 'changed_by') {
- $subject = $this->l->t('{user} changed {file}');
- if ($this->fileIsEncrypted) {
- $subject = $this->l->t('{user} changed an encrypted file in {file}');
- }
- $this->setIcon($event, 'change');
- } elseif ($event->getSubject() === 'deleted_self') {
- $subject = $this->l->t('You deleted {file}');
- if ($this->fileIsEncrypted) {
- $subject = $this->l->t('You deleted an encrypted file in {file}');
- }
- $this->setIcon($event, 'delete-color');
- } elseif ($event->getSubject() === 'deleted_by') {
- $subject = $this->l->t('{user} deleted {file}');
- if ($this->fileIsEncrypted) {
- $subject = $this->l->t('{user} deleted an encrypted file in {file}');
- }
- $this->setIcon($event, 'delete-color');
- } elseif ($event->getSubject() === 'restored_self') {
- $subject = $this->l->t('You restored {file}');
- $this->setIcon($event, 'actions/history', 'core');
- } elseif ($event->getSubject() === 'restored_by') {
- $subject = $this->l->t('{user} restored {file}');
- $this->setIcon($event, 'actions/history', 'core');
- } elseif ($event->getSubject() === 'renamed_self') {
- $oldFileName = $parsedParameters['oldfile']['name'];
- $newFileName = $parsedParameters['newfile']['name'];
- if ($this->isHiddenFile($oldFileName)) {
- if ($this->isHiddenFile($newFileName)) {
- $subject = $this->l->t('You renamed {oldfile} (hidden) to {newfile} (hidden)');
- } else {
- $subject = $this->l->t('You renamed {oldfile} (hidden) to {newfile}');
- }
- } else {
- if ($this->isHiddenFile($newFileName)) {
- $subject = $this->l->t('You renamed {oldfile} to {newfile} (hidden)');
- } else {
- $subject = $this->l->t('You renamed {oldfile} to {newfile}');
- }
- }
- $this->setIcon($event, 'change');
- } elseif ($event->getSubject() === 'renamed_by') {
- $oldFileName = $parsedParameters['oldfile']['name'];
- $newFileName = $parsedParameters['newfile']['name'];
- if ($this->isHiddenFile($oldFileName)) {
- if ($this->isHiddenFile($newFileName)) {
- $subject = $this->l->t('{user} renamed {oldfile} (hidden) to {newfile} (hidden)');
- } else {
- $subject = $this->l->t('{user} renamed {oldfile} (hidden) to {newfile}');
- }
- } else {
- if ($this->isHiddenFile($newFileName)) {
- $subject = $this->l->t('{user} renamed {oldfile} to {newfile} (hidden)');
- } else {
- $subject = $this->l->t('{user} renamed {oldfile} to {newfile}');
- }
- }
- $this->setIcon($event, 'change');
- } elseif ($event->getSubject() === 'moved_self') {
- $subject = $this->l->t('You moved {oldfile} to {newfile}');
- $this->setIcon($event, 'change');
- } elseif ($event->getSubject() === 'moved_by') {
- $subject = $this->l->t('{user} moved {oldfile} to {newfile}');
- $this->setIcon($event, 'change');
- } else {
- throw new UnknownActivityException();
- }
- if ($this->fileIsEncrypted) {
- $event->setSubject($event->getSubject() . '_enc', $event->getSubjectParameters());
- }
- if (!isset($parsedParameters['user'])) {
- // External user via public link share
- $subject = str_replace('{user}', $this->activityLang->t('"remote account"'), $subject);
- }
- $this->setSubjects($event, $subject, $parsedParameters);
- if ($event->getSubject() === 'moved_self' || $event->getSubject() === 'moved_by') {
- $event = $this->eventMerger->mergeEvents('oldfile', $event, $previousEvent);
- } else {
- $event = $this->eventMerger->mergeEvents('file', $event, $previousEvent);
- }
- if ($event->getChildEvent() === null) {
- // Couldn't group by file, maybe we can group by user
- $event = $this->eventMerger->mergeEvents('user', $event, $previousEvent);
- }
- return $event;
- }
- private function isHiddenFile(string $filename): bool {
- return strlen($filename) > 0 && $filename[0] === '.';
- }
- protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
- $event->setRichSubject($subject, $parameters);
- }
- /**
- * @param IEvent $event
- * @return array
- * @throws UnknownActivityException
- */
- protected function getParameters(IEvent $event): array {
- $parameters = $event->getSubjectParameters();
- switch ($event->getSubject()) {
- case 'created_self':
- case 'created_public':
- case 'changed_self':
- case 'deleted_self':
- case 'restored_self':
- return [
- 'file' => $this->getFile($parameters[0], $event),
- ];
- case 'created_by':
- case 'changed_by':
- case 'deleted_by':
- case 'restored_by':
- if ($parameters[1] === '') {
- // External user via public link share
- return [
- 'file' => $this->getFile($parameters[0], $event),
- ];
- }
- return [
- 'file' => $this->getFile($parameters[0], $event),
- 'user' => $this->getUser($parameters[1]),
- ];
- case 'renamed_self':
- case 'moved_self':
- return [
- 'newfile' => $this->getFile($parameters[0]),
- 'oldfile' => $this->getFile($parameters[1]),
- ];
- case 'renamed_by':
- case 'moved_by':
- if ($parameters[1] === '') {
- // External user via public link share
- return [
- 'newfile' => $this->getFile($parameters[0]),
- 'oldfile' => $this->getFile($parameters[2]),
- ];
- }
- return [
- 'newfile' => $this->getFile($parameters[0]),
- 'user' => $this->getUser($parameters[1]),
- 'oldfile' => $this->getFile($parameters[2]),
- ];
- }
- return [];
- }
- /**
- * @param array|string $parameter
- * @param IEvent|null $event
- * @return array
- * @throws UnknownActivityException
- */
- protected function getFile($parameter, ?IEvent $event = null): array {
- if (is_array($parameter)) {
- $path = reset($parameter);
- $id = (string)key($parameter);
- } elseif ($event !== null) {
- // Legacy from before ownCloud 8.2
- $path = $parameter;
- $id = $event->getObjectId();
- } else {
- throw new UnknownActivityException('Could not generate file parameter');
- }
- $encryptionContainer = $this->getEndToEndEncryptionContainer($id, $path);
- if ($encryptionContainer instanceof Folder) {
- $this->fileIsEncrypted = true;
- try {
- $fullPath = rtrim($encryptionContainer->getPath(), '/');
- // Remove /user/files/...
- [,,, $path] = explode('/', $fullPath, 4);
- if (!$path) {
- throw new InvalidPathException('Path could not be split correctly');
- }
- return [
- 'type' => 'file',
- 'id' => $encryptionContainer->getId(),
- 'name' => $encryptionContainer->getName(),
- 'path' => $path,
- 'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $encryptionContainer->getId()]),
- ];
- } catch (\Exception $e) {
- // fall back to the normal one
- $this->fileIsEncrypted = false;
- }
- }
- return [
- 'type' => 'file',
- 'id' => $id,
- 'name' => basename($path),
- 'path' => trim($path, '/'),
- 'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $id]),
- ];
- }
- protected $fileEncrypted = [];
- /**
- * Check if a file is end2end encrypted
- * @param int $fileId
- * @param string $path
- * @return Folder|null
- */
- protected function getEndToEndEncryptionContainer($fileId, $path) {
- if (isset($this->fileEncrypted[$fileId])) {
- return $this->fileEncrypted[$fileId];
- }
- $fileName = basename($path);
- if (!preg_match('/^[0-9a-fA-F]{32}$/', $fileName)) {
- $this->fileEncrypted[$fileId] = false;
- return $this->fileEncrypted[$fileId];
- }
- $userFolder = $this->rootFolder->getUserFolder($this->activityManager->getCurrentUserId());
- $file = $userFolder->getFirstNodeById($fileId);
- if (!$file) {
- try {
- // Deleted, try with parent
- $file = $this->findExistingParent($userFolder, dirname($path));
- } catch (NotFoundException $e) {
- return null;
- }
- if (!$file instanceof Folder || !$file->isEncrypted()) {
- return null;
- }
- $this->fileEncrypted[$fileId] = $file;
- return $file;
- }
- if ($file instanceof Folder && $file->isEncrypted()) {
- // If the folder is encrypted, it is the Container,
- // but can be the name is just fine.
- $this->fileEncrypted[$fileId] = true;
- return null;
- }
- $this->fileEncrypted[$fileId] = $this->getParentEndToEndEncryptionContainer($userFolder, $file);
- return $this->fileEncrypted[$fileId];
- }
- /**
- * @param Folder $userFolder
- * @param string $path
- * @return Folder
- * @throws NotFoundException
- */
- protected function findExistingParent(Folder $userFolder, $path) {
- if ($path === '/') {
- throw new NotFoundException('Reached the root');
- }
- try {
- $folder = $userFolder->get(dirname($path));
- } catch (NotFoundException $e) {
- return $this->findExistingParent($userFolder, dirname($path));
- }
- return $folder;
- }
- /**
- * Check all parents until the user's root folder if one is encrypted
- *
- * @param Folder $userFolder
- * @param Node $file
- * @return Node|null
- */
- protected function getParentEndToEndEncryptionContainer(Folder $userFolder, Node $file) {
- try {
- $parent = $file->getParent();
- if ($userFolder->getId() === $parent->getId()) {
- return null;
- }
- } catch (\Exception $e) {
- return null;
- }
- if ($parent->isEncrypted()) {
- return $parent;
- }
- return $this->getParentEndToEndEncryptionContainer($userFolder, $parent);
- }
- /**
- * @param string $uid
- * @return array
- */
- protected function getUser($uid) {
- // First try local user
- $displayName = $this->userManager->getDisplayName($uid);
- if ($displayName !== null) {
- return [
- 'type' => 'user',
- 'id' => $uid,
- 'name' => $displayName,
- ];
- }
- // Then a contact from the addressbook
- if ($this->cloudIdManager->isValidCloudId($uid)) {
- $cloudId = $this->cloudIdManager->resolveCloudId($uid);
- return [
- 'type' => 'user',
- 'id' => $cloudId->getUser(),
- 'name' => $this->getDisplayNameFromAddressBook($cloudId->getDisplayId()),
- 'server' => $cloudId->getRemote(),
- ];
- }
- // Fallback to empty dummy data
- return [
- 'type' => 'user',
- 'id' => $uid,
- 'name' => $uid,
- ];
- }
- protected function getDisplayNameFromAddressBook(string $search): string {
- if (isset($this->displayNames[$search])) {
- return $this->displayNames[$search];
- }
- $addressBookContacts = $this->contactsManager->search($search, ['CLOUD'], [
- 'limit' => 1,
- 'enumeration' => false,
- 'fullmatch' => false,
- 'strict_search' => true,
- ]);
- foreach ($addressBookContacts as $contact) {
- if (isset($contact['isLocalSystemBook'])) {
- continue;
- }
- if (isset($contact['CLOUD'])) {
- $cloudIds = $contact['CLOUD'];
- if (is_string($cloudIds)) {
- $cloudIds = [$cloudIds];
- }
- $lowerSearch = strtolower($search);
- foreach ($cloudIds as $cloudId) {
- if (strtolower($cloudId) === $lowerSearch) {
- $this->displayNames[$search] = $contact['FN'] . " ($cloudId)";
- return $this->displayNames[$search];
- }
- }
- }
- }
- return $search;
- }
- }
|