%PDF- %PDF-
Direktori : /www/varak.net/nextcloud.varak.net/apps_old/apps/passwords/lib/Cron/ |
Current File : //www/varak.net/nextcloud.varak.net/apps_old/apps/passwords/lib/Cron/SynchronizeShares.php |
<?php /* * @copyright 2023 Passwords App * * @author Marius David Wieschollek * @license AGPL-3.0 * * This file is part of the Passwords App * created by Marius David Wieschollek. */ namespace OCA\Passwords\Cron; use Exception; use OCA\Passwords\Db\Password; use OCA\Passwords\Db\PasswordRevision; use OCA\Passwords\Db\Share; use OCA\Passwords\Services\ConfigurationService; use OCA\Passwords\Services\EnvironmentService; use OCA\Passwords\Services\LoggingService; use OCA\Passwords\Services\MailService; use OCA\Passwords\Services\NotificationService; use OCA\Passwords\Services\Object\FolderService; use OCA\Passwords\Services\Object\PasswordRevisionService; use OCA\Passwords\Services\Object\PasswordService; use OCA\Passwords\Services\Object\ShareService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Utility\ITimeFactory; /** * Class SynchronizeShares * * @package OCA\Passwords\Cron */ class SynchronizeShares extends AbstractTimedJob { const EXECUTION_TIMESTAMP = 'cron/sharing/time'; /** * @var array */ protected array $notifications = ['created' => [], 'deleted' => [], 'loop' => []]; /** * SynchronizeShares constructor. * * @param ITimeFactory $time * @param LoggingService $logger * @param MailService $mailService * @param ShareService $shareService * @param ConfigurationService $config * @param EnvironmentService $environment * @param PasswordService $passwordService * @param NotificationService $notificationService * @param PasswordRevisionService $passwordRevisionService */ public function __construct( ITimeFactory $time, LoggingService $logger, protected MailService $mailService, protected ShareService $shareService, ConfigurationService $config, EnvironmentService $environment, protected PasswordService $passwordService, protected NotificationService $notificationService, protected PasswordRevisionService $passwordRevisionService ) { parent::__construct($time, $logger, $config, $environment); } /** * @param $argument * * @throws Exception */ protected function runJob($argument): void { if(!$this->canExecute()) return; $this->config->setAppValue(self::EXECUTION_TIMESTAMP, time()); $this->deleteOrphanedTargetPasswords(); $this->deleteExpiredShares(); $this->createNewShares(); $this->removeSharedAttribute(); $this->updatePasswords(); $this->notifyUsers(); $this->config->deleteAppValue(self::EXECUTION_TIMESTAMP); } /** * @return bool */ public function runManually(): bool { try { if($this->canExecute()) { $this->runJob($this->getArgument()); return true; } } catch(Exception $e) { $this->logger->logException($e); } return false; } /** * @throws Exception */ protected function deleteOrphanedTargetPasswords(): void { $total = 0; do { $passwords = $this->passwordService->findOrphanedTargetPasswords(); $count = count($passwords); $total += $count; foreach($passwords as $password) { $shares = $this->shareService->findBySourcePassword($password->getUuid()); foreach($shares as $share) { $this->shareService->delete($share); } $this->passwordService->delete($password); } } while($count !== 0); $this->logger->debugOrInfo(['Deleted %s orphaned password(s)', $total], $total); } /** * @throws Exception */ protected function deleteExpiredShares(): void { $total = 0; do { $shares = $this->shareService->findExpired(); $count = count($shares); $total += $count; foreach($shares as $share) { try { $password = $this->passwordService->findByUuid($share->getTargetPassword()); $this->passwordService->delete($password); } catch(DoesNotExistException $e) { } $this->shareService->delete($share); } } while($count !== 0); $this->logger->debugOrInfo(['Deleted %s expired share(s)', $total], $total); } /** * @throws Exception */ protected function createNewShares(): void { $shares = $this->shareService->findNew(); foreach($shares as $share) { try { if($this->shareLineageHasLoop($share)) continue; $receiverId = $share->getReceiver(); $userId = $share->getUserId(); /** @var PasswordRevision $sourceRevision */ try { $sourceRevision = $this->passwordRevisionService->findCurrentRevisionByModel($share->getSourcePassword(), true); } catch (DoesNotExistException $e) { $this->logger->error("Could not complete share {$share->getUuid()}, source password {$share->getSourcePassword()} does not exist"); $this->shareService->delete($share); /** * @TODO Notification to original user with source password id and target user */ continue; } /** @var Password $model */ $model = $this->passwordService->create(); $model->setUserId($receiverId); $model->setShareId($share->getUuid()); $model->setEditable($share->getEditable()); $revision = $this->passwordRevisionService->create( $model->getUuid(), $sourceRevision->getPassword(), $sourceRevision->getUsername(), '', $sourceRevision->getCseType(), $sourceRevision->getHash(), $sourceRevision->getLabel(), $sourceRevision->getUrl(), $sourceRevision->getNotes(), $sourceRevision->getCustomFields(), FolderService::BASE_FOLDER_UUID, time(), false, false, false ); $revision->setUserId($receiverId); $this->passwordRevisionService->save($revision); $this->passwordService->setRevision($model, $revision); $share->setTargetPassword($model->getUuid()); $share->setSourceUpdated(false); $this->shareService->save($share); if(!isset($this->notifications['created'][ $receiverId ])) $this->notifications['created'][ $receiverId ] = []; if(!isset($this->notifications['created'][ $receiverId ][ $userId ])) $this->notifications['created'][ $receiverId ][ $userId ] = 0; $this->notifications['created'][ $receiverId ][ $userId ]++; } catch(\Throwable $e) { $this->logger->logException( $e, [], "Could not create target password from {$share->getSourcePassword()} for share {$share->getUuid()}: {$e->getMessage()}" ); } } $total = count($shares); $this->logger->debugOrInfo(['Created %s new share(s)', $total], $total); } /** * @param Share $share * * @return bool * @throws MultipleObjectsReturnedException * @throws Exception */ protected function shareLineageHasLoop(Share $share): bool { $sourceUuid = $share->getSourcePassword(); while(1) { try { /** @var Password $password */ $password = $this->passwordService->findByUuid($sourceUuid); } catch(DoesNotExistException $e) { return false; } if($password->getUserId() === $share->getReceiver()) { $this->shareService->delete($share); $userId = $share->getUserId(); if(!isset($this->notifications['loop'][ $userId ])) { $this->notifications['loop'][ $userId ] = 0; } $this->notifications['loop'][ $userId ]++; return true; } if($password->getShareId() === null) return false; try { $parentShare = $this->shareService->findByUuid($password->getShareId()); $sourceUuid = $parentShare->getSourcePassword(); } catch(DoesNotExistException $e) { return false; } } } /** * @throws Exception */ protected function removeSharedAttribute(): void { $passwords = $this->passwordService->findShared(); $total = 0; foreach($passwords as $password) { $shares = $this->shareService->findBySourcePassword($password->getUuid()); if(empty($shares)) { $password->setHasShares(false); $this->passwordService->save($password); $total++; } } $this->logger->debugOrInfo(['Removed shared attribute from %s password(s)', $total], $total); } /** * @throws DoesNotExistException * @throws Exception * @throws MultipleObjectsReturnedException */ protected function updatePasswords(): void { $total = 0; do { $count = $this->updateTargetPasswords(); $count += $this->updateSourcePasswords(); $total += $count; } while($count !== 0); $this->logger->debugOrInfo(['Updated %s share(s)', $total], $total); } /** * @return int * @throws DoesNotExistException * @throws Exception * @throws MultipleObjectsReturnedException */ protected function updateTargetPasswords(): int { $total = 0; $loopCount = 0; do { $shares = $this->shareService->findBySourceUpdated(); $count = count($shares); $total += $count; $loopCount++; if($loopCount > 32) { break; } foreach($shares as $share) { if($share->getTargetPassword() === null) continue; try { $revision = $this->createNewPasswordRevision($share->getSourcePassword(), $share->getTargetPassword()); /** @var Password $password */ $password = $this->passwordService->findByUuid($share->getTargetPassword()); $password->setEditable($share->isEditable()); $this->passwordService->setRevision($password, $revision); $share->setTargetUpdated(false); $share->setSourceUpdated(false); $this->shareService->save($share); if(!$share->isShareable() && $password->hasShares()) { $subShares = $this->shareService->findBySourcePassword($password->getUuid()); foreach($subShares as $subShare) { $this->shareService->delete($subShare); } $this->deleteOrphanedTargetPasswords(); break; } if(!$share->isEditable()) { $subShares = $this->shareService->findBySourcePassword($password->getUuid()); foreach($subShares as $subShare) { if($subShare->isEditable()) { $subShare->setEditable(false); $subShare->setSourceUpdated(true); $this->shareService->save($subShare); } } } } catch(\Throwable $e) { $this->logger->logException( $e, [], "Could not sync {$share->getSourcePassword()} to target {$share->getTargetPassword()} for share {$share->getUuid()}: {$e->getMessage()}" ); } } } while($count !== 0); if($loopCount > 32 && $count !== 0) { $this->logger->error("Failed to update all target passwords in 32 loops, {$count} still pending update"); } return $total; } /** * @return int * @throws DoesNotExistException * @throws Exception * @throws MultipleObjectsReturnedException */ protected function updateSourcePasswords(): int { $total = 0; $loopCount = 0; do { $shares = $this->shareService->findByTargetUpdated(); $count = count($shares); $total += $count; $loopCount++; if($loopCount > 32) { break; } foreach($shares as $share) { try { if($share->isEditable()) { $revision = $this->createNewPasswordRevision($share->getTargetPassword(), $share->getSourcePassword()); $password = $this->passwordService->findByUuid($share->getSourcePassword()); $this->passwordService->setRevision($password, $revision); } $share->setTargetUpdated(false); $share->setSourceUpdated(false); $this->shareService->save($share); } catch(\Throwable $e) { $this->logger->logException( $e, [], "Could not sync {$share->getTargetPassword()} to source {$share->getSourcePassword()} for share {$share->getUuid()}: {$e->getMessage()}" ); } } } while($count !== 0); if($loopCount > 32 && $count !== 0) { $this->logger->error("Failed to update all target passwords in 32 loops, {$count} still pending update"); } return $total; } /** * @param string $sourceUuid * @param string $targetUuid * * @return PasswordRevision * @throws Exception */ protected function createNewPasswordRevision(string $sourceUuid, string $targetUuid): PasswordRevision { /** @var PasswordRevision $sourceRevision */ $sourceRevision = $this->passwordRevisionService->findCurrentRevisionByModel($sourceUuid, true); $currentRevision = $this->passwordRevisionService->findCurrentRevisionByModel($targetUuid, true); /** @var PasswordRevision $newRevision */ $newRevision = $this->passwordRevisionService->clone($currentRevision, [ 'password' => $sourceRevision->getPassword(), 'username' => $sourceRevision->getUsername(), 'cseKey' => $sourceRevision->getCseKey(), 'cseType' => $sourceRevision->getCseType(), 'hash' => $sourceRevision->getHash(), 'label' => $sourceRevision->getLabel(), 'url' => $sourceRevision->getUrl(), 'notes' => $sourceRevision->getNotes(), 'customFields' => $sourceRevision->getCustomFields(), 'status' => $sourceRevision->getStatus(), 'edited' => $sourceRevision->getEdited(), ]); return $this->passwordRevisionService->save($newRevision); } /** * */ protected function notifyUsers(): void { foreach($this->notifications['created'] as $receiver => $owners) { $this->notificationService->sendShareCreatedNotification($receiver, $owners); $this->mailService->sendShareCreateMail($receiver, $owners); } foreach($this->notifications['loop'] as $user => $amount) { $this->notificationService->sendShareLoopNotification($user, $amount); } } /** * @return bool */ protected function canExecute(): bool { $this->config->clearCache(); return $this->environment->getRunType() === EnvironmentService::TYPE_CRON && intval($this->config->getAppValue(self::EXECUTION_TIMESTAMP, 0)) < strtotime('-2 hours'); } }