%PDF- %PDF-
| Direktori : /www/varak.net/nextcloud.varak.net/nextcloud/apps/activity/lib/ |
| Current File : //www/varak.net/nextcloud.varak.net/nextcloud/apps/activity/lib/FilesHooks.php |
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Activity;
use OC\Files\Filesystem;
use OC\Files\View;
use OCA\Activity\BackgroundJob\RemoteActivity;
use OCA\Activity\Extension\Files;
use OCA\Activity\Extension\Files_Sharing;
use OCP\Activity\IManager;
use OCP\Constants;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\ITagManager;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Share\IShare;
use OCP\Share\IShareHelper;
use Psr\Log\LoggerInterface;
/**
* The class to handle the filesystem hooks
*/
class FilesHooks {
public const USER_BATCH_SIZE = 50;
/** @var string|bool */
protected $moveCase = false;
/** @var array */
protected $oldAccessList;
/** @var string */
protected $oldParentPath;
/** @var string */
protected $oldParentOwner;
/** @var string */
protected $oldParentId;
public function __construct(
protected IManager $manager,
protected Data $activityData,
protected UserSettings $userSettings,
protected IGroupManager $groupManager,
protected View $view,
protected IRootFolder $rootFolder,
protected IShareHelper $shareHelper,
protected IDBConnection $connection,
protected IURLGenerator $urlGenerator,
protected LoggerInterface $logger,
protected CurrentUser $currentUser,
protected IUserMountCache $userMountCache,
protected IConfig $config,
protected NotificationGenerator $notificationGenerator,
protected ITagManager $tagManager
) {
}
/**
* Store the create hook events
*
* @param string $path Path of the file that has been created
*/
public function fileCreate($path) {
if ($path === '/' || $path === '' || $path === null) {
return;
}
if ($this->currentUser->getUserIdentifier() !== '') {
$this->addNotificationsForFileAction($path, Files::TYPE_SHARE_CREATED, 'created_self', 'created_by');
} else {
$this->addNotificationsForFileAction($path, Files_Sharing::TYPE_PUBLIC_UPLOAD, '', 'created_public');
}
}
/**
* Store the update hook events
*
* @param string $path Path of the file that has been modified
*/
public function fileUpdate($path) {
$this->addNotificationsForFileAction($path, Files::TYPE_FILE_CHANGED, 'changed_self', 'changed_by');
}
/**
* Store the delete hook events
*
* @param string $path Path of the file that has been deleted
*/
public function fileDelete($path) {
$this->addNotificationsForFileAction($path, Files::TYPE_SHARE_DELETED, 'deleted_self', 'deleted_by');
}
/**
* Store the restore hook events
*
* @param string $path Path of the file that has been restored
*/
public function fileRestore($path) {
$this->addNotificationsForFileAction($path, Files::TYPE_SHARE_RESTORED, 'restored_self', 'restored_by');
}
private function getFileChangeActivitySettings(int $fileId, array $users, $type = Files::TYPE_FILE_CHANGED): array {
$filteredEmailUsers = $this->userSettings->filterUsersBySetting($users, 'email', $type);
$filteredNotificationUsers = $this->userSettings->filterUsersBySetting($users, 'notification', $type);
/** @psalm-suppress UndefinedMethod */
$favoriteUsers = $this->tagManager->getUsersFavoritingObject('files', $fileId);
if (!empty($favoriteUsers)) {
$favoriteUsers = array_intersect($users, $favoriteUsers);
if (!empty($favoriteUsers)) {
$filteredEmailUsers = array_merge($filteredEmailUsers, $this->userSettings->filterUsersBySetting($favoriteUsers, 'email', Files::TYPE_FAVORITE_CHANGED));
$filteredNotificationUsers = array_merge($filteredNotificationUsers, $this->userSettings->filterUsersBySetting($favoriteUsers, 'notification', Files::TYPE_FAVORITE_CHANGED));
}
}
return [$filteredEmailUsers, $filteredNotificationUsers];
}
/**
* Creates the entries for file actions on $file_path
*
* @param string $filePath The file that is being changed
* @param string $activityType The activity type
* @param string $subject The subject for the actor
* @param string $subjectBy The subject for other users (with "by $actor")
*/
protected function addNotificationsForFileAction($filePath, $activityType, $subject, $subjectBy) {
// Do not add activities for .part-files
if (substr($filePath, -5) === '.part') {
return;
}
[$filePath, $uidOwner, $fileId] = $this->getSourcePathAndOwner($filePath);
if ($fileId === 0) {
// Could not find the file for the owner ...
return;
}
$accessList = $this->getUserPathsFromPath($filePath, $uidOwner);
if (!empty($accessList['remotes'])) {
$this->generateRemoteActivity($accessList['remotes'], $activityType, time(), $this->currentUser->getCloudId(), $accessList['ownerPath']);
}
$affectedUsers = $accessList['users'];
// file can be shared using GroupFolders, including ACL check
if ($this->config->getSystemValueBool('activity_use_cached_mountpoints', false)) {
$affectedUsers = array_merge($affectedUsers, $this->getAffectedUsersFromCachedMounts($fileId));
}
[$filteredEmailUsers, $filteredNotificationUsers] =
$this->getFileChangeActivitySettings($fileId, array_keys($affectedUsers), $activityType);
foreach ($affectedUsers as $user => $path) {
$user = (string)$user;
if ($user === $this->currentUser->getUID()) {
$userSubject = $subject;
$userParams = [[$fileId => $path]];
} else {
$userSubject = $subjectBy;
$userParams = [[$fileId => $path], $this->currentUser->getUserIdentifier()];
}
$this->addNotificationsForUser(
$user, $userSubject, $userParams,
$fileId, $path, true,
$filteredEmailUsers[$user] ?? false,
$filteredNotificationUsers[$user] ?? false,
$activityType
);
}
}
protected function generateRemoteActivity(array $remoteUsers, string $type, int $time, $actor, $ownerPath = false) {
foreach ($remoteUsers as $remoteUser => $info) {
if ($actor === $remoteUser) {
// Current user receives the notification on their own instance already
continue;
}
$arguments = [
$remoteUser,
$info['token'],
$ownerPath !== false ? substr($ownerPath, strlen($info['node_path'])) : $info['node_path'],
$type,
$time,
$actor,
];
if (isset($info['second_path'])) {
$arguments[] = $info['second_path'];
}
\OC::$server->getJobList()->add(RemoteActivity::class, $arguments);
}
}
/**
* Collect some information for move/renames
*
* @param string $oldPath Path of the file that has been moved
* @param string $newPath Path of the file that has been moved
*/
public function fileMove($oldPath, $newPath) {
if (substr($oldPath, -5) === '.part' || substr($newPath, -5) === '.part') {
// Do not add activities for .part-files
$this->moveCase = false;
return;
}
$oldDir = dirname($oldPath);
$newDir = dirname($newPath);
if ($oldDir === $newDir) {
/**
* a/b moved to a/c
*
* Cases:
* - a/b shared: no visible change
* - a/ shared: rename
*/
$this->moveCase = 'rename';
return;
}
if (strpos($oldDir, $newDir) === 0) {
/**
* a/b/c moved to a/c
*
* Cases:
* - a/b/c shared: no visible change
* - a/b/ shared: delete
* - a/ shared: move/rename
*/
$this->moveCase = 'moveUp';
} elseif (strpos($newDir, $oldDir) === 0) {
/**
* a/b moved to a/c/b
*
* Cases:
* - a/b shared: no visible change
* - a/c/ shared: add
* - a/ shared: move/rename
*/
$this->moveCase = 'moveDown';
} else {
/**
* a/b/c moved to a/d/c
*
* Cases:
* - a/b/c shared: no visible change
* - a/b/ shared: delete
* - a/d/ shared: add
* - a/ shared: move/rename
*/
$this->moveCase = 'moveCross';
}
[$this->oldParentPath, $this->oldParentOwner, $this->oldParentId] = $this->getSourcePathAndOwner($oldDir);
if ($this->oldParentId === 0) {
// Could not find the file for the owner ...
$this->moveCase = false;
return;
}
$oldAccessList = $this->getUserPathsFromPath($this->oldParentPath, $this->oldParentOwner);
// file can be shared using GroupFolders, including ACL check
if ($this->config->getSystemValueBool('activity_use_cached_mountpoints', false)) {
[, , $oldFileId] = $this->getSourcePathAndOwner($oldPath);
$oldAccessList['users'] = array_merge($oldAccessList['users'], $this->getAffectedUsersFromCachedMounts($oldFileId));
}
$this->oldAccessList = $oldAccessList;
}
/**
* Store the move hook events
*
* @param string $oldPath Path of the file that has been moved
* @param string $newPath Path of the file that has been moved
*/
public function fileMovePost($oldPath, $newPath) {
// Do not add activities for .part-files
if ($this->moveCase === false) {
return;
}
switch ($this->moveCase) {
case 'rename':
$this->fileRenaming($oldPath, $newPath);
break;
case 'moveUp':
case 'moveDown':
case 'moveCross':
$this->fileMoving($oldPath, $newPath);
break;
}
$this->moveCase = false;
}
/**
* Renaming a file inside the same folder (a/b to a/c)
*
* @param string $oldPath
* @param string $newPath
*/
protected function fileRenaming($oldPath, $newPath) {
$dirName = dirname($newPath);
$fileName = basename($newPath);
$oldFileName = basename($oldPath);
[, , $fileId] = $this->getSourcePathAndOwner($newPath);
[$parentPath, $parentOwner, $parentId] = $this->getSourcePathAndOwner($dirName);
if ($fileId === 0 || $parentId === 0) {
// Could not find the file for the owner ...
return;
}
$accessList = $this->getUserPathsFromPath($parentPath, $parentOwner);
$renameRemotes = [];
foreach ($accessList['remotes'] as $remote => $info) {
$renameRemotes[$remote] = [
'token' => $info['token'],
'node_path' => substr($newPath, strlen($info['node_path'])),
'second_path' => substr($oldPath, strlen($info['node_path'])),
];
}
$this->generateRemoteActivity($renameRemotes, Files::TYPE_FILE_CHANGED, time(), $this->currentUser->getCloudId());
$affectedUsers = $accessList['users'];
// file can be shared using GroupFolders, including ACL check
if ($this->config->getSystemValueBool('activity_use_cached_mountpoints', false)) {
$affectedUsers = array_merge($affectedUsers, $this->getAffectedUsersFromCachedMounts($fileId));
}
[$filteredEmailUsers, $filteredNotificationUsers] = $this->getFileChangeActivitySettings($fileId, array_keys($affectedUsers));
foreach ($affectedUsers as $user => $path) {
if ($user === $this->currentUser->getUID()) {
$userSubject = 'renamed_self';
$userParams = [
[$fileId => $path . '/' . $fileName],
[$fileId => $path . '/' . $oldFileName],
];
} else {
$userSubject = 'renamed_by';
$userParams = [
[$fileId => $path . '/' . $fileName],
$this->currentUser->getUserIdentifier(),
[$fileId => $path . '/' . $oldFileName],
];
}
$this->addNotificationsForUser(
$user, $userSubject, $userParams,
$fileId, $path . '/' . $fileName, true,
$filteredEmailUsers[$user] ?? false,
$filteredNotificationUsers[$user] ?? false,
Files::TYPE_FILE_CHANGED
);
}
}
/**
* Moving a file from one folder to another
*
* @param string $oldPath
* @param string $newPath
*/
protected function fileMoving($oldPath, $newPath) {
$dirName = dirname($newPath);
$fileName = basename($newPath);
$oldFileName = basename($oldPath);
[, , $fileId] = $this->getSourcePathAndOwner($newPath);
[$parentPath, $parentOwner, $parentId] = $this->getSourcePathAndOwner($dirName);
if ($fileId === 0 || $parentId === 0) {
// Could not find the file for the owner ...
return;
}
$accessList = $this->getUserPathsFromPath($parentPath, $parentOwner);
$affectedUsers = $accessList['users'];
$oldUsers = $this->oldAccessList['users'];
// file can be shared using GroupFolders, including ACL check
if ($this->config->getSystemValueBool('activity_use_cached_mountpoints', false)) {
$this->userMountCache->clear(); // clear cache for new data
$affectedUsers = array_merge($affectedUsers, $this->getAffectedUsersFromCachedMounts($fileId));
}
$beforeUsers = array_keys($oldUsers);
$afterUsers = array_keys($affectedUsers);
$deleteUsers = array_diff($beforeUsers, $afterUsers);
$this->generateDeleteActivities($deleteUsers, $oldUsers, $fileId, $oldFileName);
$addUsers = array_diff($afterUsers, $beforeUsers);
$this->generateAddActivities($addUsers, $affectedUsers, $fileId, $fileName);
$moveUsers = array_intersect($beforeUsers, $afterUsers);
$this->generateMoveActivities($moveUsers, $oldUsers, $affectedUsers, $fileId, $oldFileName, $parentId, $fileName);
$beforeRemotes = $this->oldAccessList['remotes'];
$afterRemotes = $accessList['remotes'];
$addRemotes = $deleteRemotes = $moveRemotes = [];
foreach ($afterRemotes as $remote => $info) {
if (isset($beforeRemotes[$remote])) {
// Move
$info['node_path'] = substr($newPath, strlen($info['node_path']));
$info['second_path'] = substr($oldPath, strlen($beforeRemotes[$remote]['node_path']));
$moveRemotes[$remote] = $info;
} else {
$info['node_path'] = substr($newPath, strlen($info['node_path']));
$addRemotes[$remote] = $info;
}
}
foreach ($beforeRemotes as $remote => $info) {
if (!isset($afterRemotes[$remote])) {
$info['node_path'] = substr($oldPath, strlen($info['node_path']));
$deleteRemotes[$remote] = $info;
}
}
$this->generateRemoteActivity($deleteRemotes, Files::TYPE_SHARE_DELETED, time(), $this->currentUser->getCloudId());
$this->generateRemoteActivity($addRemotes, Files::TYPE_SHARE_CREATED, time(), $this->currentUser->getCloudId());
$this->generateRemoteActivity($moveRemotes, Files::TYPE_FILE_CHANGED, time(), $this->currentUser->getCloudId());
}
/**
* @param string[] $users
* @param string[] $pathMap
* @param int $fileId
* @param string $oldFileName
*/
protected function generateDeleteActivities($users, $pathMap, $fileId, $oldFileName) {
if (empty($users)) {
return;
}
[$filteredEmailUsers, $filteredNotificationUsers] = $this->getFileChangeActivitySettings($fileId, $users);
$shouldFlush = $this->startActivityTransaction();
foreach ($users as $user) {
$path = $pathMap[$user];
if ($user === $this->currentUser->getUID()) {
$userSubject = 'deleted_self';
$userParams = [[$fileId => $path . '/' . $oldFileName]];
} else {
$userSubject = 'deleted_by';
$userParams = [[$fileId => $path . '/' . $oldFileName], $this->currentUser->getUserIdentifier()];
}
$this->addNotificationsForUser(
$user, $userSubject, $userParams,
$fileId, $path . '/' . $oldFileName, true,
$filteredEmailUsers[$user] ?? false,
$filteredNotificationUsers[$user] ?? false,
Files::TYPE_SHARE_DELETED
);
}
$this->commitActivityTransaction($shouldFlush);
}
/**
* @param string[] $users
* @param string[] $pathMap
* @param int $fileId
* @param string $fileName
*/
protected function generateAddActivities($users, $pathMap, $fileId, $fileName) {
if (empty($users)) {
return;
}
[$filteredEmailUsers, $filteredNotificationUsers] = $this->getFileChangeActivitySettings($fileId, $users);
$shouldFlush = $this->startActivityTransaction();
foreach ($users as $user) {
$path = $pathMap[$user];
if ($user === $this->currentUser->getUID()) {
$userSubject = 'created_self';
$userParams = [[$fileId => $path . '/' . $fileName]];
} else {
$userSubject = 'created_by';
$userParams = [[$fileId => $path . '/' . $fileName], $this->currentUser->getUserIdentifier()];
}
$this->addNotificationsForUser(
$user, $userSubject, $userParams,
$fileId, $path . '/' . $fileName, true,
$filteredEmailUsers[$user] ?? false,
$filteredNotificationUsers[$user] ?? false,
Files::TYPE_FILE_CHANGED
);
}
$this->commitActivityTransaction($shouldFlush);
}
/**
* @param string[] $users
* @param string[] $beforePathMap
* @param string[] $afterPathMap
* @param int $fileId
* @param string $oldFileName
* @param int $newParentId
* @param string $fileName
*/
protected function generateMoveActivities($users, $beforePathMap, $afterPathMap, $fileId, $oldFileName, $newParentId, $fileName) {
if (empty($users)) {
return;
}
[$filteredEmailUsers, $filteredNotificationUsers] = $this->getFileChangeActivitySettings($fileId, $users);
$shouldFlush = $this->startActivityTransaction();
foreach ($users as $user) {
if ($oldFileName === $fileName) {
$userParams = [[$newParentId => $afterPathMap[$user] . '/']];
} else {
$userParams = [[$fileId => $afterPathMap[$user] . '/' . $fileName]];
}
if ($user === $this->currentUser->getUID()) {
$userSubject = 'moved_self';
} else {
$userSubject = 'moved_by';
$userParams[] = $this->currentUser->getUserIdentifier();
}
$userParams[] = [$fileId => $beforePathMap[$user] . '/' . $oldFileName];
$this->addNotificationsForUser(
$user, $userSubject, $userParams,
$fileId, $afterPathMap[$user] . '/' . $fileName, true,
$filteredEmailUsers[$user] ?? false,
$filteredNotificationUsers[$user] ?? false,
Files::TYPE_FILE_CHANGED
);
}
$this->commitActivityTransaction($shouldFlush);
}
/**
* Returns a "username => path" map for all affected users
*
* @param string $path
* @param string $uidOwner
* @return array
*/
protected function getUserPathsFromPath($path, $uidOwner) {
try {
$node = $this->rootFolder->getUserFolder($uidOwner)->get($path);
} catch (NotFoundException $e) {
return [];
}
if (!$node instanceof Node) {
return [];
}
$accessList = $this->shareHelper->getPathsForAccessList($node);
$path = $node->getPath();
$accessList['ownerPath'] = $this->getVisiblePath($path);
return $accessList;
}
protected function getVisiblePath(string $absolutePath): string {
$sections = explode('/', $absolutePath, 4);
$path = '/';
if (isset($sections[3])) {
// Not the case when a file in root is renamed
$path .= $sections[3];
}
return $path;
}
/**
* Return the source
*
* @param string $path
* @return array
*/
protected function getSourcePathAndOwner($path) {
$view = Filesystem::getView();
try {
$owner = $view->getOwner($path);
$owner = !is_string($owner) || $owner === '' ? null : $owner;
} catch (NotFoundException $e) {
$owner = null;
}
$fileId = 0;
$currentUser = $this->currentUser->getUID();
if ($owner === null || $owner !== $currentUser) {
/** @var \OCP\Files\Storage\IStorage $storage */
[$storage,] = $view->resolvePath($path);
if ($owner !== null && !$storage->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
Filesystem::initMountPoints($owner);
} else {
// Probably a remote user, let's try to at least generate activities
// for the current user
if ($currentUser === null) {
[, $owner,] = explode('/', $view->getAbsolutePath($path), 3);
} else {
$owner = $currentUser;
}
}
}
$info = Filesystem::getFileInfo($path);
if ($info !== false) {
$ownerView = new View('/' . $owner . '/files');
$fileId = (int)$info['fileid'];
$path = $ownerView->getPath($fileId);
}
return [$path, $owner, $fileId];
}
/**
* Manage sharing events
*
* @param IShare $share the share from the event
*/
public function share($share) {
switch($share->getShareType()) {
case IShare::TYPE_USER:
$this->shareWithUser(
$share->getSharedWith(),
$share->getNodeId(),
$share->getNodeType(),
$share->getTarget()
);
break;
case IShare::TYPE_GROUP:
$this->shareWithGroup(
$share->getSharedWith(),
$share->getNodeId(),
$share->getNodeType(),
$share->getTarget(),
(int)$share->getId()
);
break;
case IShare::TYPE_LINK:
$this->shareByLink(
$share->getNodeId(),
$share->getNodeType(),
$share->getSharedBy()
);
break;
default:
// Currently not supported
break;
}
}
/**
* Sharing a file or folder with a user
*
* @param string $shareWith
* @param int $fileSource File ID that is being shared
* @param string $itemType File type that is being shared (file or folder)
* @param string $fileTarget File path
*/
protected function shareWithUser($shareWith, $fileSource, $itemType, $fileTarget) {
// User performing the share
$this->shareNotificationForSharer('shared_user_self', $shareWith, $fileSource, $itemType);
if ($this->currentUser->getUID() !== null) {
$this->shareNotificationForOriginalOwners($this->currentUser->getUID(), 'reshared_user_by', $shareWith, $fileSource, $itemType);
}
// New shared user
$this->addNotificationsForUser(
$shareWith, 'shared_with_by', [[$fileSource => $fileTarget], $this->currentUser->getUserIdentifier()],
(int)$fileSource, $fileTarget, $itemType === 'file',
$this->userSettings->getUserSetting($shareWith, 'email', Files_Sharing::TYPE_SHARED) ? $this->userSettings->getUserSetting($shareWith, 'setting', 'batchtime') : false,
(bool) $this->userSettings->getUserSetting($shareWith, 'notification', Files_Sharing::TYPE_SHARED)
);
}
/**
* Sharing a file or folder with a group
*
* @param string $shareWith
* @param int $fileSource File ID that is being shared
* @param string $itemType File type that is being shared (file or folder)
* @param string $fileTarget File path
* @param int $shareId The Share ID of this share
*/
protected function shareWithGroup($shareWith, $fileSource, $itemType, $fileTarget, $shareId) {
// Members of the new group
$group = $this->groupManager->get($shareWith);
if (!($group instanceof IGroup)) {
return;
}
// User performing the share
$this->shareNotificationForSharer('shared_group_self', $shareWith, $fileSource, $itemType);
if ($this->currentUser->getUID() !== null) {
$this->shareNotificationForOriginalOwners($this->currentUser->getUID(), 'reshared_group_by', $shareWith, $fileSource, $itemType);
}
$offset = 0;
$users = $group->searchUsers('', self::USER_BATCH_SIZE, $offset);
while (!empty($users)) {
$this->addNotificationsForGroupUsers($users, 'shared_with_by', $fileSource, $itemType, $fileTarget, $shareId);
$offset += self::USER_BATCH_SIZE;
$users = $group->searchUsers('', self::USER_BATCH_SIZE, $offset);
}
}
/**
* Sharing a file or folder via link/public
*
* @param int $fileSource File ID that is being shared
* @param string $itemType File type that is being shared (file or folder)
* @param string $linkOwner
*/
protected function shareByLink($fileSource, $itemType, $linkOwner) {
$this->view->chroot('/' . $linkOwner . '/files');
try {
$path = $this->view->getPath($fileSource);
} catch (NotFoundException $e) {
return;
}
$this->shareNotificationForOriginalOwners($linkOwner, 'reshared_link_by', '', $fileSource, $itemType);
$this->addNotificationsForUser(
$linkOwner, 'shared_link_self', [[$fileSource => $path]],
(int)$fileSource, $path, $itemType === 'file',
$this->userSettings->getUserSetting($linkOwner, 'email', Files_Sharing::TYPE_SHARED) ? $this->userSettings->getUserSetting($linkOwner, 'setting', 'batchtime') : false,
(bool) $this->userSettings->getUserSetting($linkOwner, 'notification', Files_Sharing::TYPE_SHARED)
);
}
/**
* Manage unsharing events
*
* @param IShare $share
* @throws \OCP\Files\NotFoundException
*/
public function unShare(IShare $share) {
if (in_array($share->getNodeType(), ['file', 'folder'], true)) {
if ($share->getShareType() === IShare::TYPE_USER) {
$this->unshareFromUser($share);
} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
$this->unshareFromGroup($share);
} elseif ($share->getShareType() === IShare::TYPE_LINK) {
$this->unshareLink($share);
}
}
}
/**
* Manage unsharing a share from self only events
*
* @param IShare $share
* @throws \OCP\Files\NotFoundException
*/
public function unShareSelf(IShare $share) {
if (in_array($share->getNodeType(), ['file', 'folder'], true)) {
if ($share->getShareType() === IShare::TYPE_GROUP) {
$this->unshareFromSelfGroup($share);
} else {
$this->unShare($share);
}
}
}
/**
* Unharing a file or folder from a user
*
* @param IShare $share
* @throws \OCP\Files\NotFoundException
*/
protected function unshareFromUser(IShare $share) {
if ($share->getSharedWith() === $this->currentUser->getUID()) {
$this->selfUnshareFromUser($share);
return;
}
if ($share->isExpired()) {
$actionSharer = 'expired_user';
$actionOwner = 'expired_user';
$actionUser = 'expired';
} else {
$actionSharer = 'unshared_user_self';
$actionOwner = 'unshared_user_by';
$actionUser = 'unshared_by';
}
// User performing the share
$this->shareNotificationForSharer($actionSharer, $share->getSharedWith(), $share->getNodeId(), $share->getNodeType());
// Owner
if ($this->currentUser->getUID() !== null) {
$this->shareNotificationForOriginalOwners($this->currentUser->getUID(), $actionOwner, $share->getSharedWith(), $share->getNodeId(), $share->getNodeType());
}
// Recipient
$this->addNotificationsForUser(
$share->getSharedWith(), $actionUser, [[$share->getNodeId() => $share->getTarget()], $this->currentUser->getUserIdentifier()],
$share->getNodeId(), $share->getTarget(), $share->getNodeType() === 'file',
$this->userSettings->getUserSetting($share->getSharedWith(), 'email', Files_Sharing::TYPE_SHARED) ? $this->userSettings->getUserSetting($share->getSharedWith(), 'setting', 'batchtime') : false,
(bool) $this->userSettings->getUserSetting($share->getSharedWith(), 'notification', Files_Sharing::TYPE_SHARED)
);
}
/**
* Unharing a file or folder from a user
*
* @param IShare $share
* @throws \OCP\Files\NotFoundException
*/
protected function selfUnshareFromUser(IShare $share) {
// User performing the share
$this->shareNotificationForSharer('self_unshared', $share->getSharedWith(), $share->getNodeId(), $share->getNodeType());
// Owner
if ($this->currentUser->getUID() !== null) {
$this->shareNotificationForOriginalOwners($this->currentUser->getUID(), 'self_unshared_by', $share->getSharedWith(), $share->getNodeId(), $share->getNodeType());
}
}
/**
* Unsharing a file or folder from a group
*
* @param IShare $share
* @throws \OCP\Files\NotFoundException
*/
protected function unshareFromGroup(IShare $share) {
// Members of the new group
$group = $this->groupManager->get($share->getSharedWith());
if (!($group instanceof IGroup)) {
return;
}
if ($share->isExpired()) {
$actionSharer = 'expired_group';
$actionOwner = 'expired_group';
$actionUser = 'expired';
} else {
$actionSharer = 'unshared_group_self';
$actionOwner = 'unshared_group_by';
$actionUser = 'unshared_by';
}
// User performing the share
$this->shareNotificationForSharer($actionSharer, $share->getSharedWith(), $share->getNodeId(), $share->getNodeType());
if ($this->currentUser->getUID() !== null) {
$this->shareNotificationForOriginalOwners($this->currentUser->getUID(), $actionOwner, $share->getSharedWith(), $share->getNodeId(), $share->getNodeType());
}
$offset = 0;
$users = $group->searchUsers('', self::USER_BATCH_SIZE, $offset);
$shouldFlush = $this->startActivityTransaction();
while (!empty($users)) {
$this->addNotificationsForGroupUsers($users, $actionUser, $share->getNodeId(), $share->getNodeType(), $share->getTarget(), (int) $share->getId());
$offset += self::USER_BATCH_SIZE;
$users = $group->searchUsers('', self::USER_BATCH_SIZE, $offset);
}
$this->commitActivityTransaction($shouldFlush);
}
/**
* Unsharing a file or folder from self from a group share
*
* @param IShare $share
* @throws \OCP\Files\NotFoundException
*/
protected function unshareFromSelfGroup(IShare $share) {
// User performing the unshare
$this->shareNotificationForSharer('self_unshared', $this->currentUser->getUID(), $share->getNodeId(), $share->getNodeType());
// Owner
if ($this->currentUser->getUID() !== null) {
$this->shareNotificationForOriginalOwners($this->currentUser->getUID(), 'self_unshared_by', $this->currentUser->getUID(), $share->getNodeId(), $share->getNodeType());
}
}
/**
* Sharing a file or folder via link/public
*
* @param IShare $share
* @throws \OCP\Files\NotFoundException
*/
protected function unshareLink(IShare $share) {
$owner = $share->getSharedBy();
if ($share->isExpired()) {
// Link expired
$actionSharer = 'link_expired';
$actionOwner = 'link_by_expired';
} else {
$actionSharer = 'unshared_link_self';
$actionOwner = 'unshared_link_by';
}
$this->addNotificationsForUser(
$owner, $actionSharer, [[$share->getNodeId() => $share->getTarget()]],
$share->getNodeId(), $share->getTarget(), $share->getNodeType() === 'file',
$this->userSettings->getUserSetting($owner, 'email', Files_Sharing::TYPE_SHARED) ? $this->userSettings->getUserSetting($owner, 'setting', 'batchtime') : false,
(bool) $this->userSettings->getUserSetting($owner, 'notification', Files_Sharing::TYPE_SHARED)
);
if ($share->getSharedBy() !== $share->getShareOwner()) {
$owner = $share->getShareOwner();
$this->addNotificationsForUser(
$owner, $actionOwner, [[$share->getNodeId() => $share->getTarget()], $share->getSharedBy()],
$share->getNodeId(), $share->getTarget(), $share->getNodeType() === 'file',
$this->userSettings->getUserSetting($owner, 'email', Files_Sharing::TYPE_SHARED) ? $this->userSettings->getUserSetting($owner, 'setting', 'batchtime') : false,
(bool) $this->userSettings->getUserSetting($owner, 'notification', Files_Sharing::TYPE_SHARED)
);
}
}
/**
* @param IUser[] $usersInGroup
* @param string $actionUser
* @param int $fileSource File ID that is being shared
* @param string $itemType File type that is being shared (file or folder)
* @param string $fileTarget File path
* @param int $shareId The Share ID of this share
*/
protected function addNotificationsForGroupUsers(array $usersInGroup, $actionUser, $fileSource, $itemType, $fileTarget, $shareId) {
$affectedUsers = [];
foreach ($usersInGroup as $user) {
$affectedUsers[$user->getUID()] = $fileTarget;
}
// Remove the triggering user, we already managed his notifications
unset($affectedUsers[$this->currentUser->getUID()]);
if (empty($affectedUsers)) {
return;
}
$userIds = array_keys($affectedUsers);
$filteredEmailUsersInGroup = $this->userSettings->filterUsersBySetting($userIds, 'email', Files_Sharing::TYPE_SHARED);
$filteredNotificationUsers = $this->userSettings->filterUsersBySetting($userIds, 'notification', Files_Sharing::TYPE_SHARED);
$affectedUsers = $this->fixPathsForShareExceptions($affectedUsers, $shareId);
$shouldFlush = $this->startActivityTransaction();
foreach ($affectedUsers as $user => $path) {
$this->addNotificationsForUser(
$user, $actionUser, [[$fileSource => $path], $this->currentUser->getUserIdentifier()],
$fileSource, $path, ($itemType === 'file'),
$filteredEmailUsersInGroup[$user] ?? false,
$filteredNotificationUsers[$user] ?? false
);
}
$this->commitActivityTransaction($shouldFlush);
}
/**
* Check when there was a naming conflict and the target is different
* for some of the users
*
* @param array $affectedUsers
* @param int $shareId
* @return mixed
*/
protected function fixPathsForShareExceptions(array $affectedUsers, $shareId) {
$queryBuilder = $this->connection->getQueryBuilder();
$queryBuilder->select(['share_with', 'file_target'])
->from('share')
->where($queryBuilder->expr()->eq('parent', $queryBuilder->createParameter('parent')))
->setParameter('parent', (int)$shareId);
$query = $queryBuilder->executeQuery();
while ($row = $query->fetch()) {
$affectedUsers[$row['share_with']] = $row['file_target'];
}
return $affectedUsers;
}
/**
* Add notifications for the user that shares a file/folder
*
* @param string $subject
* @param string $shareWith
* @param int $fileSource
* @param string $itemType
*/
protected function shareNotificationForSharer($subject, $shareWith, $fileSource, $itemType) {
$sharer = $this->currentUser->getUID();
if ($sharer === null) {
return;
}
$this->view->chroot('/' . $sharer . '/files');
try {
$path = $this->view->getPath($fileSource);
} catch (NotFoundException $e) {
return;
}
$this->addNotificationsForUser(
$sharer, $subject, [[$fileSource => $path], $shareWith],
$fileSource, $path, ($itemType === 'file'),
$this->userSettings->getUserSetting($sharer, 'email', Files_Sharing::TYPE_SHARED) ? $this->userSettings->getUserSetting($sharer, 'setting', 'batchtime') : false,
(bool) $this->userSettings->getUserSetting($sharer, 'notification', Files_Sharing::TYPE_SHARED)
);
}
/**
* Add notifications for the user that shares a file/folder
*
* @param string $owner
* @param string $subject
* @param string $shareWith
* @param int $fileSource
* @param string $itemType
*/
protected function reshareNotificationForSharer($owner, $subject, $shareWith, $fileSource, $itemType) {
$this->view->chroot('/' . $owner . '/files');
try {
$path = $this->view->getPath($fileSource);
} catch (NotFoundException $e) {
return;
}
$this->addNotificationsForUser(
$owner, $subject, [[$fileSource => $path], $this->currentUser->getUserIdentifier(), $shareWith],
$fileSource, $path, ($itemType === 'file'),
$this->userSettings->getUserSetting($owner, 'email', Files_Sharing::TYPE_SHARED) ? $this->userSettings->getUserSetting($owner, 'setting', 'batchtime') : false,
(bool) $this->userSettings->getUserSetting($owner, 'notification', Files_Sharing::TYPE_SHARED)
);
}
/**
* Add notifications for the owners whose files have been reshared
*
* @param string $currentOwner
* @param string $subject
* @param string $shareWith
* @param int $fileSource
* @param string $itemType
*/
protected function shareNotificationForOriginalOwners($currentOwner, $subject, $shareWith, $fileSource, $itemType) {
// Get the full path of the current user
$this->view->chroot('/' . $currentOwner . '/files');
try {
$path = $this->view->getPath($fileSource);
} catch (NotFoundException $e) {
return;
}
/**
* Get the original owner and his path
*/
$owner = $this->view->getOwner($path);
if ($owner !== $currentOwner) {
$this->reshareNotificationForSharer($owner, $subject, $shareWith, $fileSource, $itemType);
}
/**
* Get the sharee who shared the item with the currentUser
*/
$this->view->chroot('/' . $currentOwner . '/files');
try {
$mount = $this->view->getMount($path);
} catch (NotFoundException $ex) {
return;
}
$storage = $mount->getStorage();
if (!$storage->instanceOfStorage('OCA\Files_Sharing\SharedStorage')) {
return;
}
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$shareOwner = $storage->getSharedFrom();
if ($shareOwner === '' || $shareOwner === null || $shareOwner === $owner || $shareOwner === $currentOwner) {
return;
}
$this->reshareNotificationForSharer($shareOwner, $subject, $shareWith, $fileSource, $itemType);
}
/**
* Adds the activity and email for a user when the settings require it
*
* @param string $user
* @param string $subject
* @param array $subjectParams
* @param int $fileId
* @param string $path
* @param bool $isFile If the item is a file, we link to the parent directory
* @param int|false $emailSetting
* @param bool $notificationSetting
* @param string $type
*/
protected function addNotificationsForUser($user, $subject, $subjectParams, $fileId, $path, $isFile, $emailSetting, $notificationSetting, $type = Files_Sharing::TYPE_SHARED) {
$user = (string)$user;
$selfAction = $user === $this->currentUser->getUID();
$app = $type === Files_Sharing::TYPE_SHARED ? 'files_sharing' : 'files';
$link = $this->urlGenerator->linkToRouteAbsolute('files.view.index', [
'dir' => ($isFile) ? dirname($path) : $path,
]);
$objectType = ($fileId) ? 'files' : '';
$event = $this->manager->generateEvent();
try {
$event->setApp($app)
->setType($type)
->setAffectedUser($user)
->setTimestamp(time())
->setSubject($subject, $subjectParams)
->setObject($objectType, $fileId, $path)
->setLink($link);
if ($this->currentUser->getUID() !== null) {
// Allow this to be empty for guests
$event->setAuthor($this->currentUser->getUID());
}
} catch (\InvalidArgumentException $e) {
$this->logger->error(
$e->getMessage(),
[
'app' => 'activity',
'exception' => $e
],
);
}
// Add activity to stream
$activityId = $this->activityData->send($event);
if ($activityId && !$selfAction && $notificationSetting) {
$this->notificationGenerator->sendNotificationForEvent($event, $activityId);
}
// Add activity to mail queue
if ($emailSetting !== false && !$selfAction) {
$latestSend = time() + $emailSetting;
$this->activityData->storeMail($event, $latestSend);
}
}
protected function startActivityTransaction(): bool {
$this->connection->beginTransaction();
return $this->notificationGenerator->deferNotifications();
}
protected function commitActivityTransaction(bool $shouldFlush): void {
if ($shouldFlush) {
$this->notificationGenerator->flushNotifications();
}
$this->connection->commit();
}
/**
* @param int $fileId
*
* @return array
*/
private function getAffectedUsersFromCachedMounts(int $fileId): array {
$affectedUsers = $cachedMounts = [];
$mountsForFile = $this->userMountCache->getMountsForFileId($fileId);
foreach ($mountsForFile as $mount) {
$affectedUsers[$mount->getUser()->getUID()] = $this->getVisiblePath($mount->getPath());
$cachedMounts[] = [
'userId' => $mount->getUser()->getUID(),
'provider' => str_replace('\\\\', '\\', $mount->getMountProvider()),
'path' => $mount->getPath(),
'visiblePath' => $this->getVisiblePath($mount->getPath()),
'storageId' => $mount->getStorageId()
];
}
$unrelatedUsers = $this->getUnrelatedUsers($fileId, $cachedMounts);
return array_filter($affectedUsers, function ($userId) use ($unrelatedUsers): bool {
return !in_array($userId, $unrelatedUsers);
}, ARRAY_FILTER_USE_KEY);
}
/**
* returns an array of users that have confirmed no access to fileId
*
* @param int $fileId
* @param array $cachedMounts
*
* @return string[] list of unrelated userIds
*/
private function getUnrelatedUsers(int $fileId, array $cachedMounts): array {
/** @var \OCA\GroupFolders\ACL\RuleManager $ruleManager */
/** @var \OCA\GroupFolders\Folder\FolderManager $folderManager */
try {
$ruleManager = \OC::$server->get(\OCA\GroupFolders\ACL\RuleManager::class);
$folderManager = \OC::$server->get(\OCA\GroupFolders\Folder\FolderManager::class);
} catch (\Throwable $e) {
return []; // if we have no access to RuleManager, we cannot filter unrelated users
}
/** @var \OCA\GroupFolders\ACL\Rule[] $rules */
$rules = $knownRules = $knownGroupRules = $usersToCheck = $cachedPath = [];
foreach ($cachedMounts as $cachedMount) {
// we are only interested in filtering GroupFolders ACL
if ($cachedMount['provider'] !== 'OCA\GroupFolders\Mount\MountProvider') {
continue;
}
// caching rules based on storage id
$storageId = $cachedMount['storageId'];
if (!array_key_exists($storageId, $knownRules)) {
$knownRules[$storageId] = [];
}
$cachedPath[$cachedMount['userId']] = $fullPath = $cachedMount['path'];
// caching rules based on storage+path to file
if (!array_key_exists($cachedMount['visiblePath'], $knownRules[$storageId])) {
// we need mountPoint and folderId to generate the correct path
try {
$node = $this->rootFolder->get($fullPath);
$mountPoint = $node->getMountPoint();
if (!$mountPoint instanceof \OCA\GroupFolders\Mount\GroupMountPoint
|| !$folderManager->getFolderAclEnabled($mountPoint->getFolderId())) {
continue; // acl are disable
}
$folderPath = $mountPoint->getSourcePath();
$path = substr($fullPath, strlen($mountPoint->getMountPoint()));
} catch (\Exception $e) {
// in case of issue during the process, we can imagine the user have no access to the file
$usersToCheck[] = $cachedMount['userId'];
continue; // we'll catch rules on next user with access to the file
}
// we generate a list of path from top level of group folder to the file itself to get all rules
$paths = [$folderPath];
while ($path !== '') {
$paths[] = $folderPath . '/' . $path;
$path = dirname($path);
if ($path === '.' || $path === '/') {
$path = '';
}
}
// we might already know the rules for some path of the list
$paths = array_filter($paths, function (string $path) use ($knownRules, $storageId): bool {
if (array_key_exists($path, $knownRules[$storageId])) {
return false;
}
return true;
});
// we get and store the rules for each path from the list
$rulesPerPath = $ruleManager->getAllRulesForPaths($storageId, $paths);
foreach (array_keys($rulesPerPath) as $path) {
$rules = array_merge($rules, $rulesPerPath[$path]);
}
$knownRules[$storageId][$cachedMount['visiblePath']] = true;
}
}
// using each rules that disable read permission to generate a list of users
// that might not have access to fileId
foreach ($rules as $rule) {
if (($rule->getMask() & Constants::PERMISSION_READ) === 0
|| ($rule->getPermissions() & Constants::PERMISSION_READ) !== 0) {
continue; // not interested of rules with 'mask' not including read capability (1), or if 'permission' does
}
$mapping = $rule->getUserMapping();
$id = $mapping->getId();
// if mapping is about user
if ($mapping->getType() === 'user' && !in_array($id, $usersToCheck)) {
$usersToCheck[] = $id;
}
// if mapping is about group
if ($mapping->getType() === 'group'
&& !in_array($mapping->getId(), $knownGroupRules)) {
$knownGroupRules[] = $mapping->getId();
$group = $this->groupManager->get($id);
if ($group === null) {
continue;
}
$userIds = array_map(function (IUser $user): string {
return $user->getUID();
}, $group->getUsers());
// merge current user list with members of the group
$usersToCheck = array_values(array_unique(array_merge($usersToCheck, $userIds)));
}
}
// now that we have a list of eventuals filtered users, we confirm they have no access to the file
$filteredUsers = [];
foreach ($usersToCheck as $userId) {
try {
$node = $this->rootFolder->get($cachedPath[$userId]);
if ($node->isReadable()) {
continue; // overkill ? as rootFolder->get() would throw an exception if file is not available
}
} catch (\Exception $e) {
}
$filteredUsers[] = $userId;
}
return $filteredUsers;
}
}