user = $userSession->getUser(); $this->registerResponder('xml', fn (DataResponse $data): V1Response => $this->buildOCSResponseXML('xml', $data)); } /** * Regular users can access their own folders, but they only get to see the permission for their own groups * * @param GroupFoldersFolder $folder * @return null|GroupFoldersFolder */ private function filterNonAdminFolder(array $folder): ?array { if ($this->user === null) { return null; } $userGroups = $this->groupManager->getUserGroupIds($this->user); $folder['groups'] = array_filter($folder['groups'], static fn (string $group): bool => in_array($group, $userGroups, true), ARRAY_FILTER_USE_KEY); $folder['group_details'] = array_filter($folder['group_details'], static fn (string $group): bool => in_array($group, $userGroups, true), ARRAY_FILTER_USE_KEY); if ($folder['groups'] !== []) { return $folder; } return null; } /** * @return GroupFoldersFolder */ private function formatFolder(FolderWithMappingsAndCache $folder): array { return [ 'id' => $folder->id, 'mount_point' => $folder->mountPoint, 'quota' => $folder->quota, 'acl' => $folder->acl, 'size' => $folder->rootCacheEntry->getSize(), 'groups' => array_map(fn (array $group): int => $group['permissions'], $folder->groups), 'group_details' => $folder->groups, 'manage' => $folder->manage, ]; } /** * Gets all Groupfolders * * @param bool $applicable Filter by applicable groups * @param non-negative-int $offset Number of items to skip. * @param ?positive-int $limit Number of items to return. * @param 'mount_point'|'quota'|'groups'|'acl' $orderBy The key to order by * @param 'asc'|'desc' $order Sort ascending or descending * @return DataResponse, array{}> * @throws OCSNotFoundException Storage not found * @throws OCSBadRequestException Wrong limit used * * 200: Groupfolders returned */ #[NoAdminRequired] #[FrontpageRoute(verb: 'GET', url: '/folders')] public function getFolders(bool $applicable = false, int $offset = 0, ?int $limit = null, string $orderBy = 'mount_point', string $order = 'asc'): DataResponse { /** @psalm-suppress DocblockTypeContradiction */ if ($limit !== null && $limit <= 0) { throw new OCSBadRequestException('The limit must be greater than 0.'); } /** @psalm-suppress DocblockTypeContradiction */ if (!in_array($orderBy, ['mount_point', 'quota', 'groups', 'acl'], true)) { throw new OCSBadRequestException('The orderBy is not allowed.'); } /** @psalm-suppress DocblockTypeContradiction */ if (!in_array($order, ['asc', 'desc'], true)) { throw new OCSBadRequestException('The order is not allowed.'); } $storageId = $this->getRootFolderStorageId(); if ($storageId === null) { throw new OCSNotFoundException(); } $folders = []; $i = 0; foreach ($this->manager->getAllFoldersWithSize($offset, $limit, $orderBy, $order) as $id => $folder) { // Make them string-indexed for OpenAPI JSON output // JavaScript doesn't preserve JSON object key orders, so we need to manually add this information. $folders[(string)$id] = array_merge($this->formatFolder($folder), [ 'sortIndex' => $offset + $i++, ]); } $isAdmin = $this->delegationService->isAdminF7cloud() || $this->delegationService->isDelegatedAdmin(); if ($isAdmin && !$applicable) { return new DataResponse($folders); } if ($this->delegationService->hasOnlyApiAccess()) { $folders = $this->foldersFilter->getForApiUser($folders); } if ($applicable || !$this->delegationService->hasApiAccess()) { $folders = array_filter(array_map($this->filterNonAdminFolder(...), $folders)); } return new DataResponse($folders); } /** * Gets a Groupfolder by ID * * @param int $id ID of the Groupfolder * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: Groupfolder returned */ #[NoAdminRequired] #[FrontpageRoute(verb: 'GET', url: '/folders/{id}')] public function getFolder(int $id): DataResponse { $storageId = $this->getRootFolderStorageId(); if ($storageId === null) { throw new OCSNotFoundException(); } $folder = $this->checkedGetFolder($id); $folder = $this->formatFolder($folder); if (!$this->delegationService->hasApiAccess()) { $folder = $this->filterNonAdminFolder($folder); if ($folder === null) { throw new OCSNotFoundException(); } } return new DataResponse($folder); } /** * @throws OCSNotFoundException */ private function checkedGetFolder(int $id): FolderWithMappingsAndCache { $folder = $this->manager->getFolder($id); if ($folder === null) { throw new OCSNotFoundException('Groupfolder not found'); } return $folder; } private function checkMountPointExists(string $mountpoint): ?DataResponse { $storageId = $this->getRootFolderStorageId(); if ($storageId === null) { throw new OCSNotFoundException('Groupfolder not found'); } $folders = $this->manager->getAllFolders(); foreach ($folders as $folder) { if ($folder->mountPoint === $mountpoint) { throw new OCSBadRequestException('Mount point already exists'); } } return null; } private function getRootFolderStorageId(): ?int { return $this->rootFolder->getMountPoint()->getNumericStorageId(); } /** * Add a new Groupfolder * * @param string $mountpoint Mountpoint of the new Groupfolder * @param ?string $bucket Overwrite the object store bucket to use for the folder * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * @throws OCSBadRequestException Folder already exists * * 200: Groupfolder added successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders')] public function addFolder(string $mountpoint, ?string $bucket = null): DataResponse { $storageId = $this->rootFolder->getMountPoint()->getNumericStorageId(); if ($storageId === null) { throw new OCSNotFoundException(); } $this->checkMountPointExists(trim($mountpoint)); $options = []; if ($bucket) { $options['bucket'] = $bucket; } $id = $this->manager->createFolder(trim($mountpoint), $options); $folder = $this->checkedGetFolder($id); return new DataResponse($this->formatFolder($folder)); } /** * Remove a Groupfolder * * @param int $id ID of the Groupfolder * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: Groupfolder removed successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'DELETE', url: '/folders/{id}')] public function removeFolder(int $id): DataResponse { $folder = $this->checkedGetFolder($id); $this->folderStorageManager->deleteStoragesForFolder($folder); $this->manager->removeFolder($id); return new DataResponse(['success' => true]); } /** * Set the mount point of a Groupfolder * * @param int $id ID of the Groupfolder * @param string $mountPoint New mount point path * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * @throws OCSBadRequestException Mount point already exists * * 200: Mount point changed successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'PUT', url: '/folders/{id}')] public function setMountPoint(int $id, string $mountPoint): DataResponse { $this->checkMountPointExists(trim($mountPoint)); $this->manager->renameFolder($id, trim($mountPoint)); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Add access of a group for a Groupfolder * * @param int $id ID of the Groupfolder * @param string $group Group to add access for * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: Group access added successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/groups')] public function addGroup(int $id, string $group): DataResponse { $this->checkedGetFolder($id); $this->manager->addApplicableGroup($id, $group); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Remove access of a group from a Groupfolder * * @param int $id ID of the Groupfolder * @param string $group Group to remove access from * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: Group access removed successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'DELETE', url: '/folders/{id}/groups/{group}', requirements: ['group' => '.+'])] public function removeGroup(int $id, string $group): DataResponse { $this->checkedGetFolder($id); $this->manager->removeApplicableGroup($id, $group); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Set the permissions of a group for a Groupfolder * * @param int $id ID of the Groupfolder * @param string $group Group for which the permissions will be set * @param int $permissions New permissions * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: Permissions updated successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/groups/{group}', requirements: ['group' => '.+'])] public function setPermissions(int $id, string $group, int $permissions): DataResponse { $this->checkedGetFolder($id); $this->manager->setGroupPermissions($id, $group, $permissions); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Updates an ACL mapping * * @param int $id ID of the Groupfolder * @param string $mappingType Type of the ACL mapping * @param string $mappingId ID of the ACL mapping * @param bool $manageAcl Whether to enable or disable the ACL mapping * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: ACL mapping updated successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/manageACL')] public function setManageACL(int $id, string $mappingType, string $mappingId, bool $manageAcl): DataResponse { $this->checkedGetFolder($id); $this->manager->setManageACL($id, $mappingType, $mappingId, $manageAcl); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Set a new quota for a Groupfolder * * @param int $id ID of the Groupfolder * @param int $quota New quota in bytes * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: New quota set successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/quota')] public function setQuota(int $id, int $quota): DataResponse { $this->checkedGetFolder($id); $this->manager->setFolderQuota($id, $quota); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Toggle the ACL for a Groupfolder * * @param int $id ID of the Groupfolder * @param bool $acl Whether ACL should be enabled or not * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * * 200: ACL toggled successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/acl')] public function setACL(int $id, bool $acl): DataResponse { $this->checkedGetFolder($id); $this->manager->setFolderACL($id, $acl); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Rename a Groupfolder * * @param int $id ID of the Groupfolder * @param string $mountpoint New Mountpoint of the Groupfolder * @return DataResponse * @throws OCSNotFoundException Groupfolder not found * @throws OCSBadRequestException Mount point already exists or invalid mount point provided * * 200: Groupfolder renamed successfully */ #[PasswordConfirmationRequired] #[RequireGroupFolderAdmin] #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/mountpoint')] public function renameFolder(int $id, string $mountpoint): DataResponse { $this->checkedGetFolder($id); // Check if the new mountpoint is valid if (empty($mountpoint)) { throw new OCSBadRequestException('Mount point cannot be empty'); } $folder = $this->checkedGetFolder($id); if ($folder->mountPoint === trim($mountpoint)) { return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } $this->checkMountPointExists(trim($mountpoint)); $this->manager->renameFolder($id, trim($mountpoint)); $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } /** * Overwrite response builder to customize xml handling to deal with spaces in folder names */ private function buildOCSResponseXML(string $format, DataResponse $data): V1Response { /** @var array $folderData */ $folderData = $data->getData(); if (isset($folderData['id'])) { // single folder response $folderData = $this->folderDataForXML($folderData); } elseif (isset($folderData['folder'])) { // single folder response $folderData['folder'] = $this->folderDataForXML(['folder']); } elseif (is_array($folderData) && count($folderData) && isset(current($folderData)['id'])) { // folder list $folderData = array_map($this->folderDataForXML(...), $folderData); } $data->setData($folderData); return new V1Response($data, $format); } private function folderDataForXML(array $data): array { $groups = $data['group_details'] ?? []; unset($data['group_details']); $data['groups'] = []; foreach ($groups as $id => $group) { $data['groups'][] = [ '@group_id' => $id, '@permissions' => $group['permissions'], '@display-name' => $group['displayName'], '@type' => $group['type'], ]; } return $data; } /** * Searches for matching ACL mappings * * @param int $id The ID of the Groupfolder * @param string $search String to search by * @return DataResponse, groups: list, circles: list}, array{}> * @throws OCSForbiddenException Not allowed to search * * 200: ACL Mappings returned */ #[NoAdminRequired] #[FrontpageRoute(verb: 'GET', url: '/folders/{id}/search')] public function aclMappingSearch(int $id, string $search = ''): DataResponse { $users = $groups = $circles = []; if ($this->user === null) { throw new OCSForbiddenException(); } if ($this->manager->canManageACL($id, $this->user) === true) { $groups = $this->manager->searchGroups($id, $search); $users = $this->manager->searchUsers($id, $search); $circles = $this->manager->searchCircles($id, $search); } return new DataResponse([ 'users' => $users, 'groups' => $groups, 'circles' => $circles ]); } private function compareFolderNames(string $a, string $b): int { if (($value = strnatcmp($a, $b)) === 0) { return $value; } // Folder names starting with '_' get pushed to the end while they are brought to the // beginning in the frontend. Do the same here to keep it consistent with the frontend if (strncmp($a, '_', 1) === 0 && strncmp($b, '_', 1) !== 0) { return -1; } if (strncmp($a, '_', 1) !== 0 && strncmp($b, '_', 1) === 0) { return 1; } return $value; } }