526 lines
16 KiB
PHP
526 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2021 F7cloud GmbH and F7cloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Circles\Service;
|
|
|
|
use OC;
|
|
use OCA\Circles\Db\EventWrapperRequest;
|
|
use OCA\Circles\Db\MemberRequest;
|
|
use OCA\Circles\Db\RemoteRequest;
|
|
use OCA\Circles\Db\ShareLockRequest;
|
|
use OCA\Circles\Exceptions\FederatedEventException;
|
|
use OCA\Circles\Exceptions\FederatedItemException;
|
|
use OCA\Circles\Exceptions\FederatedShareBelongingException;
|
|
use OCA\Circles\Exceptions\FederatedShareNotFoundException;
|
|
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
|
|
use OCA\Circles\Exceptions\OwnerNotFoundException;
|
|
use OCA\Circles\Exceptions\RemoteInstanceException;
|
|
use OCA\Circles\Exceptions\RemoteNotFoundException;
|
|
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
|
|
use OCA\Circles\Exceptions\RequestBuilderException;
|
|
use OCA\Circles\Exceptions\UnknownRemoteException;
|
|
use OCA\Circles\FederatedItems\CircleCreate;
|
|
use OCA\Circles\IFederatedItem;
|
|
use OCA\Circles\IFederatedItemAsyncProcess;
|
|
use OCA\Circles\IFederatedItemCircleCheckNotRequired;
|
|
use OCA\Circles\IFederatedItemDataRequestOnly;
|
|
use OCA\Circles\IFederatedItemHighSeverity;
|
|
use OCA\Circles\IFederatedItemInitiatorCheckNotRequired;
|
|
use OCA\Circles\IFederatedItemInitiatorMembershipNotRequired;
|
|
use OCA\Circles\IFederatedItemLimitedToInstanceWithMembership;
|
|
use OCA\Circles\IFederatedItemLoopbackTest;
|
|
use OCA\Circles\IFederatedItemMemberCheckNotRequired;
|
|
use OCA\Circles\IFederatedItemMemberEmpty;
|
|
use OCA\Circles\IFederatedItemMemberOptional;
|
|
use OCA\Circles\IFederatedItemMemberRequired;
|
|
use OCA\Circles\IFederatedItemMustBeInitializedLocally;
|
|
use OCA\Circles\IFederatedItemSharedItem;
|
|
use OCA\Circles\Model\Circle;
|
|
use OCA\Circles\Model\Federated\EventWrapper;
|
|
use OCA\Circles\Model\Federated\FederatedEvent;
|
|
use OCA\Circles\Model\Federated\RemoteInstance;
|
|
use OCA\Circles\Model\Member;
|
|
use OCA\Circles\Tools\ActivityPub\NCSignature;
|
|
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
|
|
use OCA\Circles\Tools\Model\NCRequest;
|
|
use OCA\Circles\Tools\Model\Request;
|
|
use OCA\Circles\Tools\Traits\TNCRequest;
|
|
use OCA\Circles\Tools\Traits\TStringTools;
|
|
use OCP\Server;
|
|
use ReflectionClass;
|
|
use ReflectionException;
|
|
|
|
/**
|
|
* Class FederatedEventService
|
|
*
|
|
* @package OCA\Circles\Service
|
|
*/
|
|
class FederatedEventService extends NCSignature {
|
|
use TNCRequest;
|
|
use TStringTools;
|
|
|
|
|
|
/** @var EventWrapperRequest */
|
|
private $eventWrapperRequest;
|
|
|
|
/** @var RemoteRequest */
|
|
private $remoteRequest;
|
|
|
|
/** @var ShareLockRequest */
|
|
private $shareLockRequest;
|
|
|
|
/** @var MemberRequest */
|
|
private $memberRequest;
|
|
|
|
/** @var RemoteUpstreamService */
|
|
private $remoteUpstreamService;
|
|
|
|
/** @var EventService */
|
|
private $eventService;
|
|
|
|
/** @var InterfaceService */
|
|
private $interfaceService;
|
|
|
|
/** @var ConfigService */
|
|
private $configService;
|
|
|
|
|
|
/**
|
|
* FederatedEventService constructor.
|
|
*
|
|
* @param EventWrapperRequest $eventWrapperRequest
|
|
* @param RemoteRequest $remoteRequest
|
|
* @param MemberRequest $memberRequest
|
|
* @param ShareLockRequest $shareLockRequest
|
|
* @param RemoteUpstreamService $remoteUpstreamService
|
|
* @param InterfaceService $interfaceService
|
|
* @param ConfigService $configService
|
|
*/
|
|
public function __construct(
|
|
EventWrapperRequest $eventWrapperRequest,
|
|
RemoteRequest $remoteRequest,
|
|
MemberRequest $memberRequest,
|
|
ShareLockRequest $shareLockRequest,
|
|
RemoteUpstreamService $remoteUpstreamService,
|
|
EventService $eventService,
|
|
InterfaceService $interfaceService,
|
|
ConfigService $configService,
|
|
) {
|
|
$this->eventWrapperRequest = $eventWrapperRequest;
|
|
$this->remoteRequest = $remoteRequest;
|
|
$this->shareLockRequest = $shareLockRequest;
|
|
$this->memberRequest = $memberRequest;
|
|
$this->remoteUpstreamService = $remoteUpstreamService;
|
|
$this->eventService = $eventService;
|
|
$this->interfaceService = $interfaceService;
|
|
$this->configService = $configService;
|
|
}
|
|
|
|
|
|
/**
|
|
* Called when creating a new Event.
|
|
* This method will manage the event locally and upstream the payload if needed.
|
|
*
|
|
* @param FederatedEvent $event
|
|
*
|
|
* @return array
|
|
* @throws FederatedEventException
|
|
* @throws FederatedItemException
|
|
* @throws InitiatorNotConfirmedException
|
|
* @throws OwnerNotFoundException
|
|
* @throws RemoteNotFoundException
|
|
* @throws RemoteResourceNotFoundException
|
|
* @throws UnknownRemoteException
|
|
* @throws RemoteInstanceException
|
|
* @throws RequestBuilderException
|
|
*/
|
|
public function newEvent(FederatedEvent $event): array {
|
|
$event->setOrigin($this->interfaceService->getLocalInstance())
|
|
->resetData();
|
|
|
|
$federatedItem = $this->getFederatedItem($event, false);
|
|
$this->confirmInitiator($event, true);
|
|
|
|
if ($event->canBypass(FederatedEvent::BYPASS_CIRCLE)) {
|
|
$instance = $this->interfaceService->getLocalInstance();
|
|
} else {
|
|
$instance = $event->getCircle()->getInstance();
|
|
}
|
|
|
|
if ($this->configService->isLocalInstance($instance)) {
|
|
$event->setSender($instance);
|
|
$federatedItem->verify($event);
|
|
|
|
if ($event->isDataRequestOnly()) {
|
|
return $event->getOutcome();
|
|
}
|
|
|
|
if (OC::$CLI || !$event->isAsync()) {
|
|
$federatedItem->manage($event);
|
|
}
|
|
|
|
if (!$this->initBroadcast($event)
|
|
&& $event->getClass() === CircleCreate::class) {
|
|
// Circle Creation is done in a different way as there is no Circle nor Members yet to
|
|
// base the broadcast to other instances, unless in GlobalScale. And the fact that we do
|
|
// not want to async the process.
|
|
// The result is that in a single instance setup, the CircleCreatedEvent is not trigger
|
|
// the usual (async) way.
|
|
// In case of no instances yet available for that circle, we call the event manually.
|
|
$this->eventService->circleCreated($event, [$event->getResult()]);
|
|
}
|
|
} else {
|
|
$this->remoteUpstreamService->confirmEvent($event);
|
|
if ($event->isDataRequestOnly()) {
|
|
return $event->getOutcome();
|
|
}
|
|
|
|
// if (!$event->isAsync()) {
|
|
// $federatedItem->manage($event);
|
|
// }
|
|
}
|
|
|
|
return $event->getOutcome();
|
|
}
|
|
|
|
|
|
/**
|
|
* This confirmation is optional, method is just here to avoid going too far away on the process
|
|
*
|
|
* @param FederatedEvent $event
|
|
* @param bool $local
|
|
*
|
|
* @throws InitiatorNotConfirmedException
|
|
*/
|
|
public function confirmInitiator(FederatedEvent $event, bool $local = false): void {
|
|
if ($event->canBypass(FederatedEvent::BYPASS_INITIATORCHECK)) {
|
|
return;
|
|
}
|
|
|
|
$circle = $event->getCircle();
|
|
if (!$circle->hasInitiator()) {
|
|
throw new InitiatorNotConfirmedException('Initiator does not exist');
|
|
}
|
|
|
|
if ($local) {
|
|
if (!$this->configService->isLocalInstance($circle->getInitiator()->getInstance())) {
|
|
throw new InitiatorNotConfirmedException(
|
|
'Initiator is not from the instance at the origin of the request'
|
|
);
|
|
}
|
|
} else {
|
|
if ($circle->getInitiator()->getInstance() !== $event->getSender()) {
|
|
throw new InitiatorNotConfirmedException(
|
|
'Initiator must belong to the instance at the origin of the request'
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!$event->canBypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP)
|
|
&& $circle->getInitiator()->getLevel() < Member::LEVEL_MEMBER) {
|
|
throw new InitiatorNotConfirmedException('Initiator must be a member of the Team');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param FederatedEvent $event
|
|
* @param bool $checkLocalOnly
|
|
*
|
|
* @return IFederatedItem
|
|
* @throws FederatedEventException
|
|
*/
|
|
public function getFederatedItem(FederatedEvent $event, bool $checkLocalOnly = true): IFederatedItem {
|
|
$class = $event->getClass();
|
|
try {
|
|
$test = new ReflectionClass($class);
|
|
} catch (ReflectionException $e) {
|
|
throw new FederatedEventException('ReflectionException with ' . $class . ': ' . $e->getMessage());
|
|
}
|
|
|
|
if (!in_array(IFederatedItem::class, $test->getInterfaceNames())) {
|
|
throw new FederatedEventException($class . ' does not implements IFederatedItem');
|
|
}
|
|
|
|
$item = Server::get($class);
|
|
if (!($item instanceof IFederatedItem)) {
|
|
throw new FederatedEventException($class . ' not an IFederatedItem');
|
|
}
|
|
|
|
if ($item instanceof IFederatedItemHighSeverity) {
|
|
$event->setSeverity(FederatedEvent::SEVERITY_HIGH);
|
|
}
|
|
|
|
$this->setFederatedEventBypass($event, $item);
|
|
$this->confirmRequiredCondition($event, $item, $checkLocalOnly);
|
|
$this->configureEvent($event, $item);
|
|
|
|
// $this->confirmSharedItem($event, $item);
|
|
|
|
return $item;
|
|
}
|
|
|
|
|
|
/**
|
|
* Some event might need to bypass some checks
|
|
*
|
|
* @param FederatedEvent $event
|
|
* @param IFederatedItem $item
|
|
*/
|
|
private function setFederatedEventBypass(FederatedEvent $event, IFederatedItem $item) {
|
|
if ($item instanceof IFederatedItemLoopbackTest) {
|
|
$event->bypass(FederatedEvent::BYPASS_CIRCLE);
|
|
$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
|
|
}
|
|
if ($item instanceof IFederatedItemCircleCheckNotRequired) {
|
|
$event->bypass(FederatedEvent::BYPASS_LOCALCIRCLECHECK);
|
|
}
|
|
if ($item instanceof IFederatedItemMemberCheckNotRequired) {
|
|
$event->bypass(FederatedEvent::BYPASS_LOCALMEMBERCHECK);
|
|
}
|
|
if ($item instanceof IFederatedItemInitiatorCheckNotRequired) {
|
|
$event->bypass(FederatedEvent::BYPASS_INITIATORCHECK);
|
|
}
|
|
if ($item instanceof IFederatedItemInitiatorMembershipNotRequired) {
|
|
$event->bypass(FederatedEvent::BYPASS_INITIATORMEMBERSHIP);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Some event might require additional check
|
|
*
|
|
* @param FederatedEvent $event
|
|
* @param IFederatedItem $item
|
|
* @param bool $checkLocalOnly
|
|
*
|
|
* @throws FederatedEventException
|
|
*/
|
|
private function confirmRequiredCondition(
|
|
FederatedEvent $event,
|
|
IFederatedItem $item,
|
|
bool $checkLocalOnly = true,
|
|
) {
|
|
if (!$event->canBypass(FederatedEvent::BYPASS_CIRCLE) && !$event->hasCircle()) {
|
|
throw new FederatedEventException('FederatedEvent has no Circle linked');
|
|
}
|
|
|
|
// TODO: enforce IFederatedItemMemberEmpty if no member
|
|
if ($item instanceof IFederatedItemMemberEmpty) {
|
|
$event->setMember(null);
|
|
} elseif ($item instanceof IFederatedItemMemberRequired && !$event->hasMember()) {
|
|
throw new FederatedEventException('FederatedEvent has no Member linked');
|
|
}
|
|
|
|
if ($event->hasMember()
|
|
&& !($item instanceof IFederatedItemMemberRequired)
|
|
&& !($item instanceof IFederatedItemMemberOptional)) {
|
|
throw new FederatedEventException(
|
|
get_class($item)
|
|
. ' does not implements IFederatedItemMemberOptional nor IFederatedItemMemberRequired'
|
|
);
|
|
}
|
|
|
|
if ($item instanceof IFederatedItemMustBeInitializedLocally && $checkLocalOnly) {
|
|
throw new FederatedEventException('FederatedItem must be executed locally');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param FederatedEvent $event
|
|
* @param IFederatedItem $item
|
|
*
|
|
* @throws FederatedEventException
|
|
* @throws FederatedShareBelongingException
|
|
* @throws FederatedShareNotFoundException
|
|
* @throws OwnerNotFoundException
|
|
*/
|
|
private function confirmSharedItem(FederatedEvent $event, IFederatedItem $item): void {
|
|
if (!$item instanceof IFederatedItemSharedItem) {
|
|
return;
|
|
}
|
|
|
|
if ($event->getItemId() === '') {
|
|
throw new FederatedEventException('FederatedItem must contains ItemId');
|
|
}
|
|
|
|
if ($this->configService->isLocalInstance($event->getCircle()->getInstance())) {
|
|
$shareLock = $this->shareLockRequest->getShare($event->getItemId());
|
|
if ($shareLock->getInstance() !== $event->getSender()) {
|
|
throw new FederatedShareBelongingException('ShareLock belongs to another instance');
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param FederatedEvent $event
|
|
* @param IFederatedItem $item
|
|
*/
|
|
private function configureEvent(FederatedEvent $event, IFederatedItem $item) {
|
|
if ($item instanceof IFederatedItemAsyncProcess && !$event->isForceSync()) {
|
|
$event->setAsync(true);
|
|
}
|
|
if ($item instanceof IFederatedItemLimitedToInstanceWithMembership) {
|
|
$event->setLimitedToInstanceWithMember(true);
|
|
}
|
|
if ($item instanceof IFederatedItemDataRequestOnly) {
|
|
$event->setDataRequestOnly(true);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* async the process, generate a local request that will be closed.
|
|
*
|
|
* @param FederatedEvent $event
|
|
*
|
|
* @throws RequestBuilderException
|
|
*/
|
|
public function initBroadcast(FederatedEvent $event): bool {
|
|
$instances = $this->getInstances($event);
|
|
// if empty instance and ran from CLI, any action as already been managed
|
|
if (empty($instances) && (!$event->isAsync() || OC::$CLI)) {
|
|
return false;
|
|
}
|
|
|
|
$wrapper = new EventWrapper();
|
|
$wrapper->setEvent($event);
|
|
$wrapper->setToken($this->uuid());
|
|
$wrapper->setCreation(time());
|
|
$wrapper->setSeverity($event->getSeverity());
|
|
|
|
$avoidDuplicate = [];
|
|
if ($event->isAsync()) {
|
|
$wrapper->setInstance($this->configService->getLoopbackInstance());
|
|
$this->eventWrapperRequest->save($wrapper);
|
|
$avoidDuplicate[] = $this->configService->getLoopbackInstance();
|
|
}
|
|
|
|
foreach ($instances as $instance) {
|
|
if ($event->getCircle()->isConfig(Circle::CFG_LOCAL)) {
|
|
break;
|
|
}
|
|
|
|
if (in_array($instance->getInstance(), $avoidDuplicate, true)) {
|
|
Server::get(\Psr\Log\LoggerInterface::class)->warning('duplicate instance, please verify the setup of Federated Teams', ['duplicate' => $avoidDuplicate, 'loopback' => $this->configService->getLoopbackInstance(), 'instance' => $instance->getInstance(), 'interface' => $instance->getInterface()]);
|
|
continue;
|
|
}
|
|
|
|
$wrapper->setInstance($instance->getInstance());
|
|
$wrapper->setInterface($instance->getInterface());
|
|
$this->eventWrapperRequest->save($wrapper);
|
|
$avoidDuplicate[] = $wrapper->getInstance();
|
|
}
|
|
|
|
$request = new NCRequest('', Request::TYPE_POST);
|
|
$this->configService->configureLoopbackRequest(
|
|
$request,
|
|
'circles.EventWrapper.asyncBroadcast',
|
|
['token' => $wrapper->getToken()]
|
|
);
|
|
|
|
$event->setWrapperToken($wrapper->getToken());
|
|
|
|
try {
|
|
$this->doRequest($request);
|
|
} catch (RequestNetworkException $e) {
|
|
$this->e($e);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param FederatedEvent $event
|
|
*
|
|
* @return RemoteInstance[]
|
|
* @throws RequestBuilderException
|
|
*/
|
|
public function getInstances(FederatedEvent $event): array {
|
|
if (!$event->hasCircle()) {
|
|
return [];
|
|
}
|
|
|
|
$circle = $event->getCircle();
|
|
$broadcastAsFederated = $event->getData()->gBool('_broadcastAsFederated');
|
|
$instances = $this->remoteRequest->getOutgoingRecipient($circle, $broadcastAsFederated);
|
|
|
|
if ($event->isLimitedToInstanceWithMember()) {
|
|
$knownInstances = $this->memberRequest->getMemberInstances($circle->getSingleId());
|
|
$instances = array_filter(
|
|
array_map(
|
|
function (RemoteInstance $instance) use ($knownInstances) {
|
|
if (!in_array($instance->getInstance(), $knownInstances)) {
|
|
return null;
|
|
}
|
|
|
|
return $instance;
|
|
}, $instances
|
|
)
|
|
);
|
|
}
|
|
|
|
// Check that in case of event has Member, the instance of that member is in the list.
|
|
if ($event->hasMember()
|
|
&& !$this->configService->isLocalInstance($event->getMember()->getInstance())) {
|
|
$currentInstances = array_map(
|
|
function (RemoteInstance $instance): string {
|
|
return $instance->getInstance();
|
|
}, $instances
|
|
);
|
|
|
|
if (!in_array($event->getMember()->getInstance(), $currentInstances)) {
|
|
try {
|
|
$instances[] = $this->remoteRequest->getFromInstance($event->getMember()->getInstance());
|
|
} catch (RemoteNotFoundException $e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
return $instances;
|
|
}
|
|
|
|
|
|
/**
|
|
* should be used to manage results from events, like sending mails on user creation
|
|
*
|
|
* @param string $token
|
|
*/
|
|
public function manageResults(string $token): void {
|
|
$wrappers = $this->eventWrapperRequest->getByToken($token);
|
|
|
|
$event = null;
|
|
$results = [];
|
|
foreach ($wrappers as $wrapper) {
|
|
if ($wrapper->getStatus() !== EventWrapper::STATUS_DONE) {
|
|
return;
|
|
}
|
|
|
|
if (is_null($event)) {
|
|
$event = $wrapper->getEvent();
|
|
}
|
|
|
|
$results[$wrapper->getInstance()] = $wrapper->getResult();
|
|
}
|
|
|
|
if (is_null($event)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$gs = $this->getFederatedItem($event, false);
|
|
$gs->result($event, $results);
|
|
} catch (FederatedEventException $e) {
|
|
}
|
|
}
|
|
}
|