* * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ namespace OCA\FirstRunMigrate\Migration; use Exception; use OCA\FirstRunMigrate\Migration\MigrationJob; use Throwable; use OC\Share20\DefaultShareProvider; use OC\Share20\ShareAttributes; use OC\Share20\Share; use OCP\IUser; use OCP\IConfig; use OCP\Share\IShare; use OCP\AppFramework\Utility\ITimeFactory; use OCA\FirstRunMigrate\Migration\Utils; use OCP\IUserManager; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; use Psr\Log\LoggerInterface; use OCA\FirstRunMigrate\Exceptions\MissingShareUser; use OCP\BackgroundJob\IJobList; use OCP\IDBConnection; use OCP\Share\Exceptions\AlreadySharedException; use OCP\Share\Exceptions\ShareNotFound; class ShareJob extends MigrationJob { protected LoggerInterface $logger; protected IUserManager $userManager; protected DefaultShareProvider $shareProvider; protected IRootFolder $rootFolder; protected IDBConnection $dbConn; public static string $type = 'share'; protected IJobList $jobList; /** * BackgroundJob constructor. */ public function __construct(ITimeFactory $timeFactory, IUserManager $userManager, LoggerInterface $logger, IRootFolder $rootFolder, IDBConnection $connection, IJobList $jobList) { parent::__construct($timeFactory); $this->setAllowParallelRuns(false); $this->logger = $logger; $this->userManager = $userManager; $this->shareProvider = \OC::$server->get(DefaultShareProvider::class); $this->rootFolder = $rootFolder; $this->dbConn = $connection; $this->jobList = $jobList; } /** * @param array $argument */ protected function run($argument) { $this->logger->debug("Starting shares migration job {$this->getId()} with args " . json_encode($argument)); $user = $this->userManager->get($argument['uid']); self::setMigrationStatus('started', $user); $shares = $this->getUserShares($user); $migrated = 0; $missingUser = 0; $missingFile = 0; $errors = 0; if (!empty($shares)) { foreach ($shares as $share_data) { try { $this->restoreShare($user, $share_data); $migrated++; } catch (MissingShareUser $e) { $missingUser++; $this->logger->error("{$this->getId()}: {$e->getUserType()} user '{$e->getUserID()}' not found"); } catch (NotFoundException $e) { $missingFile++; $this->logger->error("{$this->getId()}: file '{$e->getMessage()}' not found"); } catch (AlreadySharedException) { $this->logger->debug("{$this->getId()}: share already exist"); } catch (Throwable $e) { $errors++; $this->logger->error("{$this->getId()}: error while creating the share: {$e->getMessage()}"); } } } $groupShares = $this->getUsersGroupShares($user); if (!empty($groupShares)) { foreach ($groupShares as $share_data) { try { $this->restoreGroupShare($user, $share_data); $migrated++; } catch (Throwable $e) { $errors++; $this->logger->error("{$this->getId()}: error while accepting group share: {$e->getMessage()}"); } } } if (empty($shares) && empty($groupShares)) { $this->logger->info("{$this->getId()}: No shares to migrate"); } $this->logger->debug("{$this->getId()}: $migrated shares migrated, $missingUser missing users, $missingFile missing files, $errors errors"); self::setMigrationStatus('finished', $user, [$migrated, $missingUser, $missingFile, $errors]); self::schredule_next($user, $this->logger, $this->jobList); } private function createShareFromData(array $share_data, IUser $user) : Share { $share = new Share($this->rootFolder, $this->userManager); $share->setShareType($share_data['type']) ->setSharedBy($this->getUserForShare($user, 'by', $share_data)) ->setShareOwner($this->getUserForShare($user, 'owner', $share_data)) ->setNode($this->getFileForShare($share_data)) ->setTarget($share_data['target']) ->setPermissions($share_data['permissions']); // Shared with if ($share->getShareType() === IShare::TYPE_GROUP) { // Group $share->setSharedWith($share_data['with']); } elseif ($share->getShareType() === IShare::TYPE_USER) { // User $share->setSharedWith($this->getUserForShare($user, 'with', $share_data)); } // Share attributes if (array_key_exists('attributes', $share_data) && !is_null($share_data['attributes'])) { $this->setAttributes($share, $share_data['attributes']); } // Share expiration if (array_key_exists('expiration', $share_data) && !is_null($share_data['expiration'])) { $expiration = (new \DateTime())->setTimestamp($share_data['expiration']); $share->setExpirationDate($expiration); } // Share note if (array_key_exists('note', $share_data) && !is_null($share_data['note'])) { $share->setNote($share_data['note']); } return $share; } /** * @throws MissingShareUser * @throws NotFoundException * @throws Exception */ private function restoreShare(IUser $user, array $share_data) : Share { $share = $this->createShareFromData($share_data, $user); // Avoid creating a duplicated share if (!is_null($this->findExistingShare($share))) { throw new AlreadySharedException('Share already exist', $share); } // Create share $this->shareProvider->create($share); // Accept share if ( $share->getShareType() === IShare::TYPE_USER && // Normal accept for user shares ( !array_key_exists('accepted', $share_data) || // Accept as a default behavoir is_null($share_data['accepted']) || $share_data['accepted'] === IShare::STATUS_ACCEPTED // Or if the share is already accepted ) ) { $this->shareProvider->acceptShare($share, $share->getSharedWith()); } elseif ($share->getShareType() === IShare::TYPE_GROUP && // Special accept for group share array_key_exists('group_users_accepted', $share_data) && !is_null($share_data['group_users_accepted'])) { foreach ($share_data['group_users_accepted'] as $recipient_id => $group_user_accepted) { $recipient = Utils::getUserByID($recipient_id); if (is_null($recipient)) { continue; } $this->groupAccepteShare($share, $recipient, $group_user_accepted['target'], $group_user_accepted['accepted']); } } return $share; } private function restoreGroupShare(IUser $user, array $share_data) { $share = $this->createShareFromData($share_data, $user); $share = $this->findExistingShare($share); if (is_null($share)) { throw new ShareNotFound($share_data); } $group_user_accepted = $share_data['group_users_accepted'][Utils::getUserId($user)]; $this->groupAccepteShare($share, $user, $group_user_accepted['target'], $group_user_accepted['accepted']); } private function groupAccepteShare(Share $share, IUser $recipient, string $target, int $accepted) : int { $this->dbConn->beginTransaction(); // Taken from Nextcloud/lib/private/Share20/DefaultShareProvider.php -> createUserSpecificGroupShare $qb = $this->dbConn->getQueryBuilder(); $qb->insert('share') ->values([ 'share_type' => $qb->createNamedParameter(IShare::TYPE_USERGROUP), 'share_with' => $qb->createNamedParameter($recipient->getUID()), 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), 'parent' => $qb->createNamedParameter($share->getId()), 'item_type' => $qb->createNamedParameter($share->getNodeType()), 'item_source' => $qb->createNamedParameter($share->getNodeId()), 'file_source' => $qb->createNamedParameter($share->getNodeId()), 'file_target' => $qb->createNamedParameter($target), 'permissions' => $qb->createNamedParameter($share->getPermissions()), 'accepted' => $qb->createNamedParameter($accepted), 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()) ])->executeStatement(); $this->dbConn->commit(); return $qb->getLastInsertId(); } private function setAttributes(Share $share, array $attributes_data) { // Taken from Nextcloud/lib/private/Share20/DefaultShareProvider.php -> updateShareAttributes $attributes = new ShareAttributes(); foreach ($attributes_data as $compressedAttribute) { $attributes->setAttribute( $compressedAttribute[0], $compressedAttribute[1], $compressedAttribute[2] ); } $share->setAttributes($attributes); } private function findExistingShare(Share $share) : ?Share { foreach ($this->shareProvider->getSharesByPath($share->getNode()) as $existingShare) { foreach (['getShareType', 'getSharedBy', 'getShareOwner', 'getSharedWith'] as $attr) { if ($existingShare->$attr() !== $share->$attr()) { continue 2; } } return $existingShare; } return null; } /** * @throws MissingShareUser */ private function getUserForShare(IUser $user, string $userType, array $share_data) : string { $userShare = Utils::getUserByID($share_data[$userType]); if (is_null($userShare) || ($userShare->getUID() !== $user->getUID() && !self::isUserMigrated($userShare))) { throw new MissingShareUser($userType, $share_data[$userType]); } return $userShare->getUID(); } /** * @throws NotFoundException */ private function getFileForShare(array $share_data) : Node { if (str_starts_with($share_data['path'], 'files/')) { $path = substr($share_data['path'], 6); $owner = Utils::getUserByID($share_data['owner']); return $this->rootFolder->getUserFolder($owner->getUID())->get($path); } if (str_starts_with($share_data['path'], '__groupfolders/')) { $path = explode('/', $share_data['path']); $gf_id = $this->getGroupFolderByMountPoint($path[1]); $path = implode('/', array_slice($path, 2)); return $this->rootFolder->get('__groupfolders/' . $gf_id . '/' . $path); } else { return $this->rootFolder->get($share_data['path']); } } private function getGroupFolderByMountPoint(string $mountPoint) : int { $query = $this->dbConn->getQueryBuilder(); $query->select('folder_id') ->from('group_folders', 'f') ->where($query->expr()->eq('mount_point', $query->createNamedParameter($mountPoint))); $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); return (int)$row['folder_id']; } private static function getMigrationFile() : ?string { /** @var IConfig */ $config = \OC::$server->get(IConfig::class); return $config->getSystemValue('firstrunmigrate_shares', null); } private static function getShares() : array { return json_decode(file_get_contents(self::getMigrationFile()), true); } public static function isMigration(): bool { return ($file = self::getMigrationFile()) && file_exists($file); } private function getUsersGroupShares(IUser $user) : array { $id = Utils::getUserId($user); return array_filter($this->getShares(), function (array $v) use ($id) { return $v["type"] === IShare::TYPE_GROUP && array_key_exists($id, $v['group_users_accepted']); }); } private function getUserShares(IUser $user) : array { $id = Utils::getUserId($user); return array_filter($this->getShares(), function (array $v) use ($id) { return ($v["type"] === IShare::TYPE_USER && $v["with"] === $id) || $v["owner"] === $id || $v["type"] === $id; }); } }