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.
350 lines
14 KiB
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;
|
|
});
|
|
}
|
|
}
|
|
|