A first run migration tool
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
firstrunmigrate/lib/Migration/ShareJob.php

350 lines
14 KiB

<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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;
});
}
}