migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,821 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\CircleNameTooShortException;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\FederatedEventException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
use OCA\Circles\Exceptions\InitiatorNotFoundException;
use OCA\Circles\Exceptions\MembersLimitException;
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\CircleConfig;
use OCA\Circles\FederatedItems\CircleCreate;
use OCA\Circles\FederatedItems\CircleDestroy;
use OCA\Circles\FederatedItems\CircleEdit;
use OCA\Circles\FederatedItems\CircleJoin;
use OCA\Circles\FederatedItems\CircleLeave;
use OCA\Circles\FederatedItems\CircleSetting;
use OCA\Circles\IEntity;
use OCA\Circles\IFederatedUser;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\ManagedModel;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\Probes\DataProbe;
use OCA\Circles\Model\Probes\MemberProbe;
use OCA\Circles\StatusCode;
use OCA\Circles\Tools\Exceptions\InvalidItemException;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCA\Circles\Tools\Traits\TDeserialize;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IL10N;
use OCP\Security\IHasher;
class CircleService {
use TArrayTools;
use TStringTools;
use TNCLogger;
use TDeserialize;
public const CACHE_GET_CIRCLES = 'circles/getCircles';
public const CACHE_GET_CIRCLES_TTL = 300;
/** @var IL10N */
private $l10n;
/** @var IHasher */
private $hasher;
/** @var ICache $cache */
private $cache;
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var RemoteStreamService */
private $remoteStreamService;
/** @var FederatedUserService */
private $federatedUserService;
/** @var FederatedEventService */
private $federatedEventService;
/** @var MemberService */
private $memberService;
/** @var PermissionService */
private $permissionService;
/** @var ConfigService */
private $configService;
/**
* @param IL10N $l10n
* @param IHasher $hasher
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param RemoteStreamService $remoteStreamService
* @param FederatedUserService $federatedUserService
* @param FederatedEventService $federatedEventService
* @param MemberService $memberService
* @param PermissionService $permissionService
* @param ConfigService $configService
*/
public function __construct(
IL10N $l10n,
IHasher $hasher,
ICacheFactory $cacheFactory,
CircleRequest $circleRequest,
MemberRequest $memberRequest,
RemoteStreamService $remoteStreamService,
FederatedUserService $federatedUserService,
FederatedEventService $federatedEventService,
MemberService $memberService,
PermissionService $permissionService,
ConfigService $configService
) {
$this->l10n = $l10n;
$this->hasher = $hasher;
$this->cache = $cacheFactory->createDistributed(self::CACHE_GET_CIRCLES);
$this->circleRequest = $circleRequest;
$this->memberRequest = $memberRequest;
$this->remoteStreamService = $remoteStreamService;
$this->federatedUserService = $federatedUserService;
$this->federatedEventService = $federatedEventService;
$this->memberService = $memberService;
$this->permissionService = $permissionService;
$this->configService = $configService;
$this->setup('app', Application::APP_ID);
}
/**
* @param string $name
* @param FederatedUser|null $owner
* @param bool $personal
* @param bool $local
*
* @return array
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws RequestBuilderException
* @throws CircleNameTooShortException
*/
public function create(
string $name,
?FederatedUser $owner = null,
bool $personal = false,
bool $local = false
): array {
$this->federatedUserService->mustHaveCurrentUser();
if (is_null($owner)) {
$owner = $this->federatedUserService->getCurrentUser();
}
if (is_null($owner)) {
$owner = $this->federatedUserService->getCurrentApp();
}
if (is_null($owner)) {
throw new OwnerNotFoundException('owner not defined');
}
$circle = new Circle();
$circle->setName($this->cleanCircleName($name))
->setSingleId($this->token(ManagedModel::ID_LENGTH))
->setSource(Member::TYPE_CIRCLE);
if (strlen($circle->getName()) < 3) {
throw new CircleNameTooShortException('Circle name is too short');
}
if ($personal) {
$circle->setConfig(Circle::CFG_PERSONAL);
}
if ($local) {
$circle->addConfig(Circle::CFG_LOCAL);
}
$this->confirmName($circle);
$this->permissionService->confirmAllowedCircleTypes($circle);
$member = new Member();
$member->importFromIFederatedUser($owner);
$member->setId($this->token(ManagedModel::ID_LENGTH))
->setCircleId($circle->getSingleId())
->setLevel(Member::LEVEL_OWNER)
->setStatus(Member::STATUS_MEMBER);
$this->federatedUserService->setMemberPatron($member);
$circle->setOwner($member)
->setInitiator($member);
$event = new FederatedEvent(CircleCreate::class);
$event->setCircle($circle);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function destroy(string $circleId): array {
$this->federatedUserService->mustHaveCurrentUser();
$circle = $this->getCircle($circleId);
$event = new FederatedEvent(CircleDestroy::class);
$event->setCircle($circle);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
* @param int $config
* @param bool $superSession
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function updateConfig(string $circleId, int $config): array {
$this->federatedUserService->mustHaveCurrentUser();
$circle = $this->getCircle($circleId);
$event = new FederatedEvent(CircleConfig::class);
$event->setCircle($circle);
$event->setParams(
new SimpleDataStore(
[
'config' => $config,
'superSession' => $this->federatedUserService->canBypassCurrentUserCondition()
]
)
);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* if $value is null, setting is unset
*
* @param string $circleId
* @param string $setting
* @param string|null $value
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function updateSetting(string $circleId, string $setting, ?string $value): array {
$circle = $this->getCircle($circleId);
if (strtolower($setting) === 'password_single' && !is_null($value)) {
$value = $this->hasher->hash($value);
}
$event = new FederatedEvent(CircleSetting::class);
$event->setCircle($circle);
$event->setParams(
new SimpleDataStore(
[
'setting' => $setting,
'value' => $value,
'unset' => is_null($value)
]
)
);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
* @param string $name
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function updateName(string $circleId, string $name): array {
$circle = $this->getCircle($circleId);
$event = new FederatedEvent(CircleEdit::class);
$event->setCircle($circle);
$event->setParams(new SimpleDataStore(['name' => $name]));
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
* @param string $description
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function updateDescription(string $circleId, string $description): array {
$circle = $this->getCircle($circleId);
$event = new FederatedEvent(CircleEdit::class);
$event->setCircle($circle);
$event->setParams(new SimpleDataStore(['description' => $description]));
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws RequestBuilderException
*/
public function circleJoin(string $circleId): array {
$this->federatedUserService->mustHaveCurrentUser();
$probe = new CircleProbe();
$probe->includeNonVisibleCircles()
->emulateVisitor();
$circle = $this->circleRequest->getCircle(
$circleId,
$this->federatedUserService->getCurrentUser(),
$probe
);
if (!$circle->getInitiator()->hasInvitedBy()) {
$this->federatedUserService->setMemberPatron($circle->getInitiator());
}
$event = new FederatedEvent(CircleJoin::class);
$event->setCircle($circle);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
* @param bool $force
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function circleLeave(string $circleId, bool $force = false): array {
$this->federatedUserService->mustHaveCurrentUser();
$probe = new CircleProbe();
$probe->includeNonVisibleCircles()
->emulateVisitor();
$circle = $this->circleRequest->getCircle(
$circleId,
$this->federatedUserService->getCurrentUser(),
$probe
);
$event = new FederatedEvent(CircleLeave::class);
$event->setCircle($circle);
$event->getParams()->sBool('force', $force);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
* @param CircleProbe|null $probe
*
* @return Circle
* @throws CircleNotFoundException
* @throws InitiatorNotFoundException
* @throws RequestBuilderException
*/
public function getCircle(
string $circleId,
?CircleProbe $probe = null
): Circle {
$this->federatedUserService->mustHaveCurrentUser();
return $this->circleRequest->getCircle(
$circleId,
$this->federatedUserService->getCurrentEntity(),
$probe
);
}
/**
* @return Circle[]
* @throws InitiatorNotFoundException
* @throws RequestBuilderException
*/
public function getCircles(CircleProbe $probe, bool $caching = false): array {
$this->federatedUserService->mustHaveCurrentUser();
// This is a quick solution before implementation of DataProbe
if ($caching && !is_null($this->federatedUserService->getCurrentUser())) {
$key = $this->generateGetCirclesCacheKey(
$this->federatedUserService->getCurrentUser(),
$probe->getChecksum()
);
$cachedData = $this->cache->get($key);
try {
if (!is_string($cachedData)) {
throw new InvalidItemException();
}
return $this->deserializeList($cachedData, Circle::class);
} catch (InvalidItemException $e) {
}
}
$circles = $this->circleRequest->getCircles(
$this->federatedUserService->getCurrentUser(),
$probe
);
if ($caching && !is_null($this->federatedUserService->getCurrentUser())) {
$this->cache->set($key, json_encode($circles), self::CACHE_GET_CIRCLES_TTL);
}
return $circles;
}
/**
* @param Circle $circle
*
* @throws RequestBuilderException
*/
public function confirmName(Circle $circle): void {
if ($circle->isConfig(Circle::CFG_SYSTEM)
|| $circle->isConfig(Circle::CFG_SINGLE)) {
return;
}
$this->confirmDisplayName($circle);
$this->generateSanitizedName($circle);
}
/**
* @param Circle $circle
*
* @throws RequestBuilderException
*/
private function confirmDisplayName(Circle $circle) {
$baseDisplayName = $circle->getName();
$i = 1;
while (true) {
$testDisplayName = $baseDisplayName . (($i > 1) ? ' (' . $i . ')' : '');
$test = new Circle();
$test->setDisplayName($testDisplayName);
try {
$stored = $this->circleRequest->searchCircle($test);
if ($stored->getSingleId() === $circle->getSingleId()) {
throw new CircleNotFoundException();
}
} catch (CircleNotFoundException $e) {
$circle->setDisplayName($testDisplayName);
return;
}
$i++;
}
}
/**
* @param Circle $circle
*
* @throws RequestBuilderException
*/
public function generateSanitizedName(Circle $circle) {
$baseSanitizedName = $this->sanitizeName($circle->getName());
if ($baseSanitizedName === '') {
$baseSanitizedName = substr($circle->getSingleId(), 0, 3);
}
$i = 1;
while (true) {
$testSanitizedName = $baseSanitizedName . (($i > 1) ? ' (' . $i . ')' : '');
$test = new Circle();
$test->setSanitizedName($testSanitizedName);
try {
$stored = $this->circleRequest->searchCircle($test);
if ($stored->getSingleId() === $circle->getSingleId()) {
throw new CircleNotFoundException();
}
} catch (CircleNotFoundException $e) {
$circle->setSanitizedName($testSanitizedName);
return;
}
$i++;
}
}
/**
* @param string $name
*
* @return string
*/
public function sanitizeName(string $name): string {
// replace '/' with '-' to prevent directory traversal
// replacing instead of stripping seems the better tradeoff here
$sanitized = str_replace('/', '-', $name);
// remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
// see also \OC\Files\Storage\Common::verifyPosixPath(...)
/** @noinspection CascadeStringReplacementInspection */
$sanitized = str_replace(['*', '|', '\\', ':', '"', '<', '>', '?'], '', $sanitized);
// remove leading+trailing spaces and dots to prevent hidden files
return trim($sanitized, ' .');
}
/**
* @param Circle $circle
*
* @throws MembersLimitException
*/
public function confirmCircleNotFull(Circle $circle): void {
if ($this->isCircleFull($circle)) {
throw new MembersLimitException(StatusCode::$MEMBER_ADD[121], 121);
}
}
/**
* @param Circle $circle
*
* @return bool
* @throws RequestBuilderException
*/
public function isCircleFull(Circle $circle): bool {
$filterMember = new Member();
$filterMember->setLevel(Member::LEVEL_MEMBER);
$probe = new MemberProbe();
$probe->setFilterMember($filterMember);
$members = $this->memberRequest->getMembers($circle->getSingleId(), null, $probe);
$limit = $this->getInt('members_limit', $circle->getSettings());
if ($limit === 0) {
$limit = $this->configService->getAppValueInt(ConfigService::MEMBERS_LIMIT);
}
if ($limit === -1) {
return false;
}
return (sizeof($members) >= $limit);
}
/**
* @param string $name
*
* @return string
*/
public function cleanCircleName(string $name): string {
$name = preg_replace('/\s+/', ' ', $name);
return trim($name);
}
/**
* @param IEntity $entity
*
* @return string
*/
public function getDefinition(IEntity $entity): string {
if ($entity instanceof Circle) {
return $this->getDefinitionCircle($entity);
}
if ($entity instanceof IFederatedUser) {
return $this->getDefinitionUser($entity);
}
return '';
}
/**
* @param Circle $circle
*
* @return string
*/
public function getDefinitionCircle(Circle $circle): string {
$source = Circle::$DEF_SOURCE[$circle->getSource()];
if ($circle->isConfig(Circle::CFG_NO_OWNER)
|| $circle->isConfig(Circle::CFG_SINGLE)) {
return $this->l10n->t('%s', [$source]);
}
if ($circle->isConfig(Circle::CFG_PERSONAL)) {
return $this->l10n->t('Personal Circle');
}
if ($circle->hasOwner()) {
return $this->l10n->t(
'%s owned by %s',
[
$source,
$this->configService->displayFederatedUser($circle->getOwner(), true)
]
);
}
return $source;
}
/**
* @param IFederatedUser $federatedUser
*
* @return string
*/
public function getDefinitionUser(IFederatedUser $federatedUser): string {
return $this->l10n->t('%s', [Circle::$DEF_SOURCE[$federatedUser->getUserType()]]);
}
private function generateGetCirclesCacheKey(FederatedUser $federatedUser, string $probeSum): string {
return $federatedUser->getSingleId() . '#' . $probeSum;
}
/**
* @param string $circleId
* @param CircleProbe $circleProbe
* @param DataProbe|null $dataProbe
*
* @return Circle
* @throws InitiatorNotFoundException
* @throws RequestBuilderException
* @throws CircleNotFoundException
*/
public function probeCircle(
string $circleId,
?CircleProbe $circleProbe = null,
?DataProbe $dataProbe = null
): Circle {
$this->federatedUserService->mustHaveCurrentUser();
if (is_null($circleProbe)) {
$circleProbe = new CircleProbe();
$circleProbe->includeSystemCircles();
}
if (is_null($dataProbe)) {
$dataProbe = new DataProbe();
}
return $this->circleRequest->probeCircle(
$circleId,
$this->federatedUserService->getCurrentUser(),
$circleProbe,
$dataProbe
);
}
/**
* @param CircleProbe $circleProbe
* @param DataProbe|null $dataProbe
*
* @return Circle[]
* @throws InitiatorNotFoundException
* @throws RequestBuilderException
*/
public function probeCircles(CircleProbe $circleProbe, ?DataProbe $dataProbe = null): array {
$this->federatedUserService->mustHaveCurrentUser();
if (is_null($dataProbe)) {
$dataProbe = new DataProbe();
}
return $this->circleRequest->probeCircles(
$this->federatedUserService->getCurrentUser(),
$circleProbe,
$dataProbe
);
}
}
@@ -0,0 +1,584 @@
<?php
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
* @author Daniel Tygel <dtygel@eita.org.br>
*
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\Tools\Traits\TArrayTools;
use Exception;
use OC;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\CircleProviderRequest;
use OCA\Circles\Db\DeprecatedCirclesRequest;
use OCA\Circles\Db\DeprecatedMembersRequest;
use OCA\Circles\Db\FederatedLinksRequest;
use OCA\Circles\Db\FileSharesRequest;
use OCA\Circles\Db\TokensRequest;
use OCA\Circles\Exceptions\CircleAlreadyExistsException;
use OCA\Circles\Exceptions\CircleDoesNotExistException;
use OCA\Circles\Exceptions\CircleTypeDisabledException;
use OCA\Circles\Exceptions\FederatedCircleNotAllowedException;
use OCA\Circles\Exceptions\GSStatusException;
use OCA\Circles\Exceptions\MemberIsNotOwnerException;
use OCA\Circles\Exceptions\MembersLimitException;
use OCA\Circles\Model\DeprecatedCircle;
use OCA\Circles\Model\DeprecatedMember;
use OCA\Circles\Model\GlobalScale\GSEvent;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUserSession;
/**
* Class CirclesService
*
* @deprecated
* @package OCA\Circles\Service
*/
class CirclesService {
use TArrayTools;
/** @var string */
private $userId;
/** @var IL10N */
private $l10n;
/** @var IGroupManager */
private $groupManager;
/** @var MembersService */
private $membersService;
/** @var ConfigService */
private $configService;
/** @var DeprecatedCirclesRequest */
private $circlesRequest;
/** @var DeprecatedMembersRequest */
private $membersRequest;
/** @var TokensRequest */
private $tokensRequest;
/** @var FileSharesRequest */
private $fileSharesRequest;
/** @var FederatedLinksRequest */
private $federatedLinksRequest;
/** @var GSUpstreamService */
private $gsUpstreamService;
/** @var EventsService */
private $eventsService;
/** @var CircleProviderRequest */
private $circleProviderRequest;
/** @var MiscService */
private $miscService;
/**
* CirclesService constructor.
*
* @param string $userId
* @param IL10N $l10n
* @param IUserSession $userSession
* @param IGroupManager $groupManager
* @param MembersService $membersService
* @param ConfigService $configService
* @param DeprecatedCirclesRequest $circlesRequest
* @param DeprecatedMembersRequest $membersRequest
* @param TokensRequest $tokensRequest
* @param FileSharesRequest $fileSharesRequest
* @param FederatedLinksRequest $federatedLinksRequest
* @param GSUpstreamService $gsUpstreamService
* @param EventsService $eventsService
* @param CircleProviderRequest $circleProviderRequest
* @param MiscService $miscService
*/
public function __construct(
$userId,
IL10N $l10n,
IUserSession $userSession,
IGroupManager $groupManager,
MembersService $membersService,
ConfigService $configService,
DeprecatedCirclesRequest $circlesRequest,
DeprecatedMembersRequest $membersRequest,
TokensRequest $tokensRequest,
FileSharesRequest $fileSharesRequest,
FederatedLinksRequest $federatedLinksRequest,
GSUpstreamService $gsUpstreamService,
EventsService $eventsService,
CircleProviderRequest $circleProviderRequest,
MiscService $miscService
) {
if ($userId === null) {
$user = $userSession->getUser();
if ($user !== null) {
$userId = $user->getUID();
}
}
$this->userId = $userId;
$this->l10n = $l10n;
$this->groupManager = $groupManager;
$this->membersService = $membersService;
$this->configService = $configService;
$this->circlesRequest = $circlesRequest;
$this->membersRequest = $membersRequest;
$this->tokensRequest = $tokensRequest;
$this->fileSharesRequest = $fileSharesRequest;
$this->federatedLinksRequest = $federatedLinksRequest;
$this->gsUpstreamService = $gsUpstreamService;
$this->eventsService = $eventsService;
$this->circleProviderRequest = $circleProviderRequest;
$this->miscService = $miscService;
}
/**
* Create circle using this->userId as owner
*
* @param int|string $type
* @param string $name
*
* @param string $ownerId
*
* @return DeprecatedCircle
* @throws CircleAlreadyExistsException
* @throws CircleTypeDisabledException
* @throws Exception
*/
public function createCircle($type, $name, string $ownerId = '') {
$type = $this->convertTypeStringToBitValue($type);
$type = (int)$type;
if ($type === '') {
throw new CircleTypeDisabledException(
$this->l10n->t('You need a specify a type of circle')
);
}
// if (!$this->configService->isCircleAllowed($type)) {
// throw new CircleTypeDisabledException(
// $this->l10n->t('You cannot create this type of circle')
// );
// }
$circle = new DeprecatedCircle($type, $name);
if ($ownerId === '') {
$ownerId = $this->userId;
}
if (!$this->circlesRequest->isCircleUnique($circle, $ownerId)) {
throw new CircleAlreadyExistsException(
$this->l10n->t('A circle with that name exists')
);
}
$circle->generateUniqueId();
$owner = new DeprecatedMember($ownerId, DeprecatedMember::TYPE_USER);
$owner->setCircleId($circle->getUniqueId())
->setLevel(DeprecatedMember::LEVEL_OWNER)
->setStatus(DeprecatedMember::STATUS_MEMBER);
$this->membersService->updateCachedName($owner);
$circle->setOwner($owner)
->setViewer($owner);
$event = new GSEvent(GSEvent::CIRCLE_CREATE, true);
$event->setDeprecatedCircle($circle);
$this->gsUpstreamService->newEvent($event);
return $circle;
}
/**
* list Circles depends on type (or all) and name (parts) and minimum level.
*
* @param string $userId
* @param mixed $type
* @param string $name
* @param int $level
*
* @param bool $forceAll
*
* @return DeprecatedCircle[]
* @throws CircleTypeDisabledException
* @throws Exception
*/
public function listCircles($userId, $type, $name = '', $level = 0, $forceAll = false) {
$type = $this->convertTypeStringToBitValue($type);
if ($userId === '') {
throw new Exception('UserID cannot be null');
}
// if (!$this->configService->isCircleAllowed((int)$type)) {
// throw new CircleTypeDisabledException(
// $this->l10n->t('You cannot display this type of circle')
// );
// }
$data = [];
$result = $this->circlesRequest->getCircles($userId, $type, $name, $level, $forceAll);
foreach ($result as $item) {
$data[] = $item;
}
return $data;
}
/**
* returns details on circle and its members if this->userId is a member itself.
*
* @param string $circleUniqueId
* @param bool $forceAll
*
* @return DeprecatedCircle
* @throws Exception
*/
public function detailsCircle($circleUniqueId, $forceAll = false) {
try {
if (!$forceAll) {
$circle = $this->circlesRequest->getCircle(
$circleUniqueId, $this->userId, Member::TYPE_USER, '', $forceAll
);
} else {
$circle = $this->circlesRequest->getCircleFromUniqueId($circleUniqueId);
}
if ($forceAll === true || $this->viewerIsAdmin()
|| $circle->getHigherViewer()
->isLevel(Member::LEVEL_MEMBER)
) {
$this->detailsCircleMembers($circle, $forceAll);
$this->detailsCircleLinkedGroups($circle);
$this->detailsCircleFederatedCircles($circle);
}
} catch (Exception $e) {
throw $e;
}
return $circle;
}
/**
* get the Members list and add the result to the Circle.
*
* @param DeprecatedCircle $circle
*
* @throws Exception
*/
private function detailsCircleMembers(Circle $circle, $forceAll = false) {
if ($forceAll || $this->viewerIsAdmin()) {
$members = $this->membersRequest->forceGetMembers($circle->getUniqueId(), 0);
} else {
$members = $this->membersRequest->getMembers(
$circle->getUniqueId(), $circle->getHigherViewer()
);
}
$circle->setMembers($members);
}
/**
* // TODO - check this on GS setup
* get the Linked Group list and add the result to the Circle.
*
* @param DeprecatedCircle $circle
*
* @throws GSStatusException
*/
private function detailsCircleLinkedGroups(DeprecatedCircle $circle) {
// $groups = [];
// if ($this->configService->isLinkedGroupsAllowed()) {
// $groups =
// $this->membersRequest->getGroupsFromCircle(
// $circle->getUniqueId(), $circle->getHigherViewer()
// );
// }
//
// $circle->setGroups($groups);
}
/**
* get the Federated Circles list and add the result to the Circle.
*
* @param DeprecatedCircle $circle
*/
private function detailsCircleFederatedCircles(DeprecatedCircle $circle) {
$links = [];
try {
if ($this->configService->isFederatedCirclesAllowed()) {
$circle->hasToBeFederated();
$links = $this->federatedLinksRequest->getLinksFromCircle($circle->getUniqueId());
}
} catch (FederatedCircleNotAllowedException $e) {
}
$circle->setLinks($links);
}
/**
* @param DeprecatedCircle $circle
*/
public function updatePasswordOnShares(DeprecatedCircle $circle) {
$this->tokensRequest->updateSinglePassword($circle->getUniqueId(), $circle->getPasswordSingle());
}
/**
* Join a circle.
*
* @param string $circleUniqueId
*
* @return null|DeprecatedMember
* @throws Exception
*/
public function joinCircle($circleUniqueId): DeprecatedMember {
try {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
$member = $this->membersRequest->getFreshNewMember(
$circleUniqueId, $this->userId, DeprecatedMember::TYPE_USER, ''
);
$this->membersService->updateCachedName($member);
$event = new GSEvent(GSEvent::MEMBER_JOIN);
$event->setDeprecatedCircle($circle);
$event->setMember($member);
$this->gsUpstreamService->newEvent($event);
} catch (Exception $e) {
throw $e;
}
return $member;
}
/**
* Leave a circle.
*
* @param string $circleUniqueId
*
* @return null|DeprecatedMember
* @throws Exception
*/
public function leaveCircle($circleUniqueId) {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
$member = $circle->getViewer();
$event = new GSEvent(GSEvent::MEMBER_LEAVE);
$event->setDeprecatedCircle($circle);
$event->setMember($member);
$this->gsUpstreamService->newEvent($event);
return $member;
}
/**
* @param $circleName
*
* @return DeprecatedCircle|null
* @throws CircleDoesNotExistException
*/
public function infoCircleByName($circleName) {
return $this->circlesRequest->forceGetCircleByName($circleName);
}
/**
* Convert a Type in String to its Bit Value
*
* @param string $type
*
* @return int|mixed
*/
public function convertTypeStringToBitValue($type) {
$strings = [
'personal' => DeprecatedCircle::CIRCLES_PERSONAL,
'secret' => DeprecatedCircle::CIRCLES_SECRET,
'closed' => DeprecatedCircle::CIRCLES_CLOSED,
'public' => DeprecatedCircle::CIRCLES_PUBLIC,
'all' => DeprecatedCircle::CIRCLES_ALL
];
if (!key_exists(strtolower($type), $strings)) {
return $type;
}
return $strings[strtolower($type)];
}
/**
* getCircleIcon()
*
* Return the right imagePath for a type of circle.
*
* @param string $type
* @param bool $png
*
* @return string
*/
public static function getCircleIcon($type, $png = false) {
$ext = '.svg';
if ($png === true) {
$ext = '.png';
}
$urlGen = OC::$server->getURLGenerator();
switch ($type) {
case DeprecatedCircle::CIRCLES_PERSONAL:
return $urlGen->getAbsoluteURL(
$urlGen->imagePath(Application::APP_ID, 'personal' . $ext)
);
case DeprecatedCircle::CIRCLES_CLOSED:
return $urlGen->getAbsoluteURL(
$urlGen->imagePath(Application::APP_ID, 'closed' . $ext)
);
case DeprecatedCircle::CIRCLES_SECRET:
return $urlGen->getAbsoluteURL(
$urlGen->imagePath(Application::APP_ID, 'secret' . $ext)
);
case DeprecatedCircle::CIRCLES_PUBLIC:
return $urlGen->getAbsoluteURL(
$urlGen->imagePath(Application::APP_ID, 'circles' . $ext)
);
}
return $urlGen->getAbsoluteURL(
$urlGen->imagePath(Application::APP_ID, 'circles' . $ext)
);
}
/**
* @param string $circleUniqueIds
* @param int $limit
* @param int $offset
*
* @return array
* @throws GSStatusException
*/
public function getFilesForCircles($circleUniqueIds, $limit = -1, $offset = 0) {
if (!is_array($circleUniqueIds)) {
$circleUniqueIds = [$circleUniqueIds];
}
return $this->circleProviderRequest->getFilesForCircles(
$this->userId, $circleUniqueIds, $limit, $offset
);
}
/**
* @param DeprecatedCircle $circle
*
* @throws MembersLimitException
*/
public function checkThatCircleIsNotFull(DeprecatedCircle $circle) {
$members =
$this->membersRequest->forceGetMembers(
$circle->getUniqueId(), DeprecatedMember::LEVEL_MEMBER, 0, true
);
$limit = (int)$circle->getSetting('members_limit');
if ($limit === -1) {
return;
}
if ($limit === 0) {
$limit = $this->configService->getAppValue(ConfigService::MEMBERS_LIMIT);
}
if (sizeof($members) >= $limit) {
throw new MembersLimitException(
'This circle already reach its limit on the number of members'
);
}
}
/**
* @return bool
*/
public function viewerIsAdmin(): bool {
if ($this->userId === '') {
return false;
}
return ($this->groupManager->isAdmin($this->userId));
}
/**
* should be moved.
*
* @param DeprecatedMember $member
*
* @throws MemberIsNotOwnerException
*/
public function hasToBeOwner(DeprecatedMember $member) {
if (!$this->groupManager->isAdmin($this->userId)
&& $member->getLevel() < DeprecatedMember::LEVEL_OWNER) {
throw new MemberIsNotOwnerException(
$this->l10n->t('This member is not the owner of the circle')
);
}
}
/**
* should be moved.
*
* @param DeprecatedMember $member
*
* @throws MemberIsNotOwnerException
*/
public function hasToBeAdmin(DeprecatedMember $member) {
if (!$this->groupManager->isAdmin($member->getUserId())
&& $member->getLevel() < DeprecatedMember::LEVEL_ADMIN) {
throw new MemberIsNotOwnerException(
$this->l10n->t('This member is not an admin of the circle')
);
}
}
}
@@ -0,0 +1,800 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OC;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Exceptions\GSStatusException;
use OCA\Circles\IFederatedUser;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Member;
use OCA\Circles\Tools\Model\NCRequest;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\IConfig;
use OCP\IURLGenerator;
/**
* Class ConfigService
*
* @package OCA\Circles\Service
*/
class ConfigService {
use TStringTools;
use TArrayTools;
use TNCLogger;
public const FRONTAL_CLOUD_BASE = 'frontal_cloud_base';
public const FRONTAL_CLOUD_ID = 'frontal_cloud_id';
public const FRONTAL_CLOUD_SCHEME = 'frontal_cloud_scheme';
public const FRONTAL_CLOUD_PATH = 'frontal_cloud_path';
public const INTERNAL_CLOUD_ID = 'internal_cloud_id';
public const INTERNAL_CLOUD_SCHEME = 'internal_cloud_scheme';
public const INTERNAL_CLOUD_PATH = 'internal_cloud_path';
public const LOOPBACK_CLOUD_ID = 'loopback_cloud_id';
public const LOOPBACK_CLOUD_SCHEME = 'loopback_cloud_scheme';
public const LOOPBACK_CLOUD_PATH = 'loopback_cloud_path';
public const IFACE0_CLOUD_ID = 'iface0_cloud_id';
public const IFACE0_CLOUD_SCHEME = 'iface0_cloud_scheme';
public const IFACE0_CLOUD_PATH = 'iface0_cloud_path';
public const IFACE0_INTERNAL = 'iface0_internal';
public const IFACE1_CLOUD_ID = 'iface1_cloud_id';
public const IFACE1_CLOUD_SCHEME = 'iface1_cloud_scheme';
public const IFACE1_CLOUD_PATH = 'iface1_cloud_path';
public const IFACE1_INTERNAL = 'iface1_internal';
public const IFACE2_CLOUD_ID = 'iface2_cloud_id';
public const IFACE2_CLOUD_SCHEME = 'iface2_cloud_scheme';
public const IFACE2_CLOUD_PATH = 'iface2_cloud_path';
public const IFACE2_INTERNAL = 'iface2_internal';
public const IFACE3_CLOUD_ID = 'iface3_cloud_id';
public const IFACE3_CLOUD_SCHEME = 'iface3_cloud_scheme';
public const IFACE3_CLOUD_PATH = 'iface3_cloud_path';
public const IFACE3_INTERNAL = 'iface3_internal';
public const IFACE4_CLOUD_ID = 'iface4_cloud_id';
public const IFACE4_CLOUD_SCHEME = 'iface4_cloud_scheme';
public const IFACE4_CLOUD_PATH = 'iface4_cloud_path';
public const IFACE4_INTERNAL = 'iface4_internal';
public const IFACE_TEST_ID = 'iface_test_id';
public const IFACE_TEST_SCHEME = 'iface_test_scheme';
public const IFACE_TEST_PATH = 'iface_test_path';
public const IFACE_TEST_TOKEN = 'iface_test_token';
public const LOOPBACK_TMP_ID = 'loopback_tmp_id';
public const LOOPBACK_TMP_SCHEME = 'loopback_tmp_scheme';
public const LOOPBACK_TMP_PATH = 'loopback_tmp_path';
public const HARD_MODERATION = 'hard_moderation';
public const FRONTEND_ENABLED = 'frontend_enabled';
public const KEYHOLE_CFG_REQUEST = 'keyhole_cfg_request';
public const ROUTE_TO_CIRCLE = 'route_to_circle';
public const EVENT_EXAMPLES = 'event_examples';
public const ENFORCE_PASSWORD = 'enforce_password';
public const SELF_SIGNED_CERT = 'self_signed_cert';
public const MEMBERS_LIMIT = 'members_limit';
public const ACTIVITY_ON_NEW_CIRCLE = 'creation_activity';
public const ALLOWED_TYPES = 'allowed_types';
public const CIRCLE_TYPES_FORCE = 'circle_types_force';
public const CIRCLE_TYPES_BLOCK = 'circle_types_block';
public const BYPASS_CIRCLE_TYPES = 'bypass_circle_types';
public const LIMIT_CIRCLE_CREATION = 'limit_circle_creation';
public const MIGRATION_BYPASS = 'migration_bypass';
public const MIGRATION_22 = 'migration_22';
public const MIGRATION_22_1 = 'migration_22_1';
public const MIGRATION_22_CONFIRMED = 'migration_22_confirmed';
public const MIGRATION_RUN = 'migration_run';
public const MAINTENANCE_UPDATE = 'maintenance_update';
public const MAINTENANCE_RUN = 'maintenance_run';
public const GS_MODE = 'mode';
public const GS_KEY = 'key';
public const GS_LOOKUP_INSTANCES = '/instances';
public const GS_LOOKUP_USERS = '/users';
// deprecated -- removing in NC25
public const CIRCLES_CONTACT_BACKEND = 'contact_backend';
public const CIRCLES_ACCOUNTS_ONLY = 'accounts_only'; // only UserType=1
public const CIRCLES_SEARCH_FROM_COLLABORATOR = 'search_from_collaborator';
public const FORCE_NC_BASE = 'force_nc_base';
public const TEST_NC_BASE = 'test_nc_base';
private static $defaults = [
self::FRONTAL_CLOUD_BASE => '',
self::FRONTAL_CLOUD_ID => '',
self::FRONTAL_CLOUD_SCHEME => 'https',
self::FRONTAL_CLOUD_PATH => '',
self::INTERNAL_CLOUD_ID => '',
self::INTERNAL_CLOUD_SCHEME => 'https',
self::INTERNAL_CLOUD_PATH => '',
self::LOOPBACK_CLOUD_ID => '',
self::LOOPBACK_CLOUD_SCHEME => 'https',
self::LOOPBACK_CLOUD_PATH => '',
self::IFACE0_CLOUD_ID => '',
self::IFACE0_CLOUD_SCHEME => 'https',
self::IFACE0_CLOUD_PATH => '',
self::IFACE0_INTERNAL => '0',
self::IFACE1_CLOUD_ID => '',
self::IFACE1_CLOUD_SCHEME => 'https',
self::IFACE1_CLOUD_PATH => '',
self::IFACE1_INTERNAL => '0',
self::IFACE2_CLOUD_ID => '',
self::IFACE2_CLOUD_SCHEME => 'https',
self::IFACE2_CLOUD_PATH => '',
self::IFACE2_INTERNAL => '0',
self::IFACE3_CLOUD_ID => '',
self::IFACE3_CLOUD_SCHEME => 'https',
self::IFACE3_CLOUD_PATH => '',
self::IFACE3_INTERNAL => '0',
self::IFACE4_CLOUD_ID => '',
self::IFACE4_CLOUD_SCHEME => 'https',
self::IFACE4_CLOUD_PATH => '',
self::IFACE4_INTERNAL => '0',
self::IFACE_TEST_ID => '',
self::IFACE_TEST_SCHEME => 'https',
self::IFACE_TEST_PATH => '',
self::IFACE_TEST_TOKEN => '',
self::LOOPBACK_TMP_ID => '',
self::LOOPBACK_TMP_SCHEME => '',
self::LOOPBACK_TMP_PATH => '',
self::FRONTEND_ENABLED => '1',
self::HARD_MODERATION => '0',
self::KEYHOLE_CFG_REQUEST => '0',
self::ROUTE_TO_CIRCLE => 'contacts.contacts.directcircle',
self::EVENT_EXAMPLES => '0',
self::ENFORCE_PASSWORD => '2',
self::SELF_SIGNED_CERT => '0',
self::MEMBERS_LIMIT => '-1',
self::ACTIVITY_ON_NEW_CIRCLE => '1',
self::ALLOWED_TYPES => Member::ALLOWING_ALL_TYPES,
self::CIRCLE_TYPES_FORCE => '0',
self::CIRCLE_TYPES_BLOCK => '0',
self::BYPASS_CIRCLE_TYPES => '',
self::LIMIT_CIRCLE_CREATION => '',
self::MIGRATION_BYPASS => '0',
self::MIGRATION_22 => '0',
self::MIGRATION_22_1 => '0',
self::MIGRATION_22_CONFIRMED => '0',
self::MIGRATION_RUN => '0',
self::MAINTENANCE_UPDATE => '[]',
self::MAINTENANCE_RUN => '0',
self::FORCE_NC_BASE => '',
self::TEST_NC_BASE => '',
self::CIRCLES_CONTACT_BACKEND => '0',
self::CIRCLES_ACCOUNTS_ONLY => '0',
self::CIRCLES_SEARCH_FROM_COLLABORATOR => '0',
];
public const DISPLAY_NONE = 0;
public const DISPLAY_AT = 1;
public const DISPLAY_PARENTHESIS = 2;
/** @var IConfig */
private $config;
/** @var IURLGenerator */
private $urlGenerator;
/**
* ConfigService constructor.
*
* @param IConfig $config
* @param IURLGenerator $urlGenerator
*/
public function __construct(IConfig $config, IURLGenerator $urlGenerator) {
$this->config = $config;
$this->urlGenerator = $urlGenerator;
$this->setup('app', Application::APP_ID);
}
/**
* Get a value by key
*
* @param string $key
*
* @return string
*/
public function getAppValue(string $key): string {
if (($value = $this->config->getAppValue(Application::APP_ID, $key, '')) !== '') {
return $value;
}
if (($value = $this->config->getSystemValue(Application::APP_ID . '.' . $key, '')) !== '') {
return $value;
}
return $this->get($key, self::$defaults);
}
/**
* @param string $key
*
* @return int
*/
public function getAppValueInt(string $key): int {
return (int)$this->getAppValue($key);
}
/**
* @param string $key
*
* @return bool
*/
public function getAppValueBool(string $key): bool {
return ($this->getAppValueInt($key) === 1);
}
/**
* Set a value by key
*
* @param string $key
* @param string $value
*
* @return void
*/
public function setAppValue(string $key, string $value): void {
$this->config->setAppValue(Application::APP_ID, $key, $value);
}
/**
*
*/
public function unsetAppConfig(): void {
$this->config->deleteAppValues(Application::APP_ID);
}
/**
* Get available hosts
*
* @return array
*/
public function getAvailableHosts(): array {
return $this->config->getSystemValue('trusted_domains', []);
}
/**
* Get a user value by key and user
*
*
* @param string $userId
* @param string $key
*
* @param string $default
*
* @return string
*/
public function getCoreValueForUser($userId, $key, $default = '') {
return $this->config->getUserValue($userId, 'core', $key, $default);
}
/**
* @return bool
* @deprecated
*/
public function isContactsBackend(): bool {
return ($this->getAppValue(ConfigService::CIRCLES_CONTACT_BACKEND) !== '0'
&& $this->getAppValue(ConfigService::CIRCLES_CONTACT_BACKEND) !== '');
}
/**
* @return int
* @deprecated
*/
public function contactsBackendType(): int {
return (int)$this->getAppValue(ConfigService::CIRCLES_CONTACT_BACKEND);
}
/**
* true if:
* - password is generated randomly
*
* @param Circle $circle
*
* @return bool
*/
public function sendPasswordByMail(Circle $circle): bool {
if (!$this->enforcePasswordOnSharedFile($circle)) {
return false;
}
return (!$this->getBool('password_single_enabled', $circle->getSettings(), false)
|| $this->get('password_single', $circle->getSettings()) === '');
}
/**
* true if:
* - password enforced for Circle
* - single password enabled for Circle
* - single password defined within Circle's settings
*
* @param Circle $circle
*
* @return bool
*/
public function isSinglePasswordAvailable(Circle $circle): bool {
if (!$this->enforcePasswordOnSharedFile($circle)) {
return false;
}
return ($this->getBool('password_single_enabled', $circle->getSettings(), false)
&& $this->get('password_single', $circle->getSettings()) !== '');
}
/**
* true if:
* - global setting of Nextcloud enforce password on shares.
* - setting of Circles' app enforce password on shares.
* - setting for specific Circle enforce password on shares.
*
* @param Circle $circle
*
* @return bool
*/
public function enforcePasswordOnSharedFile(Circle $circle): bool {
if ($this->config->getAppValue(
'core',
'shareapi_enforce_links_password',
'no'
) === 'yes') {
return true;
}
if ($this->getAppValueInt(ConfigService::ENFORCE_PASSWORD) === 1) {
return true;
}
// Compat NC21
if ($this->getBool('password_enforcement', $circle->getSettings(), false)) {
return true;
}
return $this->getBool('enforce_password', $circle->getSettings(), false);
}
/**
* // TODO: fetch data from somewhere else than hard coded...
*
* @return array
*/
public function getSettings(): array {
return [
'frontendEnabled' => $this->getAppValueBool(self::FRONTEND_ENABLED),
'allowedCircles' => Circle::$DEF_CFG_MAX,
'allowedUserTypes' => Member::$DEF_TYPE_MAX,
'membersLimit' => $this->getAppValueInt(self::MEMBERS_LIMIT)
];
}
/**
* @return bool
*/
public function isGSAvailable(): bool {
if (!empty($this->getGSSMockup())) {
return true;
}
return $this->config->getSystemValueBool('gs.enabled', false);
}
/**
* @return string
* @throws GSStatusException
*/
public function getGSLookup(): string {
$lookup = $this->config->getSystemValue('lookup_server', '');
if (!$this->isGSAvailable() || $lookup === '') {
throw new GSStatusException();
}
return $lookup;
}
/**
* @return array
*/
public function getGSSMockup(): array {
return $this->config->getSystemValue('gss.mockup', []);
}
/**
* @param string $type
*
* @return string
*/
public function getGSInfo(string $type): string {
$clef = $this->config->getSystemValue('gss.jwt.key', '');
$mode = $this->config->getSystemValue('gss.mode', '');
switch ($type) {
case self::GS_MODE:
return $mode;
case self::GS_KEY:
return $clef;
}
return '';
}
/**
* @return array
* @throws GSStatusException
*/
public function getGSData(): array {
return [
'enabled' => $this->isGSAvailable(),
'lookup' => $this->getGSLookup(),
'mockup' => $this->getGSSMockup(),
self::GS_MODE => $this->config->getSystemValue('gss.mode', ''),
self::GS_KEY => $this->config->getSystemValue('gss.jwt.key', ''),
];
}
/**
* @return array
*/
public function getTrustedDomains(): array {
return array_map(
function (string $address) {
return strtolower($address);
}, $this->config->getSystemValue('trusted_domains', [])
);
}
/**
* @return string
*/
public function getLoopbackInstance(): string {
$loopbackCloudId = $this->getAppValue(self::LOOPBACK_TMP_ID);
if ($loopbackCloudId !== '') {
return $loopbackCloudId;
}
$loopbackCloudId = $this->getAppValue(self::LOOPBACK_CLOUD_ID);
if ($loopbackCloudId !== '') {
return $loopbackCloudId;
}
$cliUrl = $this->getAppValue(self::FORCE_NC_BASE);
if ($cliUrl === '') {
$cliUrl = $this->config->getSystemValue('circles.force_nc_base', '');
}
if ($cliUrl === '') {
$cliUrl = $this->config->getSystemValue('overwrite.cli.url', '');
}
$loopback = parse_url($cliUrl);
if (!is_array($loopback) || !array_key_exists('host', $loopback)) {
return $cliUrl;
}
if (array_key_exists('port', $loopback)) {
$loopbackCloudId = $loopback['host'] . ':' . $loopback['port'];
} else {
$loopbackCloudId = $loopback['host'];
}
if (array_key_exists('scheme', $loopback)
&& $this->getAppValue(self::LOOPBACK_TMP_SCHEME) !== $loopback['scheme']) {
$this->setAppValue(self::LOOPBACK_TMP_SCHEME, $loopback['scheme']);
}
if (array_key_exists('path', $loopback)
&& $this->getAppValue(self::LOOPBACK_TMP_PATH) !== $loopback['path']) {
$this->setAppValue(self::LOOPBACK_TMP_PATH, $loopback['path']);
}
return $loopbackCloudId;
}
/**
* returns loopback address based on getLoopbackInstance and LOOPBACK_CLOUD_SCHEME
* should be used to async process
*
* @param string $route
* @param array $args
*
* @return string
*/
public function getLoopbackPath(string $route = '', array $args = []): string {
$instance = $this->getLoopbackInstance();
$scheme = $this->getAppValue(self::LOOPBACK_TMP_SCHEME);
if ($scheme === '') {
$scheme = $this->getAppValue(self::LOOPBACK_CLOUD_SCHEME);
}
$path = $this->getAppValue(self::LOOPBACK_TMP_PATH);
if ($path === '') {
$path = $this->getAppValue(self::LOOPBACK_CLOUD_PATH);
}
$base = $scheme . '://' . $instance . $path;
if ($route === '') {
return $base;
}
return rtrim($base, '/') . $this->linkToRoute($route, $args);
}
/**
* - must be configured using INTERNAL_CLOUD_ID
* - returns host+port, does not specify any protocol
* - used mainly to assign instance and source to a request to local GlobalScale
* - important only in GlobalScale environment
*
* @return string
*
*/
public function getInternalInstance(): string {
return $this->getAppValue(self::INTERNAL_CLOUD_ID);
}
/**
* - must be configured using FRONTAL_CLOUD_ID
* - returns host+port, does not specify any protocol
* - used mainly to assign instance and source to a request
* - important only in remote environment
*
* @return string
*/
public function getFrontalInstance(): string {
$frontalCloudId = $this->getAppValue(self::FRONTAL_CLOUD_ID);
// using old settings local_cloud_id from NC20, deprecated in NC25
if ($frontalCloudId === '') {
$frontalCloudId = $this->config->getAppValue(Application::APP_ID, 'local_cloud_id', '');
if ($frontalCloudId !== '') {
$this->setAppValue(self::FRONTAL_CLOUD_ID, $frontalCloudId);
}
}
return $frontalCloudId;
}
/**
* @param int $iface
*
* @return string
*/
public function getIfaceInstance(int $iface): string {
switch ($iface) {
case InterfaceService::IFACE0:
return $this->getAppValue(self::IFACE0_CLOUD_ID);
case InterfaceService::IFACE1:
return $this->getAppValue(self::IFACE1_CLOUD_ID);
case InterfaceService::IFACE2:
return $this->getAppValue(self::IFACE2_CLOUD_ID);
case InterfaceService::IFACE3:
return $this->getAppValue(self::IFACE3_CLOUD_ID);
case InterfaceService::IFACE4:
return $this->getAppValue(self::IFACE4_CLOUD_ID);
}
return '';
}
/**
* @param string $instance
*
* @return bool
*/
public function isLocalInstance(string $instance): bool {
if ($instance === '') {
return true;
}
$instance = strtolower($instance);
if ($instance === strtolower($this->getInternalInstance())) {
return true;
}
if ($instance === strtolower($this->getFrontalInstance())) {
return true;
}
if ($instance === strtolower($this->getLoopbackInstance())) {
return true;
}
return (in_array($instance, $this->getTrustedDomains()));
}
/**
* @param IFederatedUser $federatedUser
* @param bool $displayName
* @param bool $displayInstance
*
* @return string
*/
public function displayFederatedUser(
IFederatedUser $federatedUser,
bool $displayName = false,
bool $displayInstance = true
): string {
$name = ($displayName) ? $federatedUser->getDisplayName() : $federatedUser->getUserId();
if ($federatedUser->getUserType() === Member::TYPE_MAIL) {
return $name . ' ' . $this->displayInstance(
$federatedUser->getInstance(),
self::DISPLAY_PARENTHESIS
);
}
if (!$displayInstance) {
return $name;
}
return $name . $this->displayInstance($federatedUser->getInstance(), self::DISPLAY_AT);
}
/**
* @param string $instance
* @param int $type
*
* @return string
*/
public function displayInstance(string $instance, int $type = self::DISPLAY_NONE): string {
if ($this->isLocalInstance($instance)) {
return '';
}
switch ($type) {
case self::DISPLAY_AT:
return '@' . $instance;
case self::DISPLAY_PARENTHESIS:
return '(' . $instance . ')';
}
return $instance;
}
/**
* - Create route using getLoopbackAddress()
* - perfect for loopback request.
*
* @param NCRequest $request
* @param string $route
* @param array $args
*/
public function configureLoopbackRequest(
NCRequest $request,
string $route = '',
array $args = []
): void {
$this->configureRequest($request);
$request->setVerifyPeer(false);
$request->basedOnUrl($this->getLoopbackPath($route, $args));
}
/**
* @param NCRequest $request
*/
public function configureRequest(NCRequest $request): void {
$request->setVerifyPeer($this->getAppValue(ConfigService::SELF_SIGNED_CERT) !== '1');
$request->setProtocols(['https', 'http']);
$request->setHttpErrorsAllowed(true);
$request->setLocalAddressAllowed(true);
$request->setFollowLocation(true);
$request->setTimeout(5);
}
/**
* @param string $route
* @param array $args
*
* @return string
*/
public function linkToRoute(string $route, array $args): string {
$path = $this->urlGenerator->linkToRoute($route, $args);
if (OC::$CLI) {
// when running from the occ command, /index.php is not removed by itself
if (str_starts_with($path, '/index.php/')
&& $this->config->getSystemValueString('htaccess.RewriteBase', '') !== '') {
$path = $this->config->getSystemValueString('htaccess.RewriteBase', '/') . substr($path, 11);
}
$knownPath = parse_url($this->config->getSystemValue('overwrite.cli.url'), PHP_URL_PATH);
} else {
$knownPath = OC::$WEBROOT;
}
$knownPath = $knownPath ? rtrim($knownPath, '/') : '';
if ($knownPath === '') {
return $path;
}
$pos = strpos($path, $knownPath);
if ($pos === 0) {
return substr($path, strlen($knownPath));
}
return $path;
}
/**
* Enforce or Block circle's config/type
*
* @param Circle $circle
*/
public function confirmAllowedCircleTypes(Circle $circle): void {
$config = $circle->getConfig();
$config |= $this->getAppValueInt(ConfigService::CIRCLE_TYPES_FORCE);
$config &= ~$this->getAppValueInt(ConfigService::CIRCLE_TYPES_BLOCK);
$circle->setConfig($config);
}
}
@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCA\Circles\Tools\Traits\TStringTools;
use Exception;
use OC;
use OCA\Circles\Exceptions\ContactAddressBookNotFoundException;
use OCA\Circles\Exceptions\ContactFormatException;
use OCA\Circles\Exceptions\ContactNotFoundException;
use OCA\Circles\Model\Member;
use OCA\DAV\CardDAV\ContactsManager;
use OCP\Contacts\IManager;
use OCP\IAddressBook;
use OCP\IURLGenerator;
/**
* Class ContactService
*
* @package OCA\Circles\Service
*/
class ContactService {
use TArrayTools;
use TStringTools;
use TNCLogger;
/** @var IURLGenerator */
private $urlGenerator;
/** @var ConfigService */
private $configService;
/**
* ContactService constructor.
*
* @param IURLGenerator $urlGenerator
* @param ConfigService $configService
*/
public function __construct(IURLGenerator $urlGenerator, ConfigService $configService) {
$this->urlGenerator = $urlGenerator;
$this->configService = $configService;
}
/**
* @param string $contactPath
*
* @return string
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
*/
public function getDisplayName(string $contactPath): string {
$contact = $this->getContact($contactPath);
if ($this->get('FN', $contact) !== '') {
return $this->get('FN', $contact);
}
if ($this->get('EMAIL', $contact) !== '') {
return $this->get('EMAIL', $contact);
}
if (!empty($this->getArray('EMAIL', $contact))) {
return $this->getArray('EMAIL', $contact)[0];
}
// TODO: no idea if this situation might exists or if displaying the full contactPath is safe, so md5()
return md5($contactPath);
}
/**
* @param string $contactPath
*
* @return array
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
*/
public function getMailAddresses(string $contactPath): array {
$c = $this->getContact($contactPath);
return ($this->get('EMAIL', $c) === '') ? [$this->get('EMAIL', $c)] : $this->getArray('EMAIL', $c);
}
/**
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws ContactAddressBookNotFoundException
*/
private function getContact(string $contactPath): array {
[$userId, $addressBookUri, $contactId] = explode('/', $contactPath, 3);
if ($userId === ''
|| $contactId === ''
|| $addressBookUri === ''
|| is_null($contactId)
) {
throw new ContactFormatException('issue with contact format USERID/ADDRESSBOOK/CONTACTID');
}
$contactsManager = OC::$server->get(ContactsManager::class);
$cm = OC::$server->get(IManager::class);
$contactsManager->setupContactsProvider($cm, $userId, $this->urlGenerator);
$addressBook = $this->getAddressBook($cm, $addressBookUri);
$contacts = $addressBook->search(
$contactId, ['UID'],
[
'types' => false,
'escape_like_param' => false
]
);
if (sizeof($contacts) !== 1) {
throw new ContactNotFoundException();
}
return $contacts[0];
}
/**
* @param IManager $cm
* @param string $addressBookUri
*
* @return IAddressBook
* @throws ContactAddressBookNotFoundException
*/
private function getAddressBook(IManager $cm, string $addressBookUri): IAddressBook {
foreach ($cm->getUserAddressBooks() as $addressBook) {
if ($addressBook->getUri() === $addressBookUri) {
return $addressBook;
}
}
throw new ContactAddressBookNotFoundException();
}
/**
* @param IManager $cm
* @param string $addressBookKey
*
* @return IAddressBook
* @throws ContactAddressBookNotFoundException
*/
public function getAddressBoxById(IManager $cm, string $addressBookKey): IAddressBook {
foreach ($cm->getUserAddressBooks() as $addressBook) {
if ($addressBook->getKey() === $addressBookKey) {
return $addressBook;
}
}
throw new ContactAddressBookNotFoundException();
}
/**
* @param Member $member
*
* @return array
*/
public function getMailAddressesFromMember(Member $member): array {
if ($member->getUserType() !== Member::TYPE_CONTACT
|| !$this->configService->isLocalInstance($member->getInstance())) {
return [];
}
try {
return $this->getMailAddresses($member->getUserId());
} catch (Exception $e) {
return [];
}
}
}
@@ -0,0 +1,708 @@
<?php
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2017
* @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\Circles\Service;
use Exception;
use OCA\Circles\Circles\FileSharingBroadcaster;
use OCA\Circles\Db\DeprecatedCirclesRequest;
use OCA\Circles\Db\DeprecatedMembersRequest;
use OCA\Circles\Exceptions\CircleAlreadyExistsException;
use OCA\Circles\Exceptions\CircleDoesNotExistException;
use OCA\Circles\Exceptions\MemberAlreadyExistsException;
use OCA\Circles\Exceptions\MemberDoesNotExistException;
use OCA\Circles\Exceptions\NotLocalMemberException;
use OCA\Circles\Model\DeprecatedCircle;
use OCA\Circles\Model\DavCard;
use OCA\Circles\Model\DeprecatedMember;
use OCA\DAV\CardDAV\CardDavBackend;
use OCP\App\ManagerEvent;
use OCP\Federation\ICloudIdManager;
use OCP\IUserManager;
use Symfony\Component\EventDispatcher\GenericEvent;
/**
* Class DavService
* @deprecated
* @package OCA\Circles\Service
*/
class DavService {
/** @var string */
private $userId;
/** @var IUserManager */
private $userManager;
/** @var CardDavBackend */
private $cardDavBackend;
/** @var ICloudIdManager */
private $cloudManager;
/** @var FileSharingBroadcaster */
private $fileSharingBroadcaster;
/** @var MembersService */
private $membersService;
/** @var DeprecatedCirclesRequest */
private $circlesRequest;
/** @var DeprecatedMembersRequest */
private $membersRequest;
/** @var ConfigService */
private $configService;
/** @var MiscService */
private $miscService;
/** @var array */
private $migratedBooks = [];
/**
* TimezoneService constructor.
*
* @param string $userId
* @param IUserManager $userManager
* @param CardDavBackend $cardDavBackend
* @param ICloudIdManager $cloudManager
* @param FileSharingBroadcaster $fileSharingBroadcaster
* @param MembersService $membersService
* @param DeprecatedCirclesRequest $circlesRequest
* @param DeprecatedMembersRequest $membersRequest
* @param ConfigService $configService
* @param MiscService $miscService
*/
public function __construct(
$userId, IUserManager $userManager, CardDavBackend $cardDavBackend, ICloudIdManager $cloudManager,
FileSharingBroadcaster $fileSharingBroadcaster, MembersService $membersService,
DeprecatedCirclesRequest $circlesRequest, DeprecatedMembersRequest $membersRequest, ConfigService $configService,
MiscService $miscService
) {
$this->userId = $userId;
$this->userManager = $userManager;
$this->cardDavBackend = $cardDavBackend;
$this->cloudManager = $cloudManager;
$this->fileSharingBroadcaster = $fileSharingBroadcaster;
$this->membersService = $membersService;
$this->circlesRequest = $circlesRequest;
$this->membersRequest = $membersRequest;
$this->configService = $configService;
$this->miscService = $miscService;
}
/**
* @param ManagerEvent $event
*/
public function onAppEnabled(ManagerEvent $event) {
if ($event->getAppID() !== 'circles') {
return;
}
try {
// $this->migration();
} catch (Exception $e) {
}
}
/**
* @param GenericEvent $event
*/
public function onCreateCard(GenericEvent $event) {
$davCard = $this->generateDavCard($event);
$this->manageDavCard($davCard);
}
/**
* @param GenericEvent $event
*/
public function onUpdateCard(GenericEvent $event) {
$davCard = $this->generateDavCard($event);
$this->manageDavCard($davCard);
}
/**
* @param GenericEvent $event
*/
public function onDeleteCard(GenericEvent $event) {
$davCard = $this->generateDavCard($event, true);
$this->miscService->log('Deleting Card: ' . json_encode($davCard), 1);
$this->membersRequest->removeMembersByContactId($davCard->getUniqueId(), DeprecatedMember::TYPE_USER);
$this->manageDeprecatedCircles($davCard->getAddressBookId());
$this->manageDeprecatedMembers($davCard);
}
/**
* @param GenericEvent $event
* @param bool $tiny
*
* @return DavCard
*/
private function generateDavCard(GenericEvent $event, bool $tiny = false): DavCard {
$addressBookId = $event->getArgument('addressBookId');
$cardUri = $event->getArgument('cardUri');
$davCard = new DavCard();
$davCard->setAddressBookId($addressBookId);
$davCard->setCardUri($cardUri);
if ($tiny) {
return $davCard;
}
$cardData = $event->getArgument('cardData');
$davCard->setOwner($this->getOwnerFromAddressBook($addressBookId));
$davCard->importFromDav($cardData);
return $davCard;
}
/**
* @param int $bookId
* @param array $card
*
* @return DavCard
*/
private function generateDavCardFromCard(int $bookId, array $card): DavCard {
$this->miscService->log(
'generating DavCard Model from book=' . $bookId . ' from ' . json_encode($card), 0
);
$davCard = new DavCard();
$davCard->setAddressBookId($bookId);
$davCard->setCardUri($card['uri']);
$davCard->setOwner($this->getOwnerFromAddressBook($bookId));
$davCard->importFromDav($card['carddata']);
$this->miscService->log('generated DavCard Model: ' . json_encode($davCard), 0);
return $davCard;
}
/**
* @param DavCard $davCard
*
*/
private function manageDavCard(DavCard $davCard) {
$this->miscService->log('Updating Card: ' . json_encode($davCard), 1);
$this->manageCircles($davCard);
$this->manageContact($davCard);
}
/**
* @param DavCard $davCard
*/
private function manageContact(DavCard $davCard) {
$this->manageDeprecatedMembers($davCard);
switch ($this->getMemberType($davCard)) {
case DavCard::TYPE_CONTACT:
$this->manageRemoteContact($davCard);
break;
case DavCard::TYPE_LOCAL:
$this->manageLocalContact($davCard);
break;
// case DavCard::TYPE_FEDERATED:
// $this->manageFederatedContact($davCard);
// break;
}
}
/**
* @param DavCard $davCard
*/
private function manageDeprecatedMembers(DavCard $davCard) {
$this->miscService->log(
'Managing deprecated circles memberships from DavCard Model: ' . json_encode($davCard), 0
);
$circles = array_map(
function (DeprecatedCircle $circle) {
return $circle->getUniqueId();
}, $davCard->getCircles()
);
$members = $this->membersRequest->getMembersByContactId($davCard->getUniqueId());
$this->miscService->log(
'Found ' . sizeof($members) . ' memberships with contactId=' . $davCard->getUniqueId(), 0
);
foreach ($members as $member) {
if (!in_array($member->getCircleId(), $circles)) {
$this->miscService->log(
'Removing membership ' . $member->getMemberId() . ' from ' . $member->getCircleId(), 0
);
$this->membersRequest->removeMember($member);
}
}
}
/**
* @param DavCard $davCard
*/
private function manageLocalContact(DavCard $davCard) {
foreach ($davCard->getCircles() as $circle) {
$this->manageMember($circle, $davCard, DeprecatedMember::TYPE_USER);
}
}
/**
* @param DavCard $davCard
*/
private function manageRemoteContact(DavCard $davCard) {
foreach ($davCard->getCircles() as $circle) {
$this->manageMember($circle, $davCard, DeprecatedMember::TYPE_CONTACT);
}
}
/**
* @param DeprecatedCircle $circle
* @param DavCard $davCard
* @param int $type
*/
private function manageMember(DeprecatedCircle $circle, DavCard $davCard, int $type) {
try {
$member =
$this->membersRequest->getContactMember($circle->getUniqueId(), $davCard->getUniqueId());
if ($member->getType() !== $type) {
$this->membersRequest->removeMember($member);
throw new MemberDoesNotExistException();
}
} catch (MemberDoesNotExistException $e) {
$member = new DeprecatedMember();
$member->setLevel(DeprecatedMember::LEVEL_MEMBER);
$member->setStatus(DeprecatedMember::STATUS_MEMBER);
$member->setContactId($davCard->getUniqueId());
$member->setType($type);
$member->setCircleId($circle->getUniqueId());
$member->setUserId($davCard->getUserId());
$this->membersService->updateCachedName($member);
try {
$this->membersRequest->createMember($member);
// if ($type === Member::TYPE_CONTACT) {
// $this->fileSharingBroadcaster->sendMailAboutExistingShares($circle, $member);
// }
} catch (MemberAlreadyExistsException $e) {
$this->membersRequest->checkMember($member, false);
}
}
}
/**
* @param DavCard $davCard
*
* @return int
*/
private function getMemberType(DavCard $davCard): int {
foreach (array_merge($davCard->getEmails(), $davCard->getClouds()) as $address) {
try {
$davCard->setUserId($this->isLocalMember($address));
return DavCard::TYPE_LOCAL;
} catch (NotLocalMemberException $e) {
}
}
$davCard->setUserId($davCard->getOwner() . ':' . $davCard->getContactId());
return DavCard::TYPE_CONTACT;
}
/**
* @param string $address
*
* @return string
* @throws NotLocalMemberException
*/
private function isLocalMember(string $address): string {
if (strpos($address, '@') === false) {
$user = $this->userManager->get($address);
if ($user !== null) {
return $user->getUID();
}
throw new NotLocalMemberException();
}
[$username, $domain] = explode('@', $address);
if (in_array($domain, $this->configService->getAvailableHosts())) {
$user = $this->userManager->get($username);
if ($user !== null) {
return $user->getUID();
}
}
throw new NotLocalMemberException();
}
/**
* @param DavCard $davCard
*/
private function manageCircles(DavCard $davCard) {
$fromCard = $davCard->getGroups();
$current = array_map(
function (DeprecatedCircle $circle) {
return $circle->getContactGroupName();
}, $this->getCirclesFromBook($davCard->getAddressBookId())
);
$this->miscService->log(
'manage Circles from DavCard: ' . json_encode($fromCard) . ' - current: ' . json_encode($current)
);
$this->manageNewCircles($davCard, $fromCard, $current);
$this->manageDeprecatedCircles($davCard->getAddressBookId());
$this->assignCirclesToCard($davCard);
}
/**
* @param DavCard $davCard
* @param array $fromCard
* @param array $current
*/
private function manageNewCircles(DavCard $davCard, array $fromCard, array $current) {
foreach ($fromCard as $group) {
if (in_array($group, $current)) {
continue;
}
$user = $this->userManager->get($davCard->getOwner());
$circle = new DeprecatedCircle(
$this->configService->contactsBackendType(), $group . ' - ' . $user->getDisplayName()
);
$circle->setAltName($group);
$circle->generateUniqueId();
$circle->setContactAddressBook($davCard->getAddressBookId());
$circle->setContactGroupName($group);
$this->miscService->log(
'creating new Circle: ' . json_encode($circle) . ', with owner=' . $davCard->getOwner(), 0
);
try {
$this->circlesRequest->createCircle($circle);
$owner = new DeprecatedMember($davCard->getOwner(), DeprecatedMember::TYPE_USER, $circle->getUniqueId());
$owner->setLevel(DeprecatedMember::LEVEL_OWNER);
$owner->setStatus(DeprecatedMember::STATUS_MEMBER);
$this->membersService->updateCachedName($owner);
$this->miscService->log('creating new Member: ' . json_encode($owner), 0);
try {
$this->membersRequest->createMember($owner);
} catch (MemberAlreadyExistsException $e) {
}
} catch (CircleAlreadyExistsException $e) {
}
}
}
/**
* @param DavCard $davCard
*/
private function assignCirclesToCard(DavCard $davCard) {
$this->miscService->log('assigning Circles to DavCard Model: ' . json_encode($davCard), 0);
foreach ($davCard->getGroups() as $group) {
try {
$davCard->addCircle(
$this->circlesRequest->getFromContactGroup($davCard->getAddressBookId(), $group)
);
} catch (CircleDoesNotExistException $e) {
}
}
$this->miscService->log('assigned Circles to DavCard Model: ' . json_encode($davCard), 0);
}
/**
* @param int $addressBookId
*
* @return DeprecatedCircle[]
*/
private function getCirclesFromBook(int $addressBookId): array {
return $this->circlesRequest->getFromBook($addressBookId);
}
/**
* @param int $length
*
* @return string
* @deprecated
*/
protected function uuid(int $length = 0): string {
$uuid = sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff), mt_rand(0, 0xfff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
if ($length > 0) {
if ($length <= 16) {
$uuid = str_replace('-', '', $uuid);
}
$uuid = substr($uuid, 0, $length);
}
return $uuid;
}
/**
* @param int $bookId
*
* @return string
*/
public function getOwnerFromAddressBook(int $bookId): string {
$this->miscService->log('Retrieving Owner from book:' . $bookId, 0);
$data = $this->cardDavBackend->getAddressBookById($bookId);
// let's assume the format is principals/users/OWNER
$owner = substr($data['principaluri'], 17);
$this->miscService->log('Retrieved Owner:' . $owner, 0);
return $owner;
}
/**
* @throws Exception
*/
public function migration() {
if (!$this->configService->isContactsBackend()) {
throw new Exception('Circles needs to be set as Contacts App Backend first');
}
$this->manageDeprecatedContacts();
$this->manageDeprecatedCircles();
$users = $this->userManager->search('');
$this->miscService->log('initiating migration for ' . sizeof($users) . ' users', 0);
foreach ($users as $user) {
$this->miscService->log('retrieving books for user=' . $user->getUID(), 0);
$books = $this->cardDavBackend->getAddressBooksForUser('principals/users/' . $user->getUID());
$this->miscService->log('initiating migration for user=' . $user->getUID(), 0);
foreach ($books as $book) {
$this->migrateBook($book['id']);
}
}
}
/**
*/
private function manageDeprecatedContacts() {
$contacts = $this->membersRequest->getMembersByContactId();
$this->miscService->log(
'Managing Deprecated Contacts, checking ' . sizeof($contacts) . ' known contacts in database', 0
);
foreach ($contacts as $contact) {
try {
$this->getDavCardFromMember($contact);
$this->miscService->log('Contact is not deprecated: ' . json_encode($contact));
} catch (MemberDoesNotExistException $e) {
$this->miscService->log(
'Contact is deprecated and will be removed: ' . json_encode($contact)
);
$this->membersRequest->removeMember($contact);
}
}
$this->miscService->log('Deprecated Contacts managed', 0);
}
/**
* @param int $bookId
*/
private function manageDeprecatedCircles(int $bookId = 0) {
$knownBooks = [$bookId];
$this->miscService->log('Managing Deprecated Circles, using bookId: ' . $bookId, 0);
if ($bookId > 0) {
$knownBooks = [];
$contacts = $this->membersRequest->getMembersByContactId();
$this->miscService->log(sizeof($contacts) . ' known members as contacts in Circles DB', 0);
foreach ($contacts as $contact) {
[$bookId,] = explode('/', $contact->getContactId(), 2);
if (in_array($bookId, $knownBooks)) {
continue;
}
$knownBooks[] = $bookId;
}
}
$this->miscService->log('Known books: ' . json_encode($knownBooks), 0);
foreach ($knownBooks as $bookId) {
$this->miscService->log('retrieving local Circles data for bookId=' . $bookId, 0);
$circles = $this->circlesRequest->getFromContactBook($bookId);
$this->miscService->log(
'Known circles for bookId=' . $bookId . ': ' . json_encode($circles), 0
);
$fromBook = $this->getExistingCirclesFromBook($bookId);
$this->miscService->log(
'Generated circles from bookId=' . $bookId . ': ' . json_encode($fromBook), 0
);
foreach ($circles as $circle) {
if (in_array($circle->getContactGroupName(), $fromBook)) {
continue;
}
$this->miscService->log(
$circle->getUniqueId() . ' is a deprecated Circle and will be destroyed', 0
);
$this->membersRequest->removeAllFromCircle($circle->getUniqueId());
$this->circlesRequest->destroyCircle($circle->getUniqueId());
}
}
$this->miscService->log('Deprecated Circles managed', 0);
}
/**
* @param int $bookId
*
* @return DeprecatedCircle[]
*/
private function getExistingCirclesFromBook(int $bookId): array {
$circles = [];
$cards = $this->cardDavBackend->getCards($bookId);
$this->miscService->log(
'retrieving existing circles from bookId=' . $bookId . ' in ' . sizeof($cards) . ' cards', 0
);
foreach ($cards as $card) {
$davCard = $this->generateDavCardFromCard($bookId, $card);
$this->assignCirclesToCard($davCard);
$circles = array_merge($circles, $davCard->getCircles());
}
$this->miscService->log('Found ' . sizeof($circles) . ' Circles from book=' . $bookId, 0);
$existing = array_unique(
array_map(
function (DeprecatedCircle $circle) {
return $circle->getContactGroupName();
}, $circles
)
);
$this->miscService->log(
'retrieved existing circles from book=' . $bookId . ': ' . json_encode($existing), 0
);
return $existing;
}
/**
* @param DeprecatedMember $contact
*
* @return DavCard
* @throws MemberDoesNotExistException
*/
public function getDavCardFromMember(DeprecatedMember $contact): DavCard {
[$bookId, $cardUri] = explode('/', $contact->getContactId(), 2);
$this->miscService->log('Retrieving DavCard from book:' . $bookId . ', uri:' . $cardUri, 0);
$cards = $this->cardDavBackend->getCards($bookId);
$this->miscService->log('Book contains ' . sizeof($cards) . ' cards', 0);
foreach ($cards as $card) {
if ($card['uri'] === $cardUri) {
$davCard = $this->generateDavCardFromCard($bookId, $card);
$this->miscService->log('Retrieved DavCard: ' . json_encode($card));
return $davCard;
}
}
throw new MemberDoesNotExistException();
}
/**
* @param int $bookId
*/
private function migrateBook(int $bookId) {
if (in_array($bookId, $this->migratedBooks)) {
return;
}
$this->miscService->log('migrating book: ' . $bookId, 0);
$owner = $this->getOwnerFromAddressBook($bookId);
$cards = $this->cardDavBackend->getCards($bookId);
$this->miscService->log('found ' . sizeof($cards) . 'cards from book=' . $bookId, 0);
foreach ($cards as $card) {
$davCard = new DavCard();
$davCard->setOwner($owner);
$davCard->importFromDav($card['carddata']);
$davCard->setAddressBookId($bookId);
$davCard->setCardUri($card['uri']);
$this->manageDavCard($davCard);
}
$this->migratedBooks[] = $bookId;
}
}
@@ -0,0 +1,407 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
* @author Daniel Tygel <dtygel@eita.org.br>
*
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Events\AddingCircleMemberEvent;
use OCA\Circles\Events\CircleCreatedEvent;
use OCA\Circles\Events\CircleDestroyedEvent;
use OCA\Circles\Events\CircleEditedEvent;
use OCA\Circles\Events\CircleGenericEvent;
use OCA\Circles\Events\CircleMemberAddedEvent;
use OCA\Circles\Events\CircleMemberEditedEvent;
use OCA\Circles\Events\CircleMemberRemovedEvent;
use OCA\Circles\Events\CircleMemberRequestedEvent;
use OCA\Circles\Events\CreatingCircleEvent;
use OCA\Circles\Events\DestroyingCircleEvent;
use OCA\Circles\Events\EditingCircleEvent;
use OCA\Circles\Events\EditingCircleMemberEvent;
use OCA\Circles\Events\Files\CreatingFileShareEvent;
use OCA\Circles\Events\Files\FileShareCreatedEvent;
use OCA\Circles\Events\Files\PreparingFileShareEvent;
use OCA\Circles\Events\MembershipsCreatedEvent;
use OCA\Circles\Events\MembershipsRemovedEvent;
use OCA\Circles\Events\PreparingCircleMemberEvent;
use OCA\Circles\Events\RemovingCircleMemberEvent;
use OCA\Circles\Events\RequestingCircleMemberEvent;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\Membership;
use OCA\Circles\Model\Mount;
use OCA\Circles\Model\ShareWrapper;
use OCP\EventDispatcher\IEventDispatcher;
/**
* Class EventService
*
* @package OCA\Circles\Service
*/
class EventService {
use TNCLogger;
/** @var IEventDispatcher */
private $eventDispatcher;
/**
* EventService constructor.
*
* @param IEventDispatcher $eventDispatcher
*/
public function __construct(IEventDispatcher $eventDispatcher) {
$this->eventDispatcher = $eventDispatcher;
$this->setup('app', Application::APP_ID);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function circleCreating(FederatedEvent $federatedEvent): void {
$event = new CreatingCircleEvent($federatedEvent);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function circleCreated(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleCreatedEvent($federatedEvent, $results);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function circleEditing(FederatedEvent $federatedEvent): void {
$event = new EditingCircleEvent($federatedEvent);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function circleEdited(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleEditedEvent($federatedEvent, $results);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function circleDestroying(FederatedEvent $federatedEvent): void {
$event = new DestroyingCircleEvent($federatedEvent);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function circleDestroyed(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleDestroyedEvent($federatedEvent, $results);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberPreparing(FederatedEvent $federatedEvent): void {
$event = new PreparingCircleMemberEvent($federatedEvent);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberAdding(FederatedEvent $federatedEvent): void {
$event = new AddingCircleMemberEvent($federatedEvent);
$event->setType(CircleGenericEvent::INVITED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function memberAdded(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberAddedEvent($federatedEvent, $results);
$event->setType(CircleGenericEvent::INVITED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberInviting(FederatedEvent $federatedEvent): void {
$event = new RequestingCircleMemberEvent($federatedEvent);
$event->setType(CircleGenericEvent::INVITED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function memberInvited(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberRequestedEvent($federatedEvent, $results);
$event->setType(CircleGenericEvent::INVITED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberRequesting(FederatedEvent $federatedEvent): void {
$event = new RequestingCircleMemberEvent($federatedEvent);
$event->setType(CircleGenericEvent::REQUESTED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function memberRequested(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberRequestedEvent($federatedEvent, $results);
$event->setType(CircleGenericEvent::REQUESTED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberJoining(FederatedEvent $federatedEvent): void {
$event = new AddingCircleMemberEvent($federatedEvent);
$event->setType(CircleGenericEvent::JOINED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function memberJoined(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberAddedEvent($federatedEvent, $results);
$event->setType(CircleGenericEvent::JOINED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberLevelEditing(FederatedEvent $federatedEvent): void {
$event = new EditingCircleMemberEvent($federatedEvent);
$event->setLevel($federatedEvent->getData()->gInt('level'));
$event->setType(CircleGenericEvent::LEVEL);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function memberLevelEdited(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberEditedEvent($federatedEvent, $results);
$event->setNewLevel($federatedEvent->getData()->gInt('level'));
$event->setType(CircleGenericEvent::LEVEL);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberNameEditing(FederatedEvent $federatedEvent): void {
$event = new EditingCircleMemberEvent($federatedEvent);
$event->setDisplayName($federatedEvent->getData()->g('displayName'));
$event->setType(CircleGenericEvent::NAME);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function memberNameEdited(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberEditedEvent($federatedEvent, $results);
$event->setNewDisplayName($federatedEvent->getData()->g('displayName'));
$event->setType(CircleGenericEvent::NAME);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberRemoving(FederatedEvent $federatedEvent): void {
$event = new RemovingCircleMemberEvent($federatedEvent);
$event->setType(CircleGenericEvent::REMOVED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function memberRemoved(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberRemovedEvent($federatedEvent, $results);
$event->setType(CircleGenericEvent::REMOVED);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function memberLeaving(FederatedEvent $federatedEvent): void {
$event = new RemovingCircleMemberEvent($federatedEvent);
$event->setType(CircleGenericEvent::LEFT);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param SimpleDataStore[] $results
*/
public function memberLeft(FederatedEvent $federatedEvent, array $results): void {
$event = new CircleMemberRemovedEvent($federatedEvent, $results);
$event->setType(CircleGenericEvent::LEFT);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
*/
public function fileSharePreparing(FederatedEvent $federatedEvent): void {
$event = new PreparingFileShareEvent($federatedEvent);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param Mount|null $mount
*/
public function fileShareCreating(FederatedEvent $federatedEvent, ?Mount $mount = null): void {
$event = new CreatingFileShareEvent($federatedEvent);
if (!is_null($mount)) {
$event->setMount($mount);
}
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param FederatedEvent $federatedEvent
* @param SimpleDataStore[] $result
*/
public function fileShareCreated(FederatedEvent $federatedEvent, array $result): void {
$event = new FileShareCreatedEvent($federatedEvent, $result);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param Membership[] $new
*/
public function membershipsCreated(array $new): void {
$event = new MembershipsCreatedEvent($new);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param Membership[] $deprecated
*/
public function membershipsRemoved(array $deprecated): void {
$event = new MembershipsRemovedEvent($deprecated);
$this->eventDispatcher->dispatchTyped($event);
}
/**
* @param ShareWrapper $wrappedShare
*/
public function localShareCreated(ShareWrapper $wrappedShare): void {
}
/**
* @param ShareWrapper $wrappedShare
* @param Mount $mount
*/
public function federatedShareCreated(ShareWrapper $wrappedShare, Mount $mount): void {
// Circles::shareToCircle(
// $circle->getUniqueId(), 'files', '',
// ['id' => $share->getId(), 'share' => $this->shareObjectToArray($share)],
// '\OCA\Circles\Circles\FileSharingBroadcaster'
// );
}
/**
* @param ShareWrapper $wrappedShare
*/
public function localShareDeleted(ShareWrapper $wrappedShare): void {
}
/**
* @param ShareWrapper $wrappedShare
*/
public function federatedShareDeleted(ShareWrapper $wrappedShare): void {
}
// /**
// * @param FederatedEvent $federatedEvent
// */
// public function onSharedItemsSyncRequested(FederatedEvent $federatedEvent) {
// $event = new SharedItemsSyncRequestedEvent($federatedEvent);
// $this->eventDispatcher->dispatchTyped($event);
// }
}
@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use Exception;
use OCA\Circles\Db\EventWrapperRequest;
use OCA\Circles\Model\Federated\EventWrapper;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Tools\ActivityPub\NCSignature;
use OCA\Circles\Tools\Traits\TNCRequest;
use OCA\Circles\Tools\Traits\TStringTools;
/**
* Class EventWrapperService
*
* @package OCA\Circles\Service
*/
class EventWrapperService extends NCSignature {
use TNCRequest;
use TStringTools;
public const RETRY_ASAP = 'asap';
public const RETRY_HOURLY = 'hourly';
public const RETRY_DAILY = 'daily';
public const RETRY_ERROR = 100;
public static $RETRIES = [
'asap' => [0, 5],
'hourly' => [5, 150],
'daily' => [150, 300]
];
/** @var EventWrapperRequest */
private $eventWrapperRequest;
/** @var FederatedEventService */
private $federatedEventService;
/** @var RemoteUpstreamService */
private $remoteUpstreamService;
/** @var ConfigService */
private $configService;
/**
* EventWrapperService constructor.
*
* @param EventWrapperRequest $eventWrapperRequest
* @param FederatedEventService $federatedEventService
* @param RemoteUpstreamService $remoteUpstreamService
* @param ConfigService $configService
*/
public function __construct(
EventWrapperRequest $eventWrapperRequest,
FederatedEventService $federatedEventService,
RemoteUpstreamService $remoteUpstreamService,
ConfigService $configService
) {
$this->eventWrapperRequest = $eventWrapperRequest;
$this->federatedEventService = $federatedEventService;
$this->remoteUpstreamService = $remoteUpstreamService;
$this->configService = $configService;
}
/**
* @param string $token
* @param bool $refresh
*/
public function confirmStatus(string $token, bool $refresh = false): void {
$wrappers = $this->eventWrapperRequest->getByToken($token);
foreach ($wrappers as $wrapper) {
$status = $wrapper->getStatus();
if ($refresh && ($status === EventWrapper::STATUS_FAILED ||
$status === EventWrapper::STATUS_INIT)) {
$wrapper->setStatus(EventWrapper::STATUS_INIT);
$this->eventWrapperRequest->update($wrapper);
$status = $this->manageWrapper($wrapper);
}
if ($status !== EventWrapper::STATUS_DONE) {
return;
}
}
$this->federatedEventService->manageResults($token);
$this->eventWrapperRequest->updateAll($token, EventWrapper::STATUS_OVER);
}
/**
* @param EventWrapper $wrapper
*
* @return int
*/
public function manageWrapper(EventWrapper $wrapper): int {
if ($wrapper->getStatus() !== EventWrapper::STATUS_INIT) {
return $wrapper->getStatus();
}
$status = EventWrapper::STATUS_FAILED;
$retry = $wrapper->getRetry();
try {
if ($this->configService->isLocalInstance($wrapper->getInstance())) {
$gs = $this->federatedEventService->getFederatedItem($wrapper->getEvent(), false);
$gs->manage($wrapper->getEvent());
} else {
$this->remoteUpstreamService->broadcastEvent($wrapper);
}
$status = EventWrapper::STATUS_DONE;
} catch (Exception $e) {
$retry++;
}
if ($wrapper->getSeverity() !== FederatedEvent::SEVERITY_HIGH) {
$status = EventWrapper::STATUS_OVER;
}
$wrapper->setStatus($status);
$wrapper->setRetry($retry);
$wrapper->setResult($wrapper->getEvent()->getResult());
$this->eventWrapperRequest->update($wrapper);
return $status;
}
/**
* @param string $retry
*/
public function retry(string $retry) {
$tokens = $this->getFailedEvents(self::$RETRIES[$retry]);
foreach ($tokens as $token) {
$this->confirmStatus($token, true);
}
}
/**
* @param array $retryRange
*
* @return array
*/
private function getFailedEvents(array $retryRange): array {
$token = array_map(
function (EventWrapper $event): string {
return $event->getToken();
}, $this->eventWrapperRequest->getFailedEvents($retryRange)
);
return array_values(array_unique($token));
}
}
@@ -0,0 +1,946 @@
<?php
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @author: Vinicius Cubas Brand <viniciuscb@gmail.com>
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\DeprecatedCirclesRequest;
use OCA\Circles\Db\DeprecatedMembersRequest;
use OCA\Circles\Model\DeprecatedCircle;
use OCA\Circles\Model\FederatedLink;
use OCA\Circles\Model\DeprecatedMember;
use OCP\Activity\IEvent;
use OCP\Activity\IManager as IActivityManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use OCP\Notification\INotification;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\GenericEvent;
/**
* @deprecated
*
* Class EventsService
*
* @package OCA\Circles\Service
*/
class EventsService {
/** @var string */
private $userId;
/** @var ITimeFactory */
private $time;
/** @var IActivityManager */
private $activityManager;
/** @var INotificationManager */
private $notificationManager;
/** @var IUserManager */
private $userManager;
/** @var IURLGenerator */
private $urlGenerator;
/** @var EventDispatcher */
private $eventDispatcher;
/** @var DeprecatedCirclesRequest */
private $circlesRequest;
/** @var DeprecatedMembersRequest */
private $membersRequest;
/** @var ConfigService */
private $configService;
/** @var MiscService */
private $miscService;
/**
* Events constructor.
*
* @param string $userId
* @param ITimeFactory $time
* @param IActivityManager $activityManager
* @param INotificationManager $notificationManager
* @param IUserManager $userManager
* @param IURLGenerator $urlGenerator
* @param EventDispatcher $eventDispatcher
* @param DeprecatedCirclesRequest $circlesRequest
* @param DeprecatedMembersRequest $membersRequest
* @param ConfigService $configService
* @param MiscService $miscService
*/
public function __construct(
$userId, ITimeFactory $time, IActivityManager $activityManager,
INotificationManager $notificationManager, IUserManager $userManager, IURLGenerator $urlGenerator,
EventDispatcher $eventDispatcher, DeprecatedCirclesRequest $circlesRequest, DeprecatedMembersRequest $membersRequest,
ConfigService $configService, MiscService $miscService
) {
$this->userId = $userId;
$this->time = $time;
$this->activityManager = $activityManager;
$this->notificationManager = $notificationManager;
$this->userManager = $userManager;
$this->urlGenerator = $urlGenerator;
$this->eventDispatcher = $eventDispatcher;
$this->circlesRequest = $circlesRequest;
$this->membersRequest = $membersRequest;
$this->configService = $configService;
$this->miscService = $miscService;
}
/**
* onCircleCreation()
*
* Called when a circle is created.
* Broadcast an activity to the cloud
* We won't do anything if the circle is not PUBLIC or CLOSED
*
* @param DeprecatedCircle $circle
*/
public function onCircleCreation(DeprecatedCircle $circle) {
if ($circle->getType() !== DeprecatedCircle::CIRCLES_PUBLIC
&& $circle->getType() !== DeprecatedCircle::CIRCLES_CLOSED) {
return;
}
if ($this->configService->getAppValue(ConfigService::ACTIVITY_ON_NEW_CIRCLE) === '1') {
$event = $this->generateEvent('circles_as_non_member');
$event->setSubject('circle_create', ['circle' => json_encode($circle)]);
$this->userManager->callForSeenUsers(
function ($user) use ($event) {
/** @var IUser $user */
$this->publishEvent($event, [$user]);
}
);
}
$this->dispatch('\OCA\Circles::onCircleCreation', ['circle' => $circle]);
}
/**
* onCircleDestruction()
*
* Called when a circle is destroyed.
* Broadcast an activity on its members.
* We won't do anything if the circle is PERSONAL
*
* @param DeprecatedCircle $circle
*/
public function onCircleDestruction(DeprecatedCircle $circle) {
if ($circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL) {
return;
}
$event = $this->generateEvent('circles_as_member');
$event->setSubject('circle_delete', ['circle' => json_encode($circle)]);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($circle->getUniqueId(), DeprecatedMember::LEVEL_MEMBER, 0, true)
);
$this->dispatch('\OCA\Circles::onCircleDestruction', ['circle' => $circle]);
}
/**
* onMemberNew()
*
* Called when a member is added to a circle.
* Broadcast an activity to the new member and to the moderators of the circle.
* We won't do anything if the circle is PERSONAL
* If the level is still 0, we will redirect to onMemberAlmost and manage the
* invitation/request from there
* If the level is Owner, we ignore the event.
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
public function onMemberNew(DeprecatedCircle $circle, DeprecatedMember $member) {
if ($member->getLevel() === DeprecatedMember::LEVEL_OWNER
|| $circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL
) {
return;
}
if ($member->getLevel() === DeprecatedMember::LEVEL_NONE) {
$this->onMemberAlmost($circle, $member);
return;
}
$event = $this->generateEvent('circles_as_member');
$event->setSubject(
($this->userId === $member->getUserId()) ? 'member_join' : 'member_add',
['circle' => json_encode($circle), 'member' => json_encode($member)]
);
$this->publishEvent(
$event, array_merge(
[$member],
$this->membersRequest->forceGetMembers(
$circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true
)
)
);
$this->dispatch('\OCA\Circles::onMemberNew', ['circle' => $circle, 'member' => $member]);
$this->notificationOnMemberNew($circle, $member);
}
/**
* onMemberAlmost()
*
* Called when a member is added to a circle with level=0
* Trigger onMemberInvitation() or onMemberInvitationRequest() based on Member Status
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
private function onMemberAlmost(DeprecatedCircle $circle, DeprecatedMember $member) {
switch ($member->getStatus()) {
case DeprecatedMember::STATUS_INVITED:
$this->onMemberInvited($circle, $member);
return;
case DeprecatedMember::STATUS_REQUEST:
$this->onMemberRequesting($circle, $member);
return;
}
}
/**
* onMemberInvited()
*
* Called when a member is invited to a circle.
* Broadcast an activity to the invited member and to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
private function onMemberInvited(DeprecatedCircle $circle, DeprecatedMember $member) {
if ($circle->getType() !== DeprecatedCircle::CIRCLES_CLOSED) {
return;
}
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'member_invited', ['circle' => json_encode($circle), 'member' => json_encode($member)]
);
$this->publishEvent(
$event, array_merge(
[$member],
$this->membersRequest->forceGetMembers(
$circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true
)
)
);
$this->dispatch('\OCA\Circles::onMemberInvited', ['circle' => $circle, 'member' => $member]);
$this->notificationOnInvitation($circle, $member);
}
/**
* onMemberRequesting()
*
* Called when a member request an invitation to a private circle.
* Broadcast an activity to the requester and to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
private function onMemberRequesting(DeprecatedCircle $circle, DeprecatedMember $member) {
if ($circle->getType() !== DeprecatedCircle::CIRCLES_CLOSED) {
return;
}
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'member_request_invitation',
['circle' => json_encode($circle), 'member' => json_encode($member)]
);
$this->publishEvent(
$event, array_merge(
[$member],
$this->membersRequest->forceGetMembers(
$circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true
)
)
);
$this->dispatch('\OCA\Circles::onMemberRequesting', ['circle' => $circle, 'member' => $member]);
$this->notificationOnRequest($circle, $member);
}
/**
* onMemberLeaving()
*
* Called when a member is removed from a circle.
* Broadcast an activity to the leaving member and to the moderators of the circle.
* We won't do anything if the circle is PERSONAL
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
public function onMemberLeaving(DeprecatedCircle $circle, DeprecatedMember $member) {
if ($circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL) {
return;
}
$event = $this->generateEvent('circles_as_member');
$event->setSubject(
($this->userId === $member->getUserId()) ? 'member_left' : 'member_remove',
['circle' => json_encode($circle), 'member' => json_encode($member)]
);
$this->publishEvent(
$event, array_merge(
[$member],
$this->membersRequest->forceGetMembers(
$circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true
)
)
);
$this->dispatch('\OCA\Circles::onMemberLeaving', ['circle' => $circle, 'member' => $member]);
$this->deleteNotification('membership', $member->getMemberId());
$this->deleteNotification('membership_request', $member->getMemberId());
}
/**
* onMemberLevel()
*
* Called when a member have his level changed.
* Broadcast an activity to all moderator of the circle, and the member if he is demoted.
* If the level is Owner, we identify the event as a Coup d'Etat and we broadcast all members.
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
public function onMemberLevel(DeprecatedCircle $circle, DeprecatedMember $member) {
if ($member->getLevel() === DeprecatedMember::LEVEL_OWNER) {
$this->onMemberOwner($circle, $member);
return;
}
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'member_level',
['circle' => json_encode($circle), 'member' => json_encode($member)]
);
$mods =
$this->membersRequest->forceGetMembers($circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true);
$this->membersRequest->avoidDuplicateMembers($mods, [$member]);
$this->publishEvent($event, $mods);
$this->dispatch('\OCA\Circles::onMemberLevel', ['circle' => $circle, 'member' => $member]);
}
/**
* onMemberOwner()
*
* Called when the owner rights of a circle have be given to another member.
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
public function onMemberOwner(DeprecatedCircle $circle, DeprecatedMember $member) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'member_owner',
['circle' => json_encode($circle), 'member' => json_encode($member)]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($circle->getUniqueId(), DeprecatedMember::LEVEL_MEMBER, 0, true)
);
$this->dispatch('\OCA\Circles::onMemberOwner', ['circle' => $circle, 'member' => $member]);
}
/**
* onGroupLink()
*
* Called when a group is linked to a circle.
* Broadcast an activity to the member of the linked group and to the moderators of the circle.
* We won't do anything if the circle is PERSONAL
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $group
*/
public function onGroupLink(DeprecatedCircle $circle, DeprecatedMember $group) {
if ($circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL) {
return;
}
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'group_link',
['circle' => json_encode($circle), 'group' => json_encode($group)]
);
$mods =
$this->membersRequest->forceGetMembers($circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true);
$this->membersRequest->avoidDuplicateMembers(
$mods, $this->membersRequest->getGroupMemberMembers($group)
);
$this->publishEvent($event, $mods);
$this->dispatch('\OCA\Circles::onGroupLink', ['circle' => $circle, 'group' => $group]);
}
/**
* onGroupUnlink()
*
* Called when a group is unlinked from a circle.
* Broadcast an activity to the member of the unlinked group and to the moderators of the
* circle. We won't do anything if the circle is PERSONAL
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $group
*/
public function onGroupUnlink(DeprecatedCircle $circle, DeprecatedMember $group) {
if ($circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL) {
return;
}
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'group_unlink',
['circle' => json_encode($circle), 'group' => json_encode($group)]
);
$mods =
$this->membersRequest->forceGetMembers($circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true);
$this->membersRequest->avoidDuplicateMembers(
$mods, $this->membersRequest->getGroupMemberMembers($group)
);
$this->publishEvent($event, $mods);
$this->dispatch('\OCA\Circles::onGroupUnlink', ['circle' => $circle, 'group' => $group]);
}
/**
* onGroupLevel()
*
* Called when a linked group have his level changed.
* Broadcast an activity to all moderator of the circle, and the group members in case of
* demotion.
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $group
*/
public function onGroupLevel(DeprecatedCircle $circle, DeprecatedMember $group) {
if ($circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL) {
return;
}
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'group_level',
['circle' => json_encode($circle), 'group' => json_encode($group)]
);
$mods =
$this->membersRequest->forceGetMembers($circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR, 0, true);
$this->membersRequest->avoidDuplicateMembers(
$mods, $this->membersRequest->getGroupMemberMembers($group)
);
$this->publishEvent($event, $mods);
$this->dispatch('\OCA\Circles::onGroupLevel', ['circle' => $circle, 'group' => $group]);
}
/**
* onLinkRequestSent()
*
* Called when a request to generate a link with a remote circle is sent.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkRequestSent(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_request_sent',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkRequestSent', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkRequestReceived()
*
* Called when a request to generate a link from a remote host is received.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkRequestReceived(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_request_received',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkRequestReceived', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkRequestRejected()
*
* Called when a request to generate a link from a remote host is dismissed.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkRequestRejected(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_request_rejected',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkRequestRejected', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkRequestCanceled()
*
* Called when a request to generate a link from a remote host is dismissed.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkRequestCanceled(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_request_canceled',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkRequestCanceled', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkRequestAccepted()
*
* Called when a request to generate a link from a remote host is accepted.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkRequestAccepted(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_request_accepted',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkRequestAccepted', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkRequestAccepting()
*
* Called when a link is Up and Running.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkRequestAccepting(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_request_accepting',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkRequestAccepting', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkUp()
*
* Called when a link is Up and Running.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkUp(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_up',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkUp', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkDown()
*
* Called when a link is closed (usually by remote).
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkDown(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
$event->setSubject(
'link_down',
['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkDown', ['circle' => $circle, 'link' => $link]);
}
/**
* onLinkRemove()
*
* Called when a link is removed.
* Subject is based on the current status of the Link.
* Broadcast an activity to the moderators of the circle.
*
* @param DeprecatedCircle $circle
* @param FederatedLink $link
*/
public function onLinkRemove(DeprecatedCircle $circle, FederatedLink $link) {
$event = $this->generateEvent('circles_as_moderator');
if ($link->getStatus() === FederatedLink::STATUS_LINK_DOWN) {
return;
}
$subject = 'link_remove';
if ($link->getStatus() === FederatedLink::STATUS_LINK_REQUESTED) {
$subject = 'link_request_removed';
} elseif ($link->getStatus() === FederatedLink::STATUS_REQUEST_SENT) {
$subject = 'link_request_canceling';
}
$event->setSubject(
$subject, ['circle' => $circle->getJson(false, true), 'link' => $link->getJson()]
);
$this->publishEvent(
$event,
$this->membersRequest->forceGetMembers($link->getCircleId(), DeprecatedMember::LEVEL_MODERATOR, 0, true)
);
$this->dispatch('\OCA\Circles::onLinkRemove', ['circle' => $circle, 'link' => $link]);
}
/**
* onSettingsChange()
*
* Called when the circle's settings are changed
*
* @param DeprecatedCircle $circle
* @param array $oldSettings
*/
public function onSettingsChange(DeprecatedCircle $circle, array $oldSettings = []) {
$this->dispatch(
'\OCA\Circles::onSettingsChange', ['circle' => $circle, 'oldSettings' => $oldSettings]
);
}
/**
* generateEvent()
* Create an Activity Event with the basic settings for the app.
*
* @param $type
*
* @return \OCP\Activity\IEvent
*/
private function generateEvent($type) {
$event = $this->activityManager->generateEvent();
$event->setApp(Application::APP_ID)
->setType($type);
// if ($this->userId === null) {
// $event->setAuthor($this->userId);
// }
return $event;
}
/**
* Publish the event to the users.
*
* @param IEvent $event
* @param array $users
*/
private function publishEvent(IEvent $event, array $users) {
foreach ($users as $user) {
if ($user instanceof IUser) {
$userId = $user->getUID();
} elseif ($user instanceof DeprecatedMember) {
$userId = $user->getUserId();
} else {
continue;
}
$event->setAffectedUser($userId);
$this->activityManager->publish($event);
}
}
/**
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
private function notificationOnInvitation(DeprecatedCircle $circle, DeprecatedMember $member) {
$this->deleteNotification('membership_request', $member->getMemberId());
if ($member->getType() !== DeprecatedMember::TYPE_USER) {
return;
}
$notification = $this->createNotification(
$circle, $circle->getViewer(), $member->getUserId(), 'invitation', 'membership',
$member->getMemberId()
);
$declineAction = $notification->createAction();
$declineUrl =
$this->urlGenerator->linkToRoute('circles.Circles.leave', ['uniqueId' => $circle->getUniqueId()]);
$declineAction->setLabel('refuse')
->setLink($this->urlGenerator->getAbsoluteURL($declineUrl), 'GET');
$notification->addAction($declineAction);
$acceptAction = $notification->createAction();
$acceptUrl =
$this->urlGenerator->linkToRoute('circles.Circles.join', ['uniqueId' => $circle->getUniqueId()]);
$acceptAction->setLabel('accept')
->setLink($this->urlGenerator->getAbsoluteURL($acceptUrl), 'GET');
$notification->addAction($acceptAction);
$this->notificationManager->notify($notification);
}
/**
* @param DeprecatedCircle $circle
* @param DeprecatedMember $author
*/
private function notificationOnRequest(DeprecatedCircle $circle, DeprecatedMember $author) {
$members = $this->membersRequest->forceGetMembers($circle->getUniqueId(), DeprecatedMember::LEVEL_MODERATOR);
foreach ($members as $member) {
$notification = $this->createNotification(
$circle, $author, $member->getUserId(), 'request_new', 'membership_request',
$author->getMemberId()
);
$declineAction = $notification->createAction();
$declineUrl = $this->urlGenerator->linkToRoute(
'circles.Members.removeMemberById', ['memberId' => $author->getMemberId()]
);
$declineAction->setLabel('refuse')
->setLink($this->urlGenerator->getAbsoluteURL($declineUrl), 'DELETE');
$notification->addAction($declineAction);
$acceptAction = $notification->createAction();
$acceptUrl = $this->urlGenerator->linkToRoute(
'circles.Members.addMemberById', ['memberId' => $author->getMemberId()]
);
$acceptAction->setLabel('accept')
->setLink($this->urlGenerator->getAbsoluteURL($acceptUrl), 'PUT');
$notification->addAction($acceptAction);
$this->notificationManager->notify($notification);
}
}
/**
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*/
private function notificationOnMemberNew(DeprecatedCircle $circle, DeprecatedMember $member) {
$this->deleteNotification('membership_request', $member->getMemberId());
$this->deleteNotification('membership', $member->getMemberId());
if ($this->userId === $member->getUserId()) {
return;
}
$notification =
$this->createNotification(
$circle, $circle->getViewer(), $member->getUserId(), 'member_new', 'membership',
$member->getMemberId()
);
$leave = $notification->createAction();
$leaveUrl =
$this->urlGenerator->linkToRoute('circles.Circles.leave', ['uniqueId' => $circle->getUniqueId()]);
$leave->setLabel('leave')
->setLink($this->urlGenerator->getAbsoluteURL($leaveUrl), 'GET');
$notification->addAction($leave);
$this->notificationManager->notify($notification);
}
/**
* @param string $object
* @param string $objectId
*/
public function deleteNotification(string $object, string $objectId) {
if ($objectId === '') {
return;
}
$notification = $this->notificationManager->createNotification();
$notification->setApp('circles')
->setObject($object, $objectId);
$this->notificationManager->markProcessed($notification);
}
/**
* @param DeprecatedCircle $circle
* @param DeprecatedMember $author
* @param string $userId
* @param string $subject
* @param string $object
* @param string $objectId
*
* @return INotification
*/
private function createNotification(
DeprecatedCircle $circle, DeprecatedMember $author, string $userId, string $subject, string $object, string $objectId
) {
$authorName = $author->getCachedName();
if ($authorName === '') {
$authorName = $author->getUserId();
}
$now = $this->time->getDateTime();
$notification = $this->notificationManager->createNotification();
$notification->setApp('circles')
->setDateTime($now)
->setUser($userId)
->setObject($object, $objectId)
->setSubject(
$subject, [$authorName, $circle->getName(), json_encode($circle)]
);
return $notification;
}
/**
* @param string $context
* @param array $arguments
*/
private function dispatch(string $context, $arguments) {
$this->eventDispatcher->dispatch($context, new GenericEvent(null, $arguments));
}
}
@@ -0,0 +1,535 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\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 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 (!$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 Circle');
}
}
/**
* @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 = OC::$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->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($instances) && !$event->isAsync()) {
return false;
}
$wrapper = new EventWrapper();
$wrapper->setEvent($event);
$wrapper->setToken($this->uuid());
$wrapper->setCreation(time());
$wrapper->setSeverity($event->getSeverity());
if ($event->isAsync()) {
$wrapper->setInstance($this->configService->getLoopbackInstance());
$this->eventWrapperRequest->save($wrapper);
}
foreach ($instances as $instance) {
if ($event->getCircle()->isConfig(Circle::CFG_LOCAL)) {
break;
}
$wrapper->setInstance($instance->getInstance());
$wrapper->setInterface($instance->getInterface());
$this->eventWrapperRequest->save($wrapper);
}
$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) {
}
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\FederatedEventDSyncException;
use OCA\Circles\Exceptions\FederatedEventException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\FederatedShareAlreadyLockedException;
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
use OCA\Circles\Exceptions\InitiatorNotFoundException;
use OCA\Circles\Exceptions\OwnerNotFoundException;
use OCA\Circles\Exceptions\RemoteNotFoundException;
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\FederatedItems\ItemLock;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\Federated\FederatedShare;
use OCA\Circles\Tools\ActivityPub\NCSignature;
use OCA\Circles\Tools\Exceptions\InvalidItemException;
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
use OCA\Circles\Tools\Exceptions\SignatoryException;
use OCA\Circles\Tools\Exceptions\UnknownTypeException;
/**
* Class FederatedShareService
*
* @package OCA\Circles\Service
*/
class FederatedShareService extends NCSignature {
/** @var FederatedEventService */
private $federatedEventService;
/** @var CircleService */
private $circleService;
/**
* FederatedEventService constructor.
*
* @param FederatedEventService $federatedEventService
* @param CircleService $circleService
*/
public function __construct(FederatedEventService $federatedEventService, CircleService $circleService) {
$this->federatedEventService = $federatedEventService;
$this->circleService = $circleService;
}
/**
* @param string $circleId
* @param string $itemId
*
* @return FederatedShare
* @throws FederatedEventDSyncException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws FederatedShareAlreadyLockedException
* @throws InitiatorNotConfirmedException
* @throws InvalidItemException
* @throws OwnerNotFoundException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestNetworkException
* @throws SignatoryException
* @throws UnknownRemoteException
* @throws UnknownTypeException
* @throws CircleNotFoundException
* @throws InitiatorNotFoundException
*/
public function lockItem(string $circleId, string $itemId): FederatedShare {
$circle = $this->circleService->getCircle($circleId);
$event = new FederatedEvent(ItemLock::class);
$event->setCircle($circle);
$event->getData()->s('itemId', $itemId);
$data = $this->federatedEventService->newEvent($event);
/** @var FederatedShare $share */
$share = $data->gObj('federatedShare', FederatedShare::class);
if ($share->getLockStatus() === ItemLock::STATUS_INSTANCE_LOCKED) {
throw new FederatedShareAlreadyLockedException('item already locked by ' . $share->getInstance());
}
return $share;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2017
* @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\Circles\Service;
use Exception;
use OCA\Circles\Db\DeprecatedCirclesRequest;
use OCA\Circles\Db\EventWrapperRequest;
use OCA\Circles\Exceptions\CircleDoesNotExistException;
use OCA\Circles\Exceptions\ConfigNoCircleAvailableException;
use OCA\Circles\Exceptions\GlobalScaleDSyncException;
use OCA\Circles\Exceptions\GlobalScaleEventException;
use OCA\Circles\Exceptions\GSKeyException;
use OCA\Circles\Model\GlobalScale\GSEvent;
use OCP\IURLGenerator;
/**
* Class GSDownstreamService
*
* @package OCA\Circles\Service
*/
class GSDownstreamService {
/** @var string */
private $userId = '';
/** @var IURLGenerator */
private $urlGenerator;
/** @var EventWrapperRequest */
private $eventWrapperRequest;
/** @var DeprecatedCirclesRequest */
private $circlesRequest;
/** @var GlobalScaleService */
private $globalScaleService;
/** @var ConfigService */
private $configService;
/** @var MiscService */
private $miscService;
/**
* GSUpstreamService constructor.
*
* @param $userId
* @param IURLGenerator $urlGenerator
* @param EventWrapperRequest $eventWrapperRequest
* @param DeprecatedCirclesRequest $circlesRequest
* @param GlobalScaleService $globalScaleService
* @param ConfigService $configService
* @param MiscService $miscService
*/
public function __construct(
$userId,
IURLGenerator $urlGenerator,
EventWrapperRequest $eventWrapperRequest,
DeprecatedCirclesRequest $circlesRequest,
GlobalScaleService $globalScaleService,
ConfigService $configService,
MiscService $miscService
) {
$this->userId = $userId;
$this->urlGenerator = $urlGenerator;
$this->eventWrapperRequest = $eventWrapperRequest;
$this->circlesRequest = $circlesRequest;
$this->globalScaleService = $globalScaleService;
$this->configService = $configService;
$this->miscService = $miscService;
}
/**
* @param GSEvent $event
*
* @throws GSKeyException
* @throws GlobalScaleEventException
* @throws CircleDoesNotExistException
* @throws ConfigNoCircleAvailableException
* @throws GlobalScaleDSyncException
*/
public function requestedEvent(GSEvent $event) {
$this->globalScaleService->checkEvent($event);
$gs = $this->globalScaleService->getGlobalScaleEvent($event);
$gs->verify($event, true);
if (!$event->isAsync()) {
$gs->manage($event);
}
$this->globalScaleService->asyncBroadcast($event);
}
/**
* @param GSEvent $event
*
* @throws CircleDoesNotExistException
* @throws ConfigNoCircleAvailableException
* @throws GSKeyException
* @throws GlobalScaleDSyncException
* @throws GlobalScaleEventException
*/
public function statusEvent(GSEvent $event) {
$this->globalScaleService->checkEvent($event);
$gs = $this->globalScaleService->getGlobalScaleEvent($event);
$gs->verify($event, false);
$gs->manage($event);
}
/**
* @param GSEvent $event
*/
public function onNewEvent(GSEvent $event) {
try {
$this->globalScaleService->checkEvent($event);
$gs = $this->globalScaleService->getGlobalScaleEvent($event);
$gs->manage($event);
} catch (Exception $e) {
$this->miscService->log('issue onNewEvent: ' . json_encode($event) . ' - ' . $e->getMessage());
}
}
}
@@ -0,0 +1,457 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\Tools\Exceptions\RequestContentException;
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
use OCA\Circles\Tools\Exceptions\RequestResultNotJsonException;
use OCA\Circles\Tools\Exceptions\RequestResultSizeException;
use OCA\Circles\Tools\Exceptions\RequestServerException;
use OCA\Circles\Tools\Model\NCRequest;
use OCA\Circles\Tools\Model\Request;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TNCRequest;
use Exception;
use OCA\Circles\Db\DeprecatedCirclesRequest;
use OCA\Circles\Db\DeprecatedMembersRequest;
use OCA\Circles\Db\EventWrapperRequest;
use OCA\Circles\Exceptions\GlobalScaleEventException;
use OCA\Circles\Exceptions\GSStatusException;
use OCA\Circles\Exceptions\JsonException;
use OCA\Circles\Exceptions\ModelException;
use OCA\Circles\GlobalScale\CircleStatus;
use OCA\Circles\Model\DeprecatedCircle;
use OCA\Circles\Model\GlobalScale\GSEvent;
use OCA\Circles\Model\GlobalScale\GSWrapper;
use OCP\IURLGenerator;
/**
* Class GSUpstreamService
*
* @package OCA\Circles\Service
*/
class GSUpstreamService {
use TNCRequest;
/** @var string */
private $userId = '';
/** @var IURLGenerator */
private $urlGenerator;
/** @var EventWrapperRequest */
private $eventWrapperRequest;
/** @var DeprecatedCirclesRequest */
private $circlesRequest;
/** @var DeprecatedMembersRequest */
private $membersRequest;
/** @var GlobalScaleService */
private $globalScaleService;
/** @var ConfigService */
private $configService;
/** @var MiscService */
private $miscService;
/**
* GSUpstreamService constructor.
*
* @param $userId
* @param IURLGenerator $urlGenerator
* @param EventWrapperRequest $eventWrapperRequest
* @param DeprecatedCirclesRequest $circlesRequest
* @param DeprecatedMembersRequest $membersRequest
* @param GlobalScaleService $globalScaleService
* @param ConfigService $configService
* @param MiscService $miscService
*/
public function __construct(
$userId,
IURLGenerator $urlGenerator,
EventWrapperRequest $eventWrapperRequest,
DeprecatedCirclesRequest $circlesRequest,
DeprecatedMembersRequest $membersRequest,
GlobalScaleService $globalScaleService,
ConfigService $configService,
MiscService $miscService
) {
$this->userId = $userId;
$this->urlGenerator = $urlGenerator;
$this->eventWrapperRequest = $eventWrapperRequest;
$this->circlesRequest = $circlesRequest;
$this->membersRequest = $membersRequest;
$this->globalScaleService = $globalScaleService;
$this->configService = $configService;
$this->miscService = $miscService;
}
/**
* @param GSEvent $event
*
* @return string
* @throws Exception
*/
public function newEvent(GSEvent $event): string {
$event->setSource($this->configService->getFrontalInstance());
try {
$gs = $this->globalScaleService->getGlobalScaleEvent($event);
if ($this->isLocalEvent($event)) {
$gs->verify($event, true);
if (!$event->isAsync()) {
$gs->manage($event);
}
return $this->globalScaleService->asyncBroadcast($event);
} else {
$this->confirmEvent($event);
return '';
}
} catch (Exception $e) {
$this->miscService->log(
get_class($e) . ' on new event: ' . $e->getMessage() . ' - ' . json_encode($event), 1
);
throw $e;
}
}
/**
* @param GSWrapper $wrapper
*/
public function broadcastWrapper(GSWrapper $wrapper): void {
$status = GSWrapper::STATUS_FAILED;
try {
$this->broadcastEvent($wrapper->getEvent(), $wrapper->getInstance());
$status = GSWrapper::STATUS_DONE;
} catch (RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException | RequestResultNotJsonException $e) {
}
if ($wrapper->getSeverity() === GSEvent::SEVERITY_HIGH) {
$wrapper->setStatus($status);
} else {
$wrapper->setStatus(GSWrapper::STATUS_OVER);
}
$this->eventWrapperRequest->update($wrapper);
}
/**
* @param GSEvent $event
* @param string $instance
* @param string $protocol
*
* @throws RequestContentException
* @throws RequestNetworkException
* @throws RequestResultNotJsonException
* @throws RequestResultSizeException
* @throws RequestServerException
*/
public function broadcastEvent(GSEvent $event, string $instance): void {
$this->signEvent($event);
if ($this->configService->isLocalInstance($instance)) {
$request = new NCRequest('', Request::TYPE_POST);
$this->configService->configureRequest($request, 'circles.EventWrapper.broadcast');
} else {
$path = $this->urlGenerator->linkToRoute('circles.EventWrapper.broadcast');
$request = new NCRequest($path, Request::TYPE_POST);
$this->configService->configureRequest($request);
$request->setInstance($instance);
}
$request->setDataSerialize($event);
$data = $this->retrieveJson($request);
$event->setResult(new SimpleDataStore($this->getArray('result', $data, [])));
}
/**
* @param GSEvent $event
*
* @throws RequestContentException
* @throws RequestNetworkException
* @throws RequestResultSizeException
* @throws RequestServerException
* @throws RequestResultNotJsonException
* @throws GlobalScaleEventException
*/
public function confirmEvent(GSEvent &$event): void {
$this->signEvent($event);
$circle = $event->getDeprecatedCircle();
$owner = $circle->getOwner();
$path = $this->urlGenerator->linkToRoute('circles.EventWrapper.event');
$request = new NCRequest($path, Request::TYPE_POST);
$this->configService->configureRequest($request);
$request->basedOnUrl($owner->getInstance());
$request->setDataSerialize($event);
$result = $this->retrieveJson($request);
$this->miscService->log('result ' . json_encode($result), 0);
if ($this->getInt('status', $result) === 0) {
throw new GlobalScaleEventException($this->get('error', $result));
}
$updatedData = $this->getArray('event', $result);
$this->miscService->log('updatedEvent: ' . json_encode($updatedData), 0);
if (!empty($updatedData)) {
$updated = new GSEvent();
try {
$updated->import($updatedData);
$event = $updated;
} catch (Exception $e) {
}
}
}
/**
* @param GSEvent $event
*/
private function signEvent(GSEvent $event) {
$event->setKey($this->globalScaleService->getKey());
}
/**
* We check that the event can be managed/checked locally or if the owner of the circle belongs to
* an other instance of Nextcloud
*
* @param GSEvent $event
*
* @return bool
*/
private function isLocalEvent(GSEvent $event): bool {
if ($event->isLocal()) {
return true;
}
$circle = $event->getDeprecatedCircle();
$owner = $circle->getOwner();
if ($owner->getInstance() === ''
|| in_array($owner->getInstance(), $this->configService->getTrustedDomains())) {
return true;
}
return false;
}
/**
* @param string $token
*
* @return GSWrapper[]
* @throws JsonException
* @throws ModelException
*/
public function getEventsByToken(string $token): array {
return $this->eventWrapperRequest->getByToken($token);
}
/**
* should be used to manage results from events, like sending mails on user creation
*
* @param string $token
*/
public function manageResults(string $token): void {
try {
$wrappers = $this->eventWrapperRequest->getByToken($token);
} catch (JsonException | ModelException $e) {
return;
}
$event = null;
$events = [];
foreach ($wrappers as $wrapper) {
if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
return;
}
$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
}
if ($event === null) {
return;
}
try {
$gs = $this->globalScaleService->getGlobalScaleEvent($event);
$gs->result($events);
} catch (GlobalScaleEventException $e) {
}
}
/**
*
*/
private function removeDeprecatedCircles() {
$knownCircles = $this->circlesRequest->forceGetCircles();
foreach ($knownCircles as $knownItem) {
if ($knownItem->getOwner()
->getInstance() === '') {
continue;
}
try {
$this->checkCircle($knownItem);
} catch (GSStatusException $e) {
}
}
}
/**
* @param DeprecatedCircle $circle
*
* @throws GSStatusException
*/
private function checkCircle(DeprecatedCircle $circle): void {
$status = $this->confirmCircleStatus($circle);
if (!$status) {
$this->circlesRequest->destroyCircle($circle->getUniqueId());
$this->membersRequest->removeAllFromCircle($circle->getUniqueId());
}
}
/**
* @param DeprecatedCircle $circle
*
* @return bool
* @throws GSStatusException
*/
public function confirmCircleStatus(DeprecatedCircle $circle): bool {
$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
$event->setSource($this->configService->getFrontalInstance());
$event->setDeprecatedCircle($circle);
$this->signEvent($event);
$path = $this->urlGenerator->linkToRoute('circles.EventWrapper.status');
$request = new NCRequest($path, Request::TYPE_POST);
$this->configService->configureRequest($request);
$request->setDataSerialize($event);
$requestIssue = false;
$notFound = false;
$foundWithNoOwner = false;
foreach ($this->globalScaleService->getInstances() as $instance) {
$request->setHost($instance);
try {
$result = $this->retrieveJson($request);
// $this->miscService->log('result: ' . json_encode($result));
if ($this->getInt('status', $result, 0) !== 1) {
throw new RequestContentException('result status is not good');
}
$status = $this->getInt('success.data.status', $result);
// if error, we assume the circle might still exist.
if ($status === CircleStatus::STATUS_ERROR) {
return true;
}
if ($status === CircleStatus::STATUS_OK) {
return true;
}
// TODO: check the data.supposedOwner entry.
if ($status === CircleStatus::STATUS_NOT_OWNER) {
$foundWithNoOwner = true;
}
if ($status === CircleStatus::STATUS_NOT_FOUND) {
$notFound = true;
}
} catch (RequestContentException
| RequestNetworkException
| RequestResultNotJsonException
| RequestResultSizeException
| RequestServerException $e) {
$requestIssue = true;
// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
continue;
}
}
// if no request issue, we can imagine that the instance that owns the circle is down.
// We'll wait for more information (cf request exceptions management);
if ($requestIssue) {
return true;
}
// circle were not found in any other instances, we can easily says that the circle does not exists anymore
if ($notFound && !$foundWithNoOwner) {
return false;
}
// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
if (!$notFound && $foundWithNoOwner) {
// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
return true;
}
// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
return false;
}
/**
* @throws GSStatusException
*/
public function syncEvents() {
}
/**
*
*/
private function removeDeprecatedEvents() {
// $this->deprecatedEvents();
}
}
@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2019
* @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\Circles\Service;
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 OC\Security\IdentityProof\Signer;
use OCA\Circles\Db\EventWrapperRequest;
use OCA\Circles\Exceptions\GSStatusException;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
/**
* Class GlobalScaleService
*
* @package OCA\Circles\Service
*/
class GlobalScaleService {
use TNCRequest;
use TStringTools;
/** @var IURLGenerator */
private $urlGenerator;
/** @var IUserManager */
private $userManager;
/** @var IUserSession */
private $userSession;
/** @var Signer */
private $signer;
/** @var EventWrapperRequest */
private $eventWrapperRequest;
/** @var ConfigService */
private $configService;
/** @var MiscService */
private $miscService;
/**
* GlobalScaleService constructor.
*
* @param IURLGenerator $urlGenerator
* @param IUserManager $userManager
* @param IUserSession $userSession
* @param Signer $signer
* @param EventWrapperRequest $eventWrapperRequest
* @param ConfigService $configService
* @param MiscService $miscService
*/
public function __construct(
IURLGenerator $urlGenerator,
IUserManager $userManager,
IUserSession $userSession,
Signer $signer,
EventWrapperRequest $eventWrapperRequest,
ConfigService $configService,
MiscService $miscService
) {
$this->urlGenerator = $urlGenerator;
$this->userManager = $userManager;
$this->userSession = $userSession;
$this->signer = $signer;
$this->eventWrapperRequest = $eventWrapperRequest;
$this->configService = $configService;
$this->miscService = $miscService;
}
/**
* @return array
*/
public function getGlobalScaleInstances(): array {
$mockup = $this->configService->getGSSMockup();
if (!empty($mockup)) {
return $mockup;
}
try {
$lookup = $this->configService->getGSLookup();
$request = new NCRequest(ConfigService::GS_LOOKUP_INSTANCES, Request::TYPE_POST);
$this->configService->configureRequest($request);
$request->basedOnUrl($lookup);
$request->addData('authKey', $this->configService->getGSInfo(ConfigService::GS_KEY));
try {
return $this->retrieveJson($request);
} catch (RequestNetworkException $e) {
$this->e($e);
}
} catch (GSStatusException $e) {
}
return [];
}
// /**
// * @param GSEvent $event
// *
// * @return string
// */
// public function asyncBroadcast(GSEvent $event): string {
// $wrapper = new GSWrapper();
// $wrapper->setEvent($event);
// $wrapper->setToken($this->uuid());
// $wrapper->setCreation(time());
// $wrapper->setSeverity($event->getSeverity());
//
// foreach ($this->getInstances($event->isAsync()) as $instance) {
// $wrapper->setInstance($instance);
// $wrapper = $this->eventWrapperRequest->create($wrapper);
// }
//
// $request = new NC22Request('', Request::TYPE_POST);
// $this->configService->configureRequest(
// $request, 'circles.EventWrapper.asyncBroadcast', ['token' => $wrapper->getToken()]
// );
//
// try {
// $this->doRequest($request);
// } catch (RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException $e) {
// $this->miscService->e($e);
// }
//
// return $wrapper->getToken();
// }
//
//
// /**
// * @param GSEvent $event
// *
// * @return AGlobalScaleEvent
// * @throws GlobalScaleEventException
// */
// public function getGlobalScaleEvent(GSEvent $event): AGlobalScaleEvent {
// $class = $this->getClassNameFromEvent($event);
// try {
// $gs = OC::$server->query($class);
// if (!$gs instanceof AGlobalScaleEvent) {
// throw new GlobalScaleEventException($class . ' not an AGlobalScaleEvent');
// }
//
// return $gs;
// } catch (QueryException $e) {
// throw new GlobalScaleEventException('AGlobalScaleEvent ' . $class . ' not found');
// }
// }
//
//
// /**
// * @return string
// */
// public function getKey(): string {
// try {
// $key = $this->configService->getGSStatus(ConfigService::GS_KEY);
// } catch (GSStatusException $e) {
// $key = $this->configService->getAppValue(ConfigService::CIRCLES_LOCAL_GSKEY);
// if ($key === '') {
// $key = $this->token(31);
// $this->configService->setAppValue(ConfigService::CIRCLES_LOCAL_GSKEY, $key);
// }
// }
//
// return md5('gskey:' . $key);
// }
//
// /**
// * @param string $key
// *
// * @throws GSKeyException
// */
// public function checkKey(string $key) {
// if ($key !== $this->getKey()) {
// throw new GSKeyException('invalid key');
// }
// }
//
//
// /**
// * @param GSEvent $event
// *
// * @throws GSKeyException
// */
// public function checkEvent(GSEvent $event): void {
// $this->checkKey($event->getKey());
// }
//
//
// /**
// * @param GSEvent $event
// *
// * @return string
// * @throws GlobalScaleEventException
// */
// private function getClassNameFromEvent(GSEvent $event): string {
// $className = $event->getType();
// if (substr($className, 0, 25) !== '\OCA\Circles\GlobalScale\\' || strpos($className, '.')) {
// throw new GlobalScaleEventException(
// $className . ' does not seems to be a secured AGlobalScaleEvent'
// );
// }
//
// return $className;
// }
//
//
// /**
// * @return IUser
// * @throws NoUserException
// */
// private function getRandomUser(): IUser {
// $user = $this->userSession->getUser();
// if ($user !== null) {
// return $user;
// }
//
// $random = $this->userManager->search('', 1);
// if (sizeof($random) > 0) {
// return array_shift($random);
// }
//
// throw new NoUserException();
// }
}
@@ -0,0 +1,303 @@
<?php
/**
* Circles - bring cloud-users closer
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\Db\DeprecatedCirclesRequest;
use OCA\Circles\Db\DeprecatedMembersRequest;
use OCA\Circles\Exceptions\CircleTypeNotValidException;
use OCA\Circles\Exceptions\GroupCannotBeOwnerException;
use OCA\Circles\Exceptions\GroupDoesNotExistException;
use OCA\Circles\Exceptions\MemberAlreadyExistsException;
use OCA\Circles\Exceptions\MemberDoesNotExistException;
use OCA\Circles\Model\DeprecatedCircle;
use OCA\Circles\Model\DeprecatedMember;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUserManager;
/**
* Class GroupsService
* @deprecated
* @package OCA\Circles\Service
*/
class GroupsService {
/** @var string */
private $userId;
/** @var IL10N */
private $l10n;
/** @var IGroupManager */
private $groupManager;
/** @var IUserManager */
private $userManager;
/** @var ConfigService */
private $configService;
/** @var DeprecatedCirclesRequest */
private $circlesRequest;
/** @var DeprecatedMembersRequest */
private $membersRequest;
/** @var CirclesService */
private $circlesService;
/** @var EventsService */
private $eventsService;
/** @var MiscService */
private $miscService;
/**
* GroupsService constructor.
*
* @param string $userId
* @param IL10N $l10n
* @param IGroupManager $groupManager
* @param IUserManager $userManager
* @param ConfigService $configService
* @param DeprecatedCirclesRequest $circlesRequest
* @param DeprecatedMembersRequest $membersRequest
* @param CirclesService $circlesService
* @param EventsService $eventsService
* @param MiscService $miscService
*/
public function __construct(
$userId, IL10N $l10n, IGroupManager $groupManager, IUserManager $userManager,
ConfigService $configService, DeprecatedCirclesRequest $circlesRequest,
DeprecatedMembersRequest $membersRequest, CirclesService $circlesService,
EventsService $eventsService, MiscService $miscService
) {
$this->userId = $userId;
$this->l10n = $l10n;
$this->groupManager = $groupManager;
$this->userManager = $userManager;
$this->configService = $configService;
$this->circlesRequest = $circlesRequest;
$this->membersRequest = $membersRequest;
$this->circlesService = $circlesService;
$this->eventsService = $eventsService;
$this->miscService = $miscService;
}
/**
* @param string $circleUniqueId
* @param string $groupId
*
* @return array
* @throws \Exception
*/
public function linkGroup($circleUniqueId, $groupId) {
try {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
$this->circlesService->hasToBeAdmin($circle->getHigherViewer());
$allMembers =
$this->membersRequest->forceGetMembers($circleUniqueId, DeprecatedMember::LEVEL_MEMBER, 0, true);
$group = $this->groupManager->get($groupId);
$count = $group->count();
foreach ($allMembers as $member) {
if ($member->getType() !== DeprecatedMember::TYPE_USER) {
continue;
}
$user = $this->userManager->get($member->getUserId());
if ($group->inGroup($user)) {
continue;
}
$count++;
}
$limit = (int)$circle->getSetting('members_limit');
if ($limit === 0) {
$limit = $this->configService->getAppValue(ConfigService::MEMBERS_LIMIT);
}
if ($limit !== -1 && $count > $limit) {
throw new \Exception($this->l10n->t('Group contains too many members'));
}
$group = $this->getFreshNewMember($circleUniqueId, $groupId);
} catch (\Exception $e) {
throw $e;
}
$group->setLevel(DeprecatedMember::LEVEL_MEMBER);
$this->membersRequest->updateGroup($group);
$this->eventsService->onGroupLink($circle, $group);
return $this->membersRequest->getGroupsFromCircle(
$circleUniqueId, $circle->getHigherViewer()
);
}
/**
* Check if a fresh member can be generated (by linkGroup)
*
* @param $circleId
* @param $groupId
*
* @return null|DeprecatedMember
* @throws MemberAlreadyExistsException
* @throws GroupDoesNotExistException
*/
private function getFreshNewMember($circleId, $groupId) {
if (!$this->groupManager->groupExists($groupId)) {
throw new GroupDoesNotExistException($this->l10n->t("This group does not exist"));
}
try {
$instance = ''; // TODO: group are not used in GS yet.
$member = $this->membersRequest->forceGetGroup($circleId, $groupId, $instance);
} catch (MemberDoesNotExistException $e) {
$member = new DeprecatedMember($groupId, DeprecatedMember::TYPE_GROUP, $circleId);
$this->membersRequest->createMember($member);
// $this->membersRequest->insertGroup($member);
}
if ($member->getLevel() > DeprecatedMember::LEVEL_NONE) {
throw new MemberAlreadyExistsException(
$this->l10n->t('This group is already linked to the circle')
);
}
return $member;
}
/**
* @param string $circleUniqueId
* @param string $groupId
* @param int $level
*
* @return array
* @throws \Exception
*/
public function levelGroup($circleUniqueId, $groupId, $level) {
$level = (int)$level;
try {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
if ($circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL) {
throw new CircleTypeNotValidException(
$this->l10n->t('You cannot edit level in a personal circle')
);
}
$instance = ''; // TODO: group are not used in GS yet.
$group = $this->membersRequest->forceGetGroup($circle->getUniqueId(), $groupId, $instance);
if ($group->getLevel() !== $level) {
if ($level === DeprecatedMember::LEVEL_OWNER) {
throw new GroupCannotBeOwnerException(
$this->l10n->t('Group cannot be set as owner of a circle')
);
} else {
$this->editGroupLevel($circle, $group, $level);
}
$this->eventsService->onGroupLevel($circle, $group);
}
return $this->membersRequest->getGroupsFromCircle(
$circle->getUniqueId(), $circle->getHigherViewer()
);
} catch (\Exception $e) {
throw $e;
}
}
/**
* @param DeprecatedCircle $circle
* @param DeprecatedMember $group
* @param $level
*
* @throws \Exception
*/
private function editGroupLevel(DeprecatedCircle $circle, DeprecatedMember $group, $level) {
try {
$isMod = $circle->getHigherViewer();
$this->circlesService->hasToBeAdmin($isMod);
$isMod->hasToBeHigherLevel($level);
$group->hasToBeMember();
$group->cantBeOwner();
$isMod->hasToBeHigherLevel($group->getLevel());
$group->setLevel($level);
$this->membersRequest->updateGroup($group);
} catch (\Exception $e) {
throw $e;
}
}
/**
* @param string $circleUniqueId
* @param string $groupId
*
* @return array
* @throws \Exception
*/
public function unlinkGroup($circleUniqueId, $groupId) {
try {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
$higherViewer = $circle->getHigherViewer();
$this->circlesService->hasToBeAdmin($higherViewer);
$instance = ''; // TODO: group are not used in GS yet.
$group = $this->membersRequest->forceGetGroup($circleUniqueId, $groupId, $instance);
$group->cantBeOwner();
$higherViewer->hasToBeHigherLevel($group->getLevel());
$this->membersRequest->removeMember($group);
$this->eventsService->onGroupUnlink($circle, $group);
return $this->membersRequest->getGroupsFromCircle($circleUniqueId, $higherViewer);
} catch (\Exception $e) {
throw $e;
}
}
/**
* When a group is removed, remove it from all Circles
*
* @param string $groupId
*/
public function onGroupRemoved($groupId) {
$this->membersRequest->unlinkAllFromGroup($groupId);
}
}
@@ -0,0 +1,516 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCA\Circles\Tools\Traits\TStringTools;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\RemoteRequest;
use OCA\Circles\Exceptions\RemoteNotFoundException;
use OCA\Circles\Exceptions\UnknownInterfaceException;
use OCP\IRequest;
use OCP\IURLGenerator;
/**
* Class InterfaceService
*
* @package OCA\Circles\Service
*/
class InterfaceService {
public const IFACE0 = 1;
public const IFACE1 = 2;
public const IFACE2 = 3;
public const IFACE3 = 4;
public const IFACE4 = 5;
public const IFACE_INTERNAL = 6;
public const IFACE_FRONTAL = 7;
public const IFACE_TEST = 99;
public static $LIST_IFACE = [
self::IFACE_INTERNAL => 'internal',
self::IFACE_FRONTAL => 'frontal',
self::IFACE0 => 'iface0',
self::IFACE1 => 'iface1',
self::IFACE2 => 'iface2',
self::IFACE3 => 'iface3',
self::IFACE4 => 'iface4',
];
use TStringTools;
use TArrayTools;
use TNCLogger;
/** @var IURLGenerator */
private $urlGenerator;
/** @var RemoteRequest */
private $remoteRequest;
/** @var ConfigService */
private $configService;
/** @var int */
private $currentInterface = 0;
/** @var int */
private $outgoingInterface = 0;
/**
* InterfaceService constructor.
*
* @param IURLGenerator $urlGenerator
* @param RemoteRequest $remoteRequest
* @param ConfigService $configService
*/
public function __construct(
IURLGenerator $urlGenerator,
RemoteRequest $remoteRequest,
ConfigService $configService
) {
$this->urlGenerator = $urlGenerator;
$this->remoteRequest = $remoteRequest;
$this->configService = $configService;
$this->setup('app', Application::APP_ID);
}
/**
* @param int $interface
*/
public function setCurrentInterface(int $interface): void {
$this->currentInterface = $interface;
}
/**
* @return int
* @throws UnknownInterfaceException
*/
public function getCurrentInterface(): int {
if ($this->currentInterface === 0) {
throw new UnknownInterfaceException('interface not initialized');
}
return $this->currentInterface;
}
/**
* @return bool
*/
public function hasCurrentInterface(): bool {
return ($this->currentInterface !== 0);
}
/**
* @return bool
* @throws UnknownInterfaceException
*/
public function isCurrentInterfaceInternal(): bool {
return $this->isInterfaceInternal($this->getCurrentInterface());
}
/**
* @param int $interface
*
* @return bool
*/
public function isInterfaceInternal(int $interface): bool {
if ($interface === self::IFACE_INTERNAL) {
return true;
}
switch ($interface) {
case self::IFACE0:
return $this->configService->getAppValueBool(ConfigService::IFACE0_INTERNAL);
case self::IFACE1:
return $this->configService->getAppValueBool(ConfigService::IFACE1_INTERNAL);
case self::IFACE2:
return $this->configService->getAppValueBool(ConfigService::IFACE2_INTERNAL);
case self::IFACE3:
return $this->configService->getAppValueBool(ConfigService::IFACE3_INTERNAL);
case self::IFACE4:
return $this->configService->getAppValueBool(ConfigService::IFACE4_INTERNAL);
}
return false;
}
/**
* @param IRequest $request
* @param string $testToken
*/
public function setCurrentInterfaceFromRequest(IRequest $request, string $testToken = ''): void {
$testing = [
self::IFACE_INTERNAL => $this->configService->getInternalInstance(),
self::IFACE_FRONTAL => $this->configService->getFrontalInstance(),
self::IFACE0 => $this->configService->getIfaceInstance(self::IFACE0),
self::IFACE1 => $this->configService->getIfaceInstance(self::IFACE1),
self::IFACE2 => $this->configService->getIfaceInstance(self::IFACE2),
self::IFACE3 => $this->configService->getIfaceInstance(self::IFACE3),
self::IFACE4 => $this->configService->getIfaceInstance(self::IFACE4),
];
if ($testToken !== ''
&& $testToken === $this->configService->getAppValue(ConfigService::IFACE_TEST_TOKEN)) {
$testing[self::IFACE_TEST] = $this->getTestingInstance();
}
$serverHost = strtolower($request->getServerHost());
if ($serverHost === '') {
return;
}
foreach ($testing as $iface => $instance) {
if ($serverHost === strtolower($instance)) {
$this->setCurrentInterface($iface);
return;
}
}
}
/**
* @param string $instance
*
* @return int
* @throws RemoteNotFoundException
*/
public function getInterfaceFromInstance(string $instance): int {
$remoteInstance = $this->remoteRequest->getFromInstance($instance);
return $remoteInstance->getInterface();
}
/**
*
*/
public function setCurrentInterfaceFromInstance(string $instance): void {
try {
$this->setCurrentInterface($this->getInterfaceFromInstance($instance));
} catch (RemoteNotFoundException $e) {
}
}
/**
* @param int $interface
*
* @return bool
*/
public function isInterfaceConfigured(int $interface): bool {
try {
$config = $this->getCloudIdConfigKey($interface);
} catch (UnknownInterfaceException $e) {
return false;
}
return ($this->configService->getAppValue($config) !== '');
}
/**
* @param int $interface
*
* @return string
* @throws UnknownInterfaceException
*/
private function getCloudIdConfigKey(int $interface): string {
switch ($interface) {
case self::IFACE_INTERNAL:
return ConfigService::INTERNAL_CLOUD_ID;
case self::IFACE_FRONTAL:
return ConfigService::FRONTAL_CLOUD_ID;
case self::IFACE0:
return ConfigService::IFACE0_CLOUD_ID;
case self::IFACE1:
return ConfigService::IFACE1_CLOUD_ID;
case self::IFACE2:
return ConfigService::IFACE2_CLOUD_ID;
case self::IFACE3:
return ConfigService::IFACE3_CLOUD_ID;
case self::IFACE4:
return ConfigService::IFACE4_CLOUD_ID;
}
throw new UnknownInterfaceException('unknown interface');
}
/**
* @param bool $useString
*
* @return array
*/
public function getInterfaces(bool $useString = false): array {
$interfaces = [
self::IFACE_INTERNAL => $this->configService->getInternalInstance(),
self::IFACE_FRONTAL => $this->configService->getFrontalInstance(),
self::IFACE0 => $this->configService->getIfaceInstance(InterfaceService::IFACE0),
self::IFACE1 => $this->configService->getIfaceInstance(InterfaceService::IFACE1),
self::IFACE2 => $this->configService->getIfaceInstance(InterfaceService::IFACE2),
self::IFACE3 => $this->configService->getIfaceInstance(InterfaceService::IFACE3),
self::IFACE4 => $this->configService->getIfaceInstance(InterfaceService::IFACE4)
];
if (!$useString) {
return $interfaces;
}
$detailed = [];
foreach ($interfaces as $id => $iface) {
$detailed[self::$LIST_IFACE[$id]] = $iface;
}
return $detailed;
}
/**
* @param bool $useString
*
* @return array
*/
public function getInternalInterfaces(bool $useString = false): array {
$interfaces = $this->getInterfaces(false);
$internalInterfaces = [];
foreach ($interfaces as $id => $iface) {
if (!$this->isInterfaceInternal($id)) {
continue;
}
$internalInterfaces[$id] = $iface;
}
if (!$useString) {
return $internalInterfaces;
}
$detailed = [];
foreach ($internalInterfaces as $id => $iface) {
$detailed[self::$LIST_IFACE[$id]] = $iface;
}
return $detailed;
}
/**
* use this only if interface must be defined. If not, use getLocalInstance()
*
* @throws UnknownInterfaceException
*/
public function getCloudInstance(int $interface = 0): string {
if ($interface === 0) {
$interface = $this->getCurrentInterface();
}
switch ($interface) {
case self::IFACE_INTERNAL:
return $this->configService->getInternalInstance();
case self::IFACE_FRONTAL:
return $this->configService->getFrontalInstance();
case self::IFACE0:
case self::IFACE1:
case self::IFACE2:
case self::IFACE3:
case self::IFACE4:
return $this->configService->getIfaceInstance($interface);
case self::IFACE_TEST:
return $this->getTestingInstance();
}
throw new UnknownInterfaceException('unknown configured interface');
}
/**
* @throws UnknownInterfaceException
*/
public function getCloudPath(string $route = '', array $args = [], int $interface = 0): string {
if ($interface === 0) {
$interface = $this->getCurrentInterface();
}
$scheme = $path = '';
switch ($interface) {
case self::IFACE_INTERNAL:
$scheme = $this->configService->getAppValue(ConfigService::INTERNAL_CLOUD_SCHEME);
$path = $this->configService->getAppValue(ConfigService::INTERNAL_CLOUD_PATH);
break;
case self::IFACE_FRONTAL:
$scheme = $this->configService->getAppValue(ConfigService::FRONTAL_CLOUD_SCHEME);
$path = $this->configService->getAppValue(ConfigService::FRONTAL_CLOUD_PATH);
break;
case self::IFACE0:
$scheme = $this->configService->getAppValue(ConfigService::IFACE0_CLOUD_SCHEME);
$path = $this->configService->getAppValue(ConfigService::IFACE0_CLOUD_PATH);
break;
case self::IFACE1:
$scheme = $this->configService->getAppValue(ConfigService::IFACE1_CLOUD_SCHEME);
$path = $this->configService->getAppValue(ConfigService::IFACE1_CLOUD_PATH);
break;
case self::IFACE2:
$scheme = $this->configService->getAppValue(ConfigService::IFACE2_CLOUD_SCHEME);
$path = $this->configService->getAppValue(ConfigService::IFACE2_CLOUD_PATH);
break;
case self::IFACE3:
$scheme = $this->configService->getAppValue(ConfigService::IFACE3_CLOUD_SCHEME);
$path = $this->configService->getAppValue(ConfigService::IFACE3_CLOUD_PATH);
break;
case self::IFACE4:
$scheme = $this->configService->getAppValue(ConfigService::IFACE4_CLOUD_SCHEME);
$path = $this->configService->getAppValue(ConfigService::IFACE4_CLOUD_PATH);
break;
case self::IFACE_TEST:
$scheme = $this->configService->getAppValue(ConfigService::IFACE_TEST_SCHEME);
$path = $this->configService->getAppValue(ConfigService::IFACE_TEST_PATH);
break;
}
if ($scheme === '') {
throw new UnknownInterfaceException('misconfigured scheme');
}
$base = $scheme . '://' . $this->getCloudInstance($interface) . $path;
if ($route === '') {
return $base;
}
return $base . $this->configService->linkToRoute($route, $args);
}
/**
* should be used when unsure about the used Interface
*
* @return string
*/
public function getLocalInstance(): string {
if ($this->hasCurrentInterface()) {
try {
return $this->getCloudInstance();
} catch (UnknownInterfaceException $e) {
}
}
return $this->configService->getLoopbackInstance();
}
/**
* @param string $route
* @param array $args
*
* @return string
*/
public function getLocalPath(string $route, array $args): string {
$base = $this->configService->getAppValue(ConfigService::FRONTAL_CLOUD_BASE);
if ($base === '') {
return $this->configService->getLoopbackPath($route, $args);
}
return rtrim($base, '/') . $this->configService->linkToRoute($route, $args);
}
/**
* should be used when trying to generate an address
*
* @param string $route
* @param array $args
*
* @return string
*/
public function getFrontalPath(string $route, array $args): string {
$frontalBase = $this->configService->getAppValue(ConfigService::FRONTAL_CLOUD_BASE);
if ($frontalBase !== '') {
return $this->getLocalPath($route, $args);
}
if ($this->isInterfaceConfigured(self::IFACE_FRONTAL)) {
try {
return $this->getCloudPath($route, $args, self::IFACE_FRONTAL);
} catch (UnknownInterfaceException $e) {
}
}
$ifaces = [self::IFACE0, self::IFACE1, self::IFACE2, self::IFACE3, self::IFACE4];
foreach ($ifaces as $iface) {
if ($this->isInterfaceConfigured($iface) && !$this->isInterfaceInternal($iface)) {
try {
return $this->getCloudPath($route, $args, $iface);
} catch (UnknownInterfaceException $e) {
}
}
}
if ($this->isInterfaceConfigured(self::IFACE_INTERNAL)) {
try {
return $this->getCloudPath($route, $args, self::IFACE_INTERNAL);
} catch (UnknownInterfaceException $e) {
}
}
foreach ($ifaces as $iface) {
if ($this->isInterfaceConfigured($iface) && $this->isInterfaceInternal($iface)) {
try {
return $this->getCloudPath($route, $args, $iface);
} catch (UnknownInterfaceException $e) {
}
}
}
try {
return $this->getCloudPath($route, $args);
} catch (UnknownInterfaceException $e) {
}
return $this->getLocalPath($route, $args);
}
/**
* @return string
*/
private function getTestingInstance(): string {
return $this->configService->getAppValue(ConfigService::IFACE_TEST_ID);
}
}
@@ -0,0 +1,505 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use Exception;
use OCA\Circles\Db\AccountsRequest;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Db\ShareWrapperRequest;
use OCA\Circles\Exceptions\InitiatorNotFoundException;
use OCA\Circles\Exceptions\MaintenanceException;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\ShareWrapper;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCP\IGroupManager;
use OCP\IUserManager;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class MaintenanceService
*
* @package OCA\Circles\Service
*/
class MaintenanceService {
use TNCLogger;
public const TIMEOUT = 18000;
public static $DELAY =
[
1 => 60, // every minute
2 => 300, // every 5 minutes
3 => 3600, // every hour
4 => 75400, // every day
5 => 432000 // evey week
];
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var AccountsRequest */
private $accountRequest;
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var ShareWrapperRequest */
private $shareWrapperRequest;
/** @var SyncService */
private $syncService;
/** @var FederatedUserService */
private $federatedUserService;
private ShareWrapperService $shareWrapperService;
/** @var MembershipService */
private $membershipService;
/** @var EventWrapperService */
private $eventWrapperService;
/** @var CircleService */
private $circleService;
/** @var ConfigService */
private $configService;
/** @var OutputInterface */
private $output;
/**
* MaintenanceService constructor.
*
* @param IUserManager $userManager
* @param IGroupManager $groupManager
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param ShareWrapperRequest $shareWrapperRequest
* @param SyncService $syncService
* @param FederatedUserService $federatedUserService
* @param ShareWrapperService $shareWrapperService
* @param MembershipService $membershipService
* @param EventWrapperService $eventWrapperService
* @param CircleService $circleService
* @param ConfigService $configService
*/
public function __construct(
IUserManager $userManager,
IGroupManager $groupManager,
CircleRequest $circleRequest,
AccountsRequest $accountRequest,
MemberRequest $memberRequest,
ShareWrapperRequest $shareWrapperRequest,
SyncService $syncService,
FederatedUserService $federatedUserService,
ShareWrapperService $shareWrapperService,
MembershipService $membershipService,
EventWrapperService $eventWrapperService,
CircleService $circleService,
ConfigService $configService
) {
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->circleRequest = $circleRequest;
$this->accountRequest = $accountRequest;
$this->memberRequest = $memberRequest;
$this->shareWrapperRequest = $shareWrapperRequest;
$this->syncService = $syncService;
$this->federatedUserService = $federatedUserService;
$this->shareWrapperService = $shareWrapperService;
$this->eventWrapperService = $eventWrapperService;
$this->membershipService = $membershipService;
$this->circleService = $circleService;
$this->configService = $configService;
}
/**
* @param OutputInterface $output
*/
public function setOccOutput(OutputInterface $output): void {
$this->output = $output;
}
/**
* level=1 -> run every minute
* level=2 -> run every 5 minutes
* level=3 -> run every hour
* level=4 -> run every day
* level=5 -> run every week
*
* @param int $level
*
* @throws MaintenanceException
*/
public function runMaintenance(int $level, bool $forceRefresh = false): void {
$this->federatedUserService->bypassCurrentUserCondition(true);
$this->lockMaintenanceRun();
$this->debug('running maintenance (' . $level . ')');
switch ($level) {
case 1:
$this->runMaintenance1($forceRefresh);
break;
case 2:
$this->runMaintenance2($forceRefresh);
break;
case 3:
$this->runMaintenance3($forceRefresh);
break;
case 4:
$this->runMaintenance4($forceRefresh);
break;
case 5:
$this->runMaintenance5($forceRefresh);
break;
}
$this->configService->setAppValue(ConfigService::MAINTENANCE_RUN, '0');
}
/**
* @throws MaintenanceException
*/
private function lockMaintenanceRun(): void {
$run = $this->configService->getAppValueInt(ConfigService::MAINTENANCE_RUN);
if ($run > time() - self::TIMEOUT) {
throw new MaintenanceException('maintenance already running');
}
$this->configService->setAppValue(ConfigService::MAINTENANCE_RUN, (string)time());
}
/**
* every minute
*/
private function runMaintenance1(bool $forceRefresh = false): void {
try {
$this->output('Remove circles with no owner');
$this->removeCirclesWithNoOwner();
} catch (Exception $e) {
}
}
/**
* every 10 minutes
*/
private function runMaintenance2(bool $forceRefresh = false): void {
try {
$this->output('Remove members with no circles');
$this->removeMembersWithNoCircles();
} catch (Exception $e) {
}
try {
$this->output('Retry failed FederatedEvents (asap)');
$this->eventWrapperService->retry(EventWrapperService::RETRY_ASAP);
} catch (Exception $e) {
}
}
/**
* every hour
*/
private function runMaintenance3(bool $forceRefresh = false): void {
try {
$this->output('Retry failed FederatedEvents (hourly)');
$this->eventWrapperService->retry(EventWrapperService::RETRY_HOURLY);
} catch (Exception $e) {
}
}
/**
* every day
*/
private function runMaintenance4(bool $forceRefresh = false): void {
try {
$this->output('Retry failed FederatedEvents (daily)');
$this->eventWrapperService->retry(EventWrapperService::RETRY_DAILY);
} catch (Exception $e) {
}
try {
// TODO: waiting for confirmation of a good migration before cleaning orphan shares
if ($this->configService->getAppValueBool(ConfigService::MIGRATION_22_CONFIRMED)) {
$this->output('Remove deprecated shares');
$this->removeDeprecatedShares();
}
} catch (Exception $e) {
}
try {
$this->output('Synchronizing local entities');
$this->syncService->sync();
} catch (Exception $e) {
}
}
/**
* every week
*/
private function runMaintenance5(bool $forceRefresh = false): void {
try {
$this->output('Update memberships');
$this->updateAllMemberships();
} catch (Exception $e) {
}
try {
$this->output('refresh members\' display name');
$this->refreshDisplayName($forceRefresh);
} catch (Exception $e) {
}
try {
// Can be removed in NC27.
$this->output('Remove orphan shares');
$this->removeOrphanShares();
} catch (Exception $e) {
}
try {
// Can be removed in NC27.
$this->output('fix sub-circle display name');
$this->fixSubCirclesDisplayName();
} catch (Exception $e) {
}
}
/**
* @throws InitiatorNotFoundException
* @throws RequestBuilderException
*/
private function removeCirclesWithNoOwner(): void {
$probe = new CircleProbe();
$probe->includeSystemCircles()
->includeSingleCircles()
->includePersonalCircles();
$circles = $this->circleService->getCircles($probe);
foreach ($circles as $circle) {
if (!$circle->hasOwner()) {
$this->circleRequest->delete($circle);
}
}
}
/**
*
*/
private function removeMembersWithNoCircles(): void {
// $members = $this->membersRequest->forceGetAllMembers();
//
// foreach ($members as $member) {
// try {
// $this->circlesRequest->forceGetCircle($member->getCircleId());
// } catch (CircleDoesNotExistException $e) {
// $this->membersRequest->removeMember($member);
// }
// }
}
private function removeOrphanShares(): void {
$this->shareWrapperRequest->removeOrphanShares();
}
/**
* @throws RequestBuilderException
*/
private function removeDeprecatedShares(): void {
$probe = new CircleProbe();
$probe->includePersonalCircles()
->includeSingleCircles()
->includeSystemCircles();
$circles = array_map(
function (Circle $circle) {
return $circle->getSingleId();
}, $this->circleRequest->getCircles(null, $probe)
);
$shares = array_unique(
array_map(
function (ShareWrapper $share) {
return $share->getSharedWith();
}, $this->shareWrapperRequest->getShares()
)
);
foreach ($shares as $share) {
if (!in_array($share, $circles)) {
$this->shareWrapperService->deleteAllSharesToCircle($share);
}
}
}
/**
* @throws InitiatorNotFoundException
* @throws RequestBuilderException
*/
private function updateAllMemberships(): void {
$probe = new CircleProbe();
$probe->includeSystemCircles()
->includeSingleCircles()
->includePersonalCircles();
foreach ($this->circleService->getCircles($probe) as $circle) {
$this->membershipService->manageMemberships($circle->getSingleId());
}
}
/**
* @throws RequestBuilderException
* @throws InitiatorNotFoundException
*/
private function refreshDisplayName(bool $forceRefresh = false): void {
$circleFilter = new Circle();
$circleFilter->setConfig(Circle::CFG_SINGLE);
$probe = new CircleProbe();
$probe->includeSingleCircles()
->setFilterCircle($circleFilter)
->mustBeOwner();
$circles = $this->circleService->getCircles($probe);
foreach ($circles as $circle) {
$owner = $circle->getOwner();
if (!$forceRefresh && $owner->getDisplayUpdate() > (time() - 691200)) {
continue; // ignore update done in the last 8 days.
}
if ($owner->getUserType() === Member::TYPE_USER) {
$accountData = $this->accountRequest->getAccountData($owner->getUserId());
if (array_key_exists('displayName', $accountData)) {
$this->memberRequest->updateDisplayName($owner->getSingleId(), $accountData['displayName']);
$this->circleRequest->updateDisplayName($owner->getSingleId(), $accountData['displayName']);
}
}
}
}
/**
* @throws RequestBuilderException
* @throws InitiatorNotFoundException
*/
private function fixSubCirclesDisplayName(): void {
$probe = new CircleProbe();
$probe->includeSingleCircles();
$circles = $this->circleService->getCircles($probe);
foreach ($circles as $circle) {
$this->memberRequest->updateDisplayName($circle->getSingleId(), $circle->getDisplayName());
}
}
/**
* should only be called from a BackgroundJob
*
* @param bool $heavy - set to true to run heavy maintenance process.
*/
public function runMaintenances(bool $heavy = false): void {
$last = new SimpleDataStore();
$last->json($this->configService->getAppValue(ConfigService::MAINTENANCE_UPDATE));
$maxLevel = ($heavy) ? 5 : 3;
for ($i = $maxLevel; $i > 0; $i--) {
if ($this->canRunLevel($i, $last)) {
try {
$this->runMaintenance($i);
} catch (MaintenanceException $e) {
continue;
}
$last->sInt((string)$i, time());
}
}
$this->configService->setAppValue(ConfigService::MAINTENANCE_UPDATE, json_encode($last));
}
/**
* @param int $level
* @param SimpleDataStore $last
*
* @return bool
*/
private function canRunLevel(int $level, SimpleDataStore $last): bool {
$now = time();
$timeLastRun = $last->gInt((string)$level);
if ($timeLastRun === 0) {
return true;
}
return ($timeLastRun + self::$DELAY[$level] < $now);
}
/**
* @param string $message
*/
private function output(string $message): void {
if (!is_null($this->output)) {
$this->output->writeln('- ' . $message);
}
}
}
@@ -0,0 +1,370 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\ContactAddressBookNotFoundException;
use OCA\Circles\Exceptions\ContactFormatException;
use OCA\Circles\Exceptions\ContactNotFoundException;
use OCA\Circles\Exceptions\FederatedEventException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\FederatedUserException;
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
use OCA\Circles\Exceptions\InitiatorNotFoundException;
use OCA\Circles\Exceptions\InvalidIdException;
use OCA\Circles\Exceptions\MemberNotFoundException;
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\SingleCircleNotFoundException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\FederatedItems\MassiveMemberAdd;
use OCA\Circles\FederatedItems\MemberLevel;
use OCA\Circles\FederatedItems\MemberRemove;
use OCA\Circles\FederatedItems\SingleMemberAdd;
use OCA\Circles\IFederatedUser;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\MemberProbe;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TStringTools;
/**
* Class MemberService
*
* @package OCA\Circles\Service
*/
class MemberService {
use TArrayTools;
use TStringTools;
use TNCLogger;
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var FederatedUserService */
private $federatedUserService;
/** @var MembershipService */
private $membershipService;
/** @var FederatedEventService */
private $federatedEventService;
/** @var RemoteStreamService */
private $remoteStreamService;
/**
* MemberService constructor.
*
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param FederatedUserService $federatedUserService
* @param FederatedEventService $federatedEventService
* @param RemoteStreamService $remoteStreamService
*/
public function __construct(
CircleRequest $circleRequest,
MemberRequest $memberRequest,
FederatedUserService $federatedUserService,
MembershipService $membershipService,
FederatedEventService $federatedEventService,
RemoteStreamService $remoteStreamService
) {
$this->circleRequest = $circleRequest;
$this->memberRequest = $memberRequest;
$this->federatedUserService = $federatedUserService;
$this->membershipService = $membershipService;
$this->federatedEventService = $federatedEventService;
$this->remoteStreamService = $remoteStreamService;
}
//
// /**
// * @param Member $member
// *
// * @throws MemberAlreadyExistsException
// */
// public function saveMember(Member $member) {
// $member->setId($this->token(Member::ID_LENGTH));
// $this->memberRequest->save($member);
// }
//
/**
* @param string $memberId
* @param string $circleId
* @param bool $canBeVisitor
*
* @return Member
* @throws InitiatorNotFoundException
* @throws MemberNotFoundException
* @throws RequestBuilderException
*/
public function getMemberById(
string $memberId,
string $circleId = '',
?MemberProbe $probe = null
): Member {
$this->federatedUserService->mustHaveCurrentUser();
$member = $this->memberRequest->getMemberById(
$memberId,
$this->federatedUserService->getCurrentUser(),
$probe
);
if ($circleId !== '' && $member->getCircle()->getSingleId() !== $circleId) {
throw new MemberNotFoundException();
}
return $member;
}
/**
* @param string $circleId
*
* @return Member[]
* @throws InitiatorNotFoundException
* @throws RequestBuilderException
*/
public function getMembers(string $circleId): array {
$this->federatedUserService->mustHaveCurrentUser();
$probe = new MemberProbe();
if ($this->federatedUserService->hasRemoteInstance()) {
$probe->setFilterRemoteInstance($this->federatedUserService->getRemoteInstance());
}
$probe->initiatorAsDirectMember();
$probe->mustBeMember();
return $this->memberRequest->getMembers(
$circleId,
$this->federatedUserService->getCurrentUser(),
$probe
);
}
/**
* @param string $circleId
* @param FederatedUser $federatedUser
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws FederatedUserException
* @throws InvalidIdException
* @throws SingleCircleNotFoundException
*/
public function addMember(string $circleId, FederatedUser $federatedUser): array {
$this->federatedUserService->mustHaveCurrentUser();
$circle = $this->circleRequest->getCircle($circleId, $this->federatedUserService->getCurrentUser());
$member = new Member();
$member->importFromIFederatedUser($federatedUser);
$this->federatedUserService->setMemberPatron($member);
$event = new FederatedEvent(SingleMemberAdd::class);
$event->setCircle($circle);
$event->setMember($member);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $circleId
* @param IFederatedUser[] $members
*
* @return FederatedUser[]
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws RemoteInstanceException
* @throws RequestBuilderException
*/
public function addMembers(string $circleId, array $federatedUsers): array {
$this->federatedUserService->mustHaveCurrentUser();
$circle = $this->circleRequest->getCircle($circleId, $this->federatedUserService->getCurrentUser());
if ($this->federatedUserService->isInitiatedByOcc()) {
$patron = $this->federatedUserService->getAppInitiator('occ', Member::APP_OCC);
} else {
$patron = $this->federatedUserService->getCurrentUser();
}
$members = array_map(
function (FederatedUser $federatedUser) use ($patron) {
$member = new Member();
$member->importFromIFederatedUser($federatedUser);
$member->setInvitedBy($patron);
return $member;
}, $federatedUsers
);
$event = new FederatedEvent(MassiveMemberAdd::class);
$event->setCircle($circle);
$event->setMembers($members);
$event->setParams(new SimpleDataStore(['federatedUsers' => $members]));
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $memberId
*
* @return array
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws MemberNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws RequestBuilderException
*/
public function removeMember(string $memberId): array {
$this->federatedUserService->mustHaveCurrentUser();
$member = $this->memberRequest->getMemberById(
$memberId,
$this->federatedUserService->getCurrentUser()
);
$event = new FederatedEvent(MemberRemove::class);
$event->setCircle($member->getCircle());
$event->setMember($member);
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param string $memberId
* @param int $level
*
* @return array
* @throws FederatedEventException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws MemberNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws FederatedItemException
* @throws RequestBuilderException
*/
public function memberLevel(string $memberId, int $level): array {
$this->federatedUserService->mustHaveCurrentUser();
$member =
$this->memberRequest->getMemberById($memberId, $this->federatedUserService->getCurrentUser());
$event = new FederatedEvent(MemberLevel::class);
$event->setCircle($member->getCircle());
$event->setMember($member);
$event->setParams(new SimpleDataStore(['level' => $level]));
$this->federatedEventService->newEvent($event);
return $event->getOutcome();
}
/**
* @param Member $member
*
* @return bool
* @throws InvalidIdException
* @throws RemoteNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function insertOrUpdate(Member $member): bool {
try {
$this->federatedUserService->confirmSingleIdUniqueness($member);
$member->setNoteObj('invitedBy', $member->getInvitedBy());
$this->memberRequest->insertOrUpdate($member);
$this->membershipService->onUpdate($member->getSingleId());
} catch (FederatedUserException $e) {
$this->e($e);
return false;
}
return true;
}
}
@@ -0,0 +1,775 @@
<?php
/**
* Circles - bring cloud-users closer
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
use OCA\Circles\Tools\Exceptions\RequestResultNotJsonException;
use OCA\Circles\Tools\Model\NCRequest;
use OCA\Circles\Tools\Model\Request;
use OCA\Circles\Tools\Traits\TNCRequest;
use OCA\Circles\Tools\Traits\TArrayTools;
use Exception;
use OC;
use OC\User\NoUserException;
use OCA\Circles\Circles\FileSharingBroadcaster;
use OCA\Circles\Db\AccountsRequest;
use OCA\Circles\Db\DeprecatedCirclesRequest;
use OCA\Circles\Db\DeprecatedMembersRequest;
use OCA\Circles\Db\FileSharesRequest;
use OCA\Circles\Db\TokensRequest;
use OCA\Circles\Exceptions\CircleDoesNotExistException;
use OCA\Circles\Exceptions\CircleTypeNotValidException;
use OCA\Circles\Exceptions\ConfigNoCircleAvailableException;
use OCA\Circles\Exceptions\EmailAccountInvalidFormatException;
use OCA\Circles\Exceptions\GroupDoesNotExistException;
use OCA\Circles\Exceptions\GSStatusException;
use OCA\Circles\Exceptions\MemberAlreadyExistsException;
use OCA\Circles\Exceptions\MemberCantJoinCircleException;
use OCA\Circles\Exceptions\MemberDoesNotExistException;
use OCA\Circles\Exceptions\MemberIsNotModeratorException;
use OCA\Circles\Model\DeprecatedCircle;
use OCA\Circles\Model\DeprecatedMember;
use OCA\Circles\Model\GlobalScale\GSEvent;
use OCP\IL10N;
use OCP\IUserManager;
/**
* Class MembersService
*
* @deprecated
* @package OCA\Circles\Service
*/
class MembersService {
use TNCRequest;
use TArrayTools;
/** @var string */
private $userId;
/** @var IL10N */
private $l10n;
/** @var IUserManager */
private $userManager;
/** @var ConfigService */
private $configService;
/** @var DeprecatedCirclesRequest */
private $circlesRequest;
/** @var DeprecatedMembersRequest */
private $membersRequest;
/** @var AccountsRequest */
private $accountsRequest;
/** @var FileSharesRequest */
private $fileSharesRequest;
/** @var TokensRequest */
private $tokensRequest;
/** @var EventsService */
private $eventsService;
/** @var GSUpstreamService */
private $gsUpstreamService;
/** @var FileSharingBroadcaster */
private $fileSharingBroadcaster;
/** @var MiscService */
private $miscService;
/**
* MembersService constructor.
*
* @param string $userId
* @param IL10N $l10n
* @param IUserManager $userManager
* @param ConfigService $configService
* @param DeprecatedCirclesRequest $circlesRequest
* @param DeprecatedMembersRequest $membersRequest
* @param AccountsRequest $accountsRequest
* @param FileSharesRequest $fileSharesRequest
* @param TokensRequest $tokensRequest
* @param EventsService $eventsService
* @param GSUpstreamService $gsUpstreamService
* @param FileSharingBroadcaster $fileSharingBroadcaster
* @param MiscService $miscService
*/
public function __construct(
$userId, IL10N $l10n, IUserManager $userManager, ConfigService $configService,
DeprecatedCirclesRequest $circlesRequest, DeprecatedMembersRequest $membersRequest,
AccountsRequest $accountsRequest,
FileSharesRequest $fileSharesRequest, TokensRequest $tokensRequest, EventsService $eventsService,
GSUpstreamService $gsUpstreamService, FileSharingBroadcaster $fileSharingBroadcaster,
MiscService $miscService
) {
$this->userId = $userId;
$this->l10n = $l10n;
$this->userManager = $userManager;
$this->configService = $configService;
$this->circlesRequest = $circlesRequest;
$this->membersRequest = $membersRequest;
$this->accountsRequest = $accountsRequest;
$this->fileSharesRequest = $fileSharesRequest;
$this->tokensRequest = $tokensRequest;
$this->eventsService = $eventsService;
$this->gsUpstreamService = $gsUpstreamService;
$this->fileSharingBroadcaster = $fileSharingBroadcaster;
$this->miscService = $miscService;
}
/**
* addMember();
*
* add a new member to a circle.
*
* @param string $circleUniqueId
* @param $ident
* @param int $type
* @param string $instance
*
* @param bool $force
*
* @return array
* @throws Exception
*/
public function addMember($circleUniqueId, $ident, $type, string $instance, bool $force = false) {
if ($force === true) {
$circle = $this->circlesRequest->forceGetCircle($circleUniqueId);
} else {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
$circle->getHigherViewer()
->hasToBeModerator();
}
$curr = $this->membersRequest->getMembers($circle->getUniqueId(), $circle->getHigherViewer(), $force);
$new = $this->addMassiveMembers($circle, $ident, $type);
if (empty($new)) {
$new = [$this->addSingleMember($circle, $ident, $type, $instance, $force)];
}
return $this->filterDuplicate($curr, $new);
}
/**
* add a single member to a circle.
*
* @param DeprecatedCircle $circle
* @param string $ident
* @param int $type
*
* @param string $instance
* @param bool $force
*
* @return DeprecatedMember
* @throws EmailAccountInvalidFormatException
* @throws NoUserException
* @throws Exception
*/
private function addSingleMember(
DeprecatedCircle $circle, $ident, $type, $instance = '', bool $force = false
): DeprecatedMember {
$this->verifyIdentBasedOnItsType($ident, $type, $instance);
$this->verifyIdentContact($ident, $type);
$member = $this->membersRequest->getFreshNewMember($circle->getUniqueId(), $ident, $type, $instance);
$this->updateCachedName($member);
$event = new GSEvent(GSEvent::MEMBER_ADD, false, $force);
$event->setSeverity(GSEvent::SEVERITY_HIGH);
$event->setAsync(true);
$event->setDeprecatedCircle($circle);
$event->setMember($member);
$this->gsUpstreamService->newEvent($event);
$new = $event->getMember();
$new->setJoined($this->l10n->t('now'));
if ($circle->getType() === DeprecatedCircle::CIRCLES_CLOSED) {
// $new->setLevel(Member::LEVEL_MEMBER);
$new->setStatus(DeprecatedMember::STATUS_INVITED);
} else {
// $new->setLevel(Member::LEVEL_MEMBER);
$new->setStatus(DeprecatedMember::STATUS_MEMBER);
}
if ($this->configService->isLocalInstance($new->getInstance())) {
$new->setInstance('');
}
return $new;
}
/**
* add a bunch of users to a circle based on the type of the 'bunch'
*
* @param DeprecatedCircle $circle
* @param string $ident
* @param int $type
*
* @return DeprecatedMember[]
* @throws Exception
*/
private function addMassiveMembers(DeprecatedCircle $circle, $ident, $type): array {
if ($type === DeprecatedMember::TYPE_GROUP) {
return $this->addGroupMembers($circle, $ident);
}
if ($type === DeprecatedMember::TYPE_USER) {
return $this->addMassiveMails($circle, $ident);
}
return [];
}
/**
* add a new member based on its type.
*
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*
* @throws CircleTypeNotValidException
* @throws MemberCantJoinCircleException
*/
public function addMemberBasedOnItsType(DeprecatedCircle $circle, DeprecatedMember $member) {
$this->addLocalMember($circle, $member);
$this->addEmailAddress($member);
$this->addContact($member);
}
/**
* @param DeprecatedCircle $circle
* @param DeprecatedMember $member
*
* @throws CircleTypeNotValidException
* @throws MemberCantJoinCircleException
*/
private function addLocalMember(DeprecatedCircle $circle, DeprecatedMember $member) {
if ($member->getType() !== DeprecatedMember::TYPE_USER) {
return;
}
$member->inviteToCircle($circle->getType());
if ($circle->getType() === DeprecatedCircle::CIRCLES_CLOSED
&& $this->configService->isInvitationSkipped()) {
$member->joinCircle($circle->getType());
}
}
/**
* add mail address as contact.
*
* @param DeprecatedMember $member
*/
private function addEmailAddress(DeprecatedMember $member) {
if ($member->getType() !== DeprecatedMember::TYPE_MAIL) {
return;
}
$member->addMemberToCircle();
}
/**
* // TODO - check this on GS setup
* Add contact as member.
*
* @param DeprecatedMember $member
*/
private function addContact(DeprecatedMember $member) {
if ($member->getType() !== DeprecatedMember::TYPE_CONTACT) {
return;
}
$member->addMemberToCircle();
}
/**
* // TODO - check this on GS setup
* Verify the availability of an ident, based on its type.
*
* @param string $ident
* @param int $type
* @param string $instance
*
* @throws EmailAccountInvalidFormatException
* @throws NoUserException
*/
public function verifyIdentBasedOnItsType(&$ident, $type, string $instance = '') {
if ($this->configService->isLocalInstance($instance)) {
$instance = '';
}
$this->verifyIdentLocalMember($ident, $type, $instance);
$this->verifyIdentEmailAddress($ident, $type);
// $this->verifyIdentContact($ident, $type);
}
/**
* Verify if a local account is valid.
*
* @param $ident
* @param $type
*
* @param string $instance
*
* @throws NoUserException
*/
private function verifyIdentLocalMember(&$ident, $type, string $instance = '') {
if ($type !== DeprecatedMember::TYPE_USER) {
return;
}
if ($instance === '') {
try {
$ident = $this->miscService->getRealUserId($ident);
} catch (NoUserException $e) {
throw new NoUserException($this->l10n->t("This account does not exist"));
}
}
}
/**
* Verify if a mail have a valid format.
*
* @param string $ident
* @param int $type
*
* @throws EmailAccountInvalidFormatException
*/
private function verifyIdentEmailAddress(string $ident, int $type) {
if ($type !== DeprecatedMember::TYPE_MAIL) {
return;
}
if (!filter_var($ident, FILTER_VALIDATE_EMAIL)) {
throw new EmailAccountInvalidFormatException(
$this->l10n->t('Email format is not valid')
);
}
}
/**
* Verify if a contact exist in current user address books.
*
* @param $ident
* @param $type
*
* @throws NoUserException
* @throws EmailAccountInvalidFormatException
*/
private function verifyIdentContact(&$ident, $type) {
if ($type !== DeprecatedMember::TYPE_CONTACT) {
return;
}
$tmpContact = $this->userId . ':' . $ident;
$result = MiscService::getContactData($tmpContact);
if (empty($result)) {
throw new NoUserException($this->l10n->t("This contact is not available"));
}
$ident = $tmpContact;
}
/**
* @param DeprecatedCircle $circle
* @param string $groupId
*
* @return DeprecatedMember[]
* @throws Exception
*/
private function addGroupMembers(DeprecatedCircle $circle, $groupId): array {
$group = OC::$server->getGroupManager()
->get($groupId);
if ($group === null) {
throw new GroupDoesNotExistException($this->l10n->t('This group does not exist'));
}
$members = [];
foreach ($group->getUsers() as $user) {
try {
$members[] = $this->addSingleMember($circle, $user->getUID(), DeprecatedMember::TYPE_USER);
} catch (MemberAlreadyExistsException $e) {
} catch (Exception $e) {
throw $e;
}
}
return $members;
}
/**
* // TODO - check this on GS setup
*
* @param DeprecatedCircle $circle
* @param string $mails
*
* @return DeprecatedMember[]
*/
private function addMassiveMails(DeprecatedCircle $circle, $mails): array {
$mails = trim($mails);
if (substr($mails, 0, 6) !== 'mails:') {
return [];
}
$mails = substr($mails, 6);
$members = [];
foreach (explode(' ', $mails) as $mail) {
if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) {
continue;
}
try {
$members[] = $this->addMember($circle->getUniqueId(), $mail, DeprecatedMember::TYPE_MAIL, '');
} catch (Exception $e) {
}
}
return $members;
}
/**
* getMember();
*
* Will return any data of a user related to a circle (as a Member). User can be a 'non-member'
* Viewer needs to be at least Member of the Circle
*
* @param $circleId
* @param $userId
* @param $type
* @param bool $forceAll
*
* @return DeprecatedMember
* @throws CircleDoesNotExistException
* @throws ConfigNoCircleAvailableException
* @throws MemberDoesNotExistException
*/
public function getMember($circleId, $userId, $type, $forceAll = false) {
if (!$forceAll) {
$this->circlesRequest->getCircle($circleId, $this->userId)
->getHigherViewer()
->hasToBeMember();
}
$member = $this->membersRequest->forceGetMember($circleId, $userId, $type);
$member->setNote('');
return $member;
}
/**
* @param string $memberId
*
* @return DeprecatedMember
* @throws MemberDoesNotExistException
*/
public function getMemberById(string $memberId): DeprecatedMember {
return $this->membersRequest->forceGetMemberById($memberId);
}
/**
* @param DeprecatedMember $member
*
* @throws Exception
*/
public function updateMember(DeprecatedMember $member) {
$event = new GSEvent(GSEvent::MEMBER_UPDATE);
$event->setMember($member);
$event->setDeprecatedCircle($this->getCircleFromMembership($member));
$this->gsUpstreamService->newEvent($event);
}
/**
* @param string $circleUniqueId
* @param string $name
* @param int $type
* @param string $instance
* @param int $level
* @param bool $force
*
* @return array
* @throws CircleDoesNotExistException
* @throws CircleTypeNotValidException
* @throws ConfigNoCircleAvailableException
* @throws MemberDoesNotExistException
* @throws Exception
*/
public function levelMember(
string $circleUniqueId, string $name, int $type, string $instance, int $level, bool $force = false
) {
$level = (int)$level;
if ($force === false) {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
} else {
$circle = $this->circlesRequest->forceGetCircle($circleUniqueId);
}
if ($circle->getType() === DeprecatedCircle::CIRCLES_PERSONAL) {
throw new CircleTypeNotValidException(
$this->l10n->t('You cannot edit level in a personal circle')
);
}
$member = $this->membersRequest->forceGetMember($circle->getUniqueId(), $name, $type, $instance);
if ($member->getLevel() !== $level) {
$event = new GSEvent(GSEvent::MEMBER_LEVEL, false, $force);
$event->setDeprecatedCircle($circle);
$event->getData()
->sInt('level', $level);
$event->setMember($member);
$this->gsUpstreamService->newEvent($event);
}
if ($force === false) {
return $this->membersRequest->getMembers(
$circle->getUniqueId(), $circle->getHigherViewer()
);
} else {
return $this->membersRequest->forceGetMembers($circle->getUniqueId());
}
}
/**
* @param string $circleUniqueId
* @param string $name
* @param int $type
* @param string $instance
* @param bool $force
*
* @return DeprecatedMember[]
* @throws CircleDoesNotExistException
* @throws ConfigNoCircleAvailableException
* @throws MemberDoesNotExistException
* @throws MemberIsNotModeratorException
* @throws Exception
*/
public function removeMember(
string $circleUniqueId, string $name, int $type, string $instance, bool $force = false
): array {
if ($force === false) {
$circle = $this->circlesRequest->getCircle($circleUniqueId, $this->userId);
$circle->getHigherViewer()
->hasToBeModerator();
} else {
$circle = $this->circlesRequest->forceGetCircle($circleUniqueId);
}
$member = $this->membersRequest->forceGetMember($circleUniqueId, $name, $type, $instance);
$event = new GSEvent(GSEvent::MEMBER_REMOVE, false, $force);
$event->setDeprecatedCircle($circle);
$event->setMember($member);
$this->gsUpstreamService->newEvent($event);
if ($force === false) {
return $this->membersRequest->getMembers(
$circle->getUniqueId(), $circle->getHigherViewer()
);
} else {
return $this->membersRequest->forceGetMembers($circle->getUniqueId());
}
}
/**
* When a user is removed, remove him from all Circles
*
* @param string $userId
*
* @throws Exception
*/
public function onUserRemoved(string $userId) {
$event = new GSEvent(GSEvent::USER_DELETED, true, true);
$member = new DeprecatedMember($userId);
$event->setMember($member);
$event->getData()
->s('userId', $userId);
$this->gsUpstreamService->newEvent($event);
}
/**
* @param DeprecatedMember $member
* @param bool $fresh
*/
public function updateCachedName(DeprecatedMember $member, bool $fresh = true) {
try {
$cachedName = '';
if ($member->getType() === DeprecatedMember::TYPE_USER) {
$cachedName = $this->getUserDisplayName($member->getUserId(), $fresh);
}
if ($member->getType() === DeprecatedMember::TYPE_CONTACT) {
$cachedName = $this->miscService->getContactDisplayName($member->getUserId());
}
if ($cachedName !== '') {
$member->setCachedName($cachedName);
}
} catch (Exception $e) {
}
}
/**
* @param DeprecatedCircle $circle
*/
public function updateCachedFromCircle(DeprecatedCircle $circle) {
$members = $this->membersRequest->forceGetMembers(
$circle->getUniqueId(), DeprecatedMember::LEVEL_NONE, DeprecatedMember::TYPE_USER
);
foreach ($members as $member) {
if (time() - $member->getCachedUpdate() > 72000) {
$this->updateCachedName($member, false);
$this->membersRequest->updateMemberInfo($member);
}
}
}
/**
* @param string $ident
* @param bool $fresh
*
* @return string
* @throws GSStatusException
*/
public function getUserDisplayName(string $ident, bool $fresh = false): string {
if ($this->configService->isGSAvailable()) {
return $this->getGlobalScaleUserDisplayName($ident);
}
if (!$fresh) {
try {
$account = $this->accountsRequest->getFromUserId($ident);
return $this->get('displayName', $account);
} catch (MemberDoesNotExistException $e) {
}
}
$user = $this->userManager->get($ident);
if ($user === null) {
return '';
}
return $user->getDisplayName();
}
/**
* @param string $ident
*
* @return string
* @throws GSStatusException
*/
private function getGlobalScaleUserDisplayName(string $ident): string {
$lookup = $this->configService->getGSLookup();
$request = new NCRequest(ConfigService::GS_LOOKUP_USERS, Request::TYPE_GET);
$this->configService->configureRequest($request);
$request->basedOnUrl($lookup);
$request->addParam('search', $ident);
$request->addParam('exact', '1');
try {
$users = $this->retrieveJson($request);
return $this->get('name.value', $users);
} catch (
RequestNetworkException |
RequestResultNotJsonException $e
) {
}
return '';
}
/**
* @param DeprecatedMember $member
*
* @return DeprecatedCircle
* @throws CircleDoesNotExistException
*/
public function getCircleFromMembership(DeprecatedMember $member): DeprecatedCircle {
return $this->circlesRequest->forceGetCircle($member->getCircleId());
}
/**
* @param DeprecatedMember[] $curr
* @param DeprecatedMember[] $new
*
* @return array
*/
private function filterDuplicate(array $curr, array $new): array {
$base = [];
foreach ($curr as $currMember) {
$known = false;
foreach ($new as $newMember) {
if ($newMember->getMemberId() === $currMember->getMemberId()) {
$known = true;
}
}
if (!$known) {
$base[] = $currMember;
}
}
return array_merge($base, $new);
}
}
@@ -0,0 +1,430 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Db\MembershipRequest;
use OCA\Circles\Exceptions\FederatedUserNotFoundException;
use OCA\Circles\Exceptions\MembershipNotFoundException;
use OCA\Circles\Exceptions\OwnerNotFoundException;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Membership;
use OCA\Circles\Tools\Exceptions\ItemNotFoundException;
use OCA\Circles\Tools\Traits\TNCLogger;
/**
* Class MembershipService
*
* @package OCA\Circles\Service
*/
class MembershipService {
use TNCLogger;
/** @var MembershipRequest */
private $membershipRequest;
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var EventService */
private $eventService;
private ShareWrapperService $shareWrapperService;
/** @var OutputService */
private $outputService;
/**
* MembershipService constructor.
*
* @param MembershipRequest $membershipRequest
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param EventService $eventService
* @param ShareWrapperService $shareWrapperService
* @param OutputService $outputService
*/
public function __construct(
MembershipRequest $membershipRequest,
CircleRequest $circleRequest,
MemberRequest $memberRequest,
EventService $eventService,
ShareWrapperService $shareWrapperService,
OutputService $outputService
) {
$this->membershipRequest = $membershipRequest;
$this->circleRequest = $circleRequest;
$this->memberRequest = $memberRequest;
$this->eventService = $eventService;
$this->shareWrapperService = $shareWrapperService;
$this->outputService = $outputService;
}
/**
* @param string $singleId
*
* @throws RequestBuilderException
*/
public function onUpdate(string $singleId): void {
if ($singleId === '') {
return;
}
try {
$this->circleRequest->getFederatedUserBySingleId($singleId);
} catch (FederatedUserNotFoundException | OwnerNotFoundException $e) {
$this->membershipRequest->removeBySingleId($singleId);
}
$children = array_unique(
array_merge(
[$singleId],
$this->getChildrenMembers($singleId),
$this->getChildrenMemberships($singleId)
)
);
foreach ($children as $singleId) {
$this->manageMemberships($singleId);
}
}
/**
* @param string $singleId
*
* @return int
* @throws RequestBuilderException
*/
public function manageMemberships(string $singleId): int {
$memberships = $this->generateMemberships($singleId);
return $this->updateMembershipsDatabase($singleId, $memberships);
}
/**
* @param string $circleId
* @param string $singleId
* @param bool $detailed
*
* @return Membership
* @throws MembershipNotFoundException
* @throws RequestBuilderException
*/
public function getMembership(string $circleId, string $singleId, bool $detailed = false): Membership {
$membership = $this->membershipRequest->getMembership($circleId, $singleId);
if ($detailed) {
$details = $this->circleRequest->getCirclesByIds($membership->getInheritancePath());
$membership->setInheritanceDetails($details);
}
return $membership;
}
/**
* @param string $singleId
* @param bool $all
*/
public function resetMemberships(string $singleId = '', bool $all = false) {
$this->membershipRequest->removeBySingleId($singleId, $all);
}
/**
* @param FederatedUser $federatedUser
*/
public function deleteFederatedUser(FederatedUser $federatedUser) {
$this->membershipRequest->deleteFederatedUser($federatedUser);
}
/**
* @param string $singleId
* @param string $circleId
* @param array $memberships
* @param array $knownIds
* @param array $path
*
* @return array
* @throws RequestBuilderException
*/
private function generateMemberships(
string $singleId,
string $circleId = '',
array &$memberships = [],
array $knownIds = [],
array $path = []
): array {
$circleId = ($circleId === '') ? $singleId : $circleId;
$path[] = $circleId;
$knownIds[] = $circleId;
$members = $this->memberRequest->getMembersBySingleId($circleId);
foreach ($members as $member) {
if (!$member->hasCircle() || $member->getLevel() < Member::LEVEL_MEMBER) {
continue;
}
if ($member->getCircle()->isConfig(Circle::CFG_NO_OWNER)
&& $member->getLevel() === Member::LEVEL_OWNER) {
continue;
}
$membership = new Membership($singleId, count($path) > 1 ? $path[1] : '', $member);
$membership->setInheritancePath(array_reverse($path))
->setInheritanceDepth(sizeof($path));
$this->fillMemberships($membership, $memberships);
if (!in_array($member->getCircleId(), $knownIds)) {
$this->generateMemberships(
$singleId,
$member->getCircleId(),
$memberships,
$knownIds,
$path
);
}
}
return $memberships;
}
/**
* @param string $singleId
* @param Membership[] $memberships
*
* @return int
*/
private function updateMembershipsDatabase(string $singleId, array $memberships): int {
$known = $this->membershipRequest->getMemberships($singleId);
$deprecated = $this->removeDeprecatedMemberships($memberships, $known);
if (!empty($deprecated)) {
$this->eventService->membershipsRemoved($deprecated);
}
$new = $this->createNewMemberships($memberships, $known);
if (!empty($new)) {
$this->eventService->membershipsCreated($new);
}
return count($deprecated) + count($new);
}
/**
* @param string $id
* @param array $knownIds
*
* @return array
* @throws RequestBuilderException
*/
private function getChildrenMembers(string $id, array &$knownIds = []): array {
$singleIds = array_map(
function (Member $item): string {
return $item->getSingleId();
}, $this->memberRequest->getMembers($id)
);
foreach ($singleIds as $singleId) {
if ($singleId !== $id && !in_array($singleId, $knownIds)) {
$knownIds[] = $singleId;
$singleIds = array_merge($singleIds, $this->getChildrenMembers($singleId, $knownIds));
}
}
return array_unique($singleIds);
}
/**
* @param string $id
* @param array $knownIds
*
* @return array
*/
private function getChildrenMemberships(string $id, array &$knownIds = []): array {
$singleIds = array_map(
function (Membership $item): string {
return $item->getSingleId();
}, $this->membershipRequest->getInherited($id)
);
foreach ($singleIds as $singleId) {
if ($singleId !== $id && !in_array($singleId, $knownIds)) {
$knownIds[] = $singleId;
$singleIds = array_merge($singleIds, $this->getChildrenMemberships($singleId, $knownIds));
}
}
return array_unique($singleIds);
}
/**
* Add the new membership if unknown or Update known membership if:
* - new membership comes with more power
* - level is the same, but inheritance is shorter
*
* @param Membership $membership
* @param Membership[] $memberships
*/
private function fillMemberships(Membership $membership, array &$memberships) {
foreach ($memberships as $known) {
if ($known->getCircleId() === $membership->getCircleId()) {
if ($known->getLevel() < $membership->getLevel()
|| ($known->getLevel() === $membership->getLevel()
&& $known->getInheritanceDepth() > $membership->getInheritanceDepth())
) {
$known->setLevel($membership->getLevel());
$known->setInheritanceFirst($membership->getInheritanceFirst());
$known->setInheritanceLast($membership->getInheritanceLast());
$known->setInheritanceDepth($membership->getInheritanceDepth());
$known->setInheritancePath($membership->getInheritancePath());
}
return;
}
}
$memberships[$membership->getCircleId()] = $membership;
}
/**
* @param Membership[] $memberships
* @param Membership[] $known
*
* @return Membership[]
*/
private function removeDeprecatedMemberships(array $memberships, array $known): array {
$circleIds = array_map(
function (Membership $membership): string {
return $membership->getCircleId();
}, $memberships
);
$deprecated = [];
foreach ($known as $item) {
if (!in_array($item->getCircleId(), $circleIds)) {
$deprecated[] = $item;
$this->membershipRequest->delete($item);
// clearing the getSharedWith() cache for singleId related to the membership
$this->shareWrapperService->clearCache($item->getSingleId());
}
}
return $deprecated;
}
/**
* @param Membership[] $memberships
* @param Membership[] $known
*
* @return Membership[]
*/
private function createNewMemberships(array $memberships, array $known): array {
$new = [];
foreach ($memberships as $membership) {
try {
$item = $this->getMembershipsFromList($known, $membership->getCircleId());
if ($item->getLevel() !== $membership->getLevel()
|| $item->getInheritanceDepth() !== $membership->getInheritanceDepth()) {
$this->membershipRequest->update($membership);
$new[] = $item;
} elseif ($item->getInheritancePath() !== $membership->getInheritancePath()) {
$this->membershipRequest->update($membership);
}
} catch (ItemNotFoundException $e) {
$this->membershipRequest->insert($membership);
$new[] = $membership;
}
// clearing the getSharedWith() cache for singleId related to the membership
$this->shareWrapperService->clearCache($membership->getSingleId());
}
return $new;
}
/**
* @param Membership[] $list
* @param string $circleId
*
* @return Membership
* @throws ItemNotFoundException
*/
private function getMembershipsFromList(array $list, string $circleId): Membership {
foreach ($list as $item) {
if ($item->getCircleId() === $circleId) {
return $item;
}
}
throw new ItemNotFoundException();
}
/**
* @param Circle $circle
*/
public function updatePopulation(Circle $circle): void {
$local = $inherited = 0;
$memberships = $this->membershipRequest->getInherited($circle->getSingleId(), Member::LEVEL_MEMBER);
foreach ($memberships as $membership) {
$inherited++;
if ($membership->getCircleId() === $circle->getSingleId()) {
$local++;
}
}
$settings = $circle->getSettings();
$settings['population'] = $local;
$settings['populationInherited'] = $inherited;
$this->circleRequest->updateSettings($circle->setSettings($settings));
}
}
@@ -0,0 +1,690 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use Exception;
use OC;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Db\ShareTokenRequest;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\ContactAddressBookNotFoundException;
use OCA\Circles\Exceptions\ContactFormatException;
use OCA\Circles\Exceptions\ContactNotFoundException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\FederatedUserException;
use OCA\Circles\Exceptions\FederatedUserNotFoundException;
use OCA\Circles\Exceptions\InvalidIdException;
use OCA\Circles\Exceptions\MemberNotFoundException;
use OCA\Circles\Exceptions\MigrationException;
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\ShareTokenNotFoundException;
use OCA\Circles\Exceptions\SingleCircleNotFoundException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\Exceptions\UserTypeNotFoundException;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\ShareToken;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TStringTools;
use OCA\DAV\CardDAV\ContactsManager;
use OCP\Contacts\IManager;
use OCP\IDBConnection;
use OCP\IURLGenerator;
use OCP\Share\IShare;
/**
* Class MigrationService
*
* @package OCA\Circles\Service
*/
class MigrationService {
use TStringTools;
use TNCLogger;
/** @var IDBConnection */
private $dbConnection;
/** @var IURLGenerator */
private $urlGenerator;
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var ShareTokenRequest */
private $shareTokenRequest;
/** @var MembershipService */
private $membershipService;
/** @var FederatedUserService */
private $federatedUserService;
/** @var CircleService */
private $circleService;
/** @var ContactService */
private $contactService;
/** @var TimezoneService */
private $timezoneService;
/** @var OutputService */
private $outputService;
/** @var ConfigService */
private $configService;
/** @var FederatedUser */
private $appCircle = null;
/**
* MigrationService constructor.
*
* @param IDBConnection $dbConnection
* @param IURLGenerator $urlGenerator
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param ShareTokenRequest $shareTokenRequest
* @param MembershipService $membershipService
* @param FederatedUserService $federatedUserService
* @param CircleService $circleService
* @param ContactService $contactService
* @param TimezoneService $timezoneService
* @param OutputService $outputService
* @param ConfigService $configService
*/
public function __construct(
IDBConnection $dbConnection,
IURLGenerator $urlGenerator,
CircleRequest $circleRequest,
MemberRequest $memberRequest,
ShareTokenRequest $shareTokenRequest,
MembershipService $membershipService,
FederatedUserService $federatedUserService,
CircleService $circleService,
ContactService $contactService,
TimezoneService $timezoneService,
OutputService $outputService,
ConfigService $configService
) {
$this->dbConnection = $dbConnection;
$this->urlGenerator = $urlGenerator;
$this->circleRequest = $circleRequest;
$this->memberRequest = $memberRequest;
$this->shareTokenRequest = $shareTokenRequest;
$this->membershipService = $membershipService;
$this->federatedUserService = $federatedUserService;
$this->circleService = $circleService;
$this->contactService = $contactService;
$this->timezoneService = $timezoneService;
$this->outputService = $outputService;
$this->configService = $configService;
$this->setup('app', Application::APP_ID);
}
/**
* @param bool $force
*
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws FederatedUserException
* @throws InvalidIdException
* @throws MigrationException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
*/
public function migration(bool $force = false): void {
if ($this->configService->getAppValueBool(ConfigService::MIGRATION_RUN)) {
throw new MigrationException('A migration process is already running');
}
$this->configService->setAppValue(ConfigService::MIGRATION_RUN, '1');
if ($force) {
$this->configService->setAppValue(ConfigService::MIGRATION_22, '0');
$this->configService->setAppValue(ConfigService::MIGRATION_22_1, '0');
// $this->configService->setAppValue(ConfigService::MIGRATION_23, '0');
}
$this->appCircle = $this->federatedUserService->getAppInitiator(
Application::APP_ID,
Member::APP_CIRCLES
);
$this->migrationTo22();
$this->configService->setAppValue(ConfigService::MIGRATION_RUN, '0');
}
/**
* @throws RequestBuilderException
*/
private function migrationTo22(): void {
if ($this->configService->getAppValueBool(ConfigService::MIGRATION_22)) {
return;
}
if (!$this->migrationTo22Feasibility()) {
$this->configService->setAppValue(ConfigService::MIGRATION_22, '1');
return;
}
$this->outputService->output('Migrating to 22');
$this->migrationTo22_Circles();
$this->migrationTo22_Members();
$this->membershipService->resetMemberships('', true);
$this->migrationTo22_Members_Memberships();
$this->migrationTo22_Tokens();
$this->migrationTo22_1_SubShares();
$this->configService->setAppValue(ConfigService::MIGRATION_22, '1');
}
/**
* run migration if:
* - old tables exist.
* - new tables are (almost) empty.
*
* @return bool
* @throws \OCP\DB\Exception
*/
public function migrationTo22Feasibility(): bool {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')->from('circle_circles');
try {
$cursor = $qb->executeQuery();
$cursor->closeCursor();
} catch (\OCP\DB\Exception $e) {
return false;
}
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')->from('circles_circle');
$cursor = $qb->executeQuery();
if ($cursor->rowCount() > 1) {
return false;
}
$cursor->closeCursor();
return true;
}
/**
* @throws RequestBuilderException
*/
public function migrationTo22_Members_Memberships(): void {
$probe = new CircleProbe();
$probe->includeSystemCircles();
$circles = $this->circleRequest->getCircles(null, $probe);
$this->outputService->startMigrationProgress(sizeof($circles));
$done = [];
foreach ($circles as $circle) {
$this->outputService->output(
'Caching memberships for Members of \'' . $circle->getDisplayName() . '\'',
true
);
$members = $circle->getMembers();
foreach ($members as $member) {
if (in_array($member->getSingleId(), $done)) {
continue;
}
$this->membershipService->manageMemberships($member->getSingleId());
$done[] = $member->getSingleId();
}
}
$this->outputService->finishMigrationProgress();
}
/**
*
*/
private function migrationTo22_Circles(): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')->from('circle_circles');
try {
$cursor = $qb->executeQuery();
$this->outputService->startMigrationProgress($cursor->rowCount());
while ($row = $cursor->fetch()) {
try {
$data = new SimpleDataStore($row);
$this->outputService->output(
'Migrating Circle \'' . $data->g('name') . '\' (' . $data->g('unique_id') . ')',
true
);
$circle = $this->generateCircleFrom21($data);
$this->saveGeneratedCircle($circle);
} catch (Exception $e) {
}
}
$cursor->closeCursor();
} catch (\OCP\DB\Exception $e) {
}
$this->outputService->finishMigrationProgress();
}
/**
* @param SimpleDataStore $data
*
* @return Circle
* @throws RequestBuilderException
*/
private function generateCircleFrom21(SimpleDataStore $data): Circle {
$circle = new Circle();
$circle->setSingleId($data->g('unique_id'))
->setName($data->g('name'))
->setDisplayName($data->g('display_name'))
->setSettings($data->gArray('settings'))
->setDescription($data->g('description'))
->setContactAddressBook($data->gInt('contact_addressbook'))
->setContactGroupName($data->g('contact_groupname'))
->setSource(Member::TYPE_CIRCLE);
$dTime = $this->timezoneService->getDateTime($data->g('creation'));
$circle->setCreation($dTime->getTimestamp());
if ($circle->getDisplayName() === '') {
$circle->setDisplayName($circle->getName());
}
$this->circleService->generateSanitizedName($circle);
$this->convertCircleTypeFrom21($circle, $data->gInt('type'));
return $circle;
}
/**
* @param Circle $circle
* @param int $type
*/
private function convertCircleTypeFrom21(Circle $circle, int $type): void {
switch ($type) {
case 1: // personal
$circle->setConfig(Circle::CFG_PERSONAL);
break;
case 2: // secret
$circle->setConfig(Circle::CFG_OPEN + Circle::CFG_REQUEST);
break;
case 4: // closed
$circle->setConfig(Circle::CFG_OPEN + Circle::CFG_REQUEST + Circle::CFG_VISIBLE);
break;
case 8: // public
$circle->setConfig(Circle::CFG_OPEN + Circle::CFG_VISIBLE);
break;
}
}
/**
* @param Circle $circle
*/
private function saveGeneratedCircle(Circle $circle): void {
try {
$this->circleRequest->getCircle($circle->getSingleId());
} catch (CircleNotFoundException $e) {
try {
$this->circleRequest->save($circle);
} catch (InvalidIdException $e) {
}
} catch (RequestBuilderException $e) {
}
}
/**
*/
private function migrationTo22_Members(): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')->from('circle_members');
try {
$cursor = $qb->executeQuery();
$this->outputService->startMigrationProgress($cursor->rowCount());
while ($row = $cursor->fetch()) {
try {
$data = new SimpleDataStore($row);
$this->outputService->output(
'Migrating Member \'' . $data->g('user_id') . '\' from \'' . $data->g('circle_id')
. '\'',
true
);
$member = $this->generateMemberFrom21($data);
$this->saveGeneratedMember($member);
} catch (Exception $e) {
}
}
$cursor->closeCursor();
} catch (\OCP\DB\Exception $e) {
}
$this->outputService->finishMigrationProgress();
}
/**
* @throws CircleNotFoundException
* @throws RemoteInstanceException
* @throws UserTypeNotFoundException
* @throws FederatedUserNotFoundException
* @throws OwnerNotFoundException
* @throws RequestBuilderException
* @throws RemoteNotFoundException
* @throws UnknownRemoteException
* @throws FederatedUserException
* @throws ContactAddressBookNotFoundException
* @throws RemoteResourceNotFoundException
* @throws MemberNotFoundException
* @throws FederatedItemException
* @throws SingleCircleNotFoundException
* @throws InvalidIdException
*/
private function generateMemberFrom21(SimpleDataStore $data): Member {
$member = new Member();
$member->setCircleId($data->g('circle_id'))
->setId($data->g('member_id'))
->setUserId($data->g('user_id'))
->setInstance($data->g('instance'))
->setDisplayName($data->g('cached_name'))
->setLevel($data->gInt('level'))
->setStatus($data->g('status'))
->setContactMeta($data->g('contact_meta'))
->setContactId($data->g('contact_id'))
->setInvitedBy($this->appCircle);
$this->convertMemberUserTypeFrom21($member, $data->gInt('user_type'));
$singleMember = $this->federatedUserService->getFederatedUser(
$member->getUserId(),
$member->getUserType()
);
$member->setSingleId($singleMember->getSingleId());
return $member;
}
/**
* @param Member $member
* @param int $userType
*
* @throws ContactAddressBookNotFoundException
*/
private function convertMemberUserTypeFrom21(Member $member, int $userType): void {
switch ($userType) {
case 1:
$member->setUserType(1);
return;
case 2:
$member->setUserType(2);
return;
case 3:
$member->setUserType(4);
return;
case 4:
$member->setUserType(8);
$this->fixContactId($member);
return;
}
}
private function migrationTo22_1_SubShares(): void {
$qb = $this->dbConnection->getQueryBuilder();
$expr = $qb->expr();
$qb->select('*')
->from('share')
->where($expr->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE)))
->andWhere($expr->isNotNull('parent'));
try {
$cursor = $qb->executeQuery();
$this->outputService->startMigrationProgress($cursor->rowCount());
while ($row = $cursor->fetch()) {
try {
$data = new SimpleDataStore($row);
$federatedUser =
$this->federatedUserService->getLocalFederatedUser($data->g('share_with'));
$this->outputService->output(
'Migrating child share #' . $data->gInt('id') . ' owner: ' . $data->g('share_with')
. ' -> ' . $federatedUser->getSingleId(),
true
);
$this->updateSubShare($data, $federatedUser);
} catch (Exception $e) {
}
}
$cursor->closeCursor();
} catch (\OCP\DB\Exception $e) {
}
$this->outputService->finishMigrationProgress();
}
/**
* @param SimpleDataStore $data
* @param FederatedUser $federatedUser
*
* @throws \OCP\DB\Exception
*/
private function updateSubShare(SimpleDataStore $data, FederatedUser $federatedUser): void {
$qb = $this->dbConnection->getQueryBuilder();
$expr = $qb->expr();
$qb->select('*')
->from('share')
->where($expr->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE)))
->andWhere($expr->eq('parent', $qb->createNamedParameter($data->gInt('parent'))))
->andWhere($expr->eq('share_with', $qb->createNamedParameter($federatedUser->getSingleId())));
$cursor = $qb->executeQuery();
if ($cursor->rowCount() > 0) {
// TODO: delete current row ?
return;
}
$cursor->closeCursor();
$qb = $this->dbConnection->getQueryBuilder();
$expr = $qb->expr();
$qb->update('share')
->set('share_with', $qb->createNamedParameter($federatedUser->getSingleId()))
->where($expr->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE)))
->andWhere($expr->eq('id', $qb->createNamedParameter($data->gInt('id'))))
->setMaxResults(1);
$qb->executeStatement();
}
/**
* @param Member $member
*
* @throws ContactAddressBookNotFoundException
*/
private function fixContactId(Member $member) {
[$userId, $contactId] = explode(':', $member->getUserId());
$contactsManager = OC::$server->get(ContactsManager::class);
/** @var IManager $cm */
$cm = OC::$server->get(IManager::class);
$contactsManager->setupContactsProvider($cm, $userId, $this->urlGenerator);
$contact = $cm->search($contactId, ['UID']);
if (sizeof($contact) === 1) {
$entry = array_shift($contact);
$addressBook =
$this->contactService->getAddressBoxById($cm, $this->get('addressbook-key', $entry));
$member->setUserId($userId . '/' . $addressBook->getUri() . '/' . $contactId);
}
}
/**
* @param Member $member
*/
private function saveGeneratedMember(Member $member): void {
try {
$this->memberRequest->getMemberById($member->getId());
} catch (MemberNotFoundException $e) {
try {
$this->memberRequest->save($member);
} catch (InvalidIdException $e) {
}
} catch (RequestBuilderException $e) {
}
}
/**
*/
public function migrationTo22_Tokens(): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')->from('circle_tokens');
try {
$cursor = $qb->executeQuery();
$this->outputService->startMigrationProgress($cursor->rowCount());
while ($row = $cursor->fetch()) {
try {
$data = new SimpleDataStore($row);
$this->outputService->output(
'Migrating ShareToken \'' . $data->g('token') . '\' for \'' . $data->g('user_id')
. '\'',
true
);
$shareToken = $this->generateShareTokenFrom21($data);
$this->saveGeneratedShareToken($shareToken);
} catch (Exception $e) {
}
}
$cursor->closeCursor();
} catch (\OCP\DB\Exception $e) {
}
$this->outputService->finishMigrationProgress();
}
/**
* @param SimpleDataStore $data
*
* @return ShareToken
* @throws MemberNotFoundException
* @throws RequestBuilderException
*/
private function generateShareTokenFrom21(SimpleDataStore $data): ShareToken {
$shareToken = new ShareToken();
$member = $this->memberRequest->getMemberById($data->g('member_id'));
if ($member->getUserType() !== Member::TYPE_MAIL
&& $member->getUserType() !== Member::TYPE_CONTACT) {
throw new MemberNotFoundException();
}
$shareToken->setShareId($data->gInt('share_id'))
->setCircleId($data->g('circle_id'))
->setSingleId($member->getSingleId())
->setMemberId($data->g('member_id'))
->setToken($data->g('token'))
->setPassword($data->g('password'))
->setAccepted(IShare::STATUS_ACCEPTED);
return $shareToken;
}
/**
* @param ShareToken $shareToken
*/
private function saveGeneratedShareToken(ShareToken $shareToken): void {
try {
$this->shareTokenRequest->getByToken($shareToken->getToken());
} catch (ShareTokenNotFoundException $e) {
$this->shareTokenRequest->save($shareToken);
} catch (RequestBuilderException $e) {
}
}
}
@@ -0,0 +1,411 @@
<?php
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\Tools\Traits\TArrayTools;
use Exception;
use OC;
use OC\User\NoUserException;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Exceptions\MissingKeyInArrayException;
use OCA\Circles\Model\DeprecatedMember;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\Contacts\ContactsMenu\IContactsStore;
use OCP\ILogger;
use OCP\IUserManager;
class MiscService {
use TArrayTools;
/** @var ILogger */
private $logger;
/** @var IContactsStore */
private $contactsStore;
/** @var string */
private $appName;
/** @var IUserManager */
private $userManager;
public function __construct(
ILogger $logger, IContactsStore $contactsStore, $appName, IUserManager $userManager
) {
$this->logger = $logger;
$this->contactsStore = $contactsStore;
$this->appName = $appName;
$this->userManager = $userManager;
}
public function log($message, $level = 4) {
$data = [
'app' => $this->appName,
'level' => $level
];
$this->logger->log($level, $message, $data);
}
/**
* @param Exception $e
*/
public function e(Exception $e) {
$this->logger->logException($e, ['app' => 'circles']);
}
/**
* @param $arr
* @param $k
*
* @param string $default
*
* @return array|string
*/
public static function get($arr, $k, $default = '') {
if (!key_exists($k, $arr)) {
return $default;
}
return $arr[$k];
}
public static function mustContains($data, $arr) {
if (!is_array($arr)) {
$arr = [$arr];
}
foreach ($arr as $k) {
if (!key_exists($k, $data)) {
throw new MissingKeyInArrayException('missing_key_in_array');
}
}
}
/**
* @param $data
*
* @return DataResponse
*/
public function fail($data) {
$this->log(json_encode($data));
return new DataResponse(
array_merge($data, ['status' => 0]),
Http::STATUS_NON_AUTHORATIVE_INFORMATION
);
}
/**
* @param $data
*
* @return DataResponse
*/
public function success($data) {
return new DataResponse(
array_merge($data, ['status' => 1]),
Http::STATUS_CREATED
);
}
/**
* return the real userId, with its real case
*
* @param $userId
*
* @return string
* @throws NoUserException
*/
public function getRealUserId($userId) {
if ($this->userManager->userExists($userId)) {
return $this->userManager->get($userId)
->getUID();
}
$result = $this->userManager->search($userId);
if (sizeof($result) !== 1) {
throw new NoUserException();
}
$user = array_shift($result);
return $user->getUID();
}
/**
* @param string $ident
*
* @return string
*/
public function getContactDisplayName(string $ident): string {
if (!class_exists(\OCA\DAV\CardDAV\ContactsManager::class) || !strpos($ident, ':')) {
return '';
}
[$userId, $contactId] = explode(':', $ident);
$entries = [];
try {
/** @var \OCA\DAV\CardDAV\ContactsManager $cManager */
$cManager = OC::$server->query(\OCA\DAV\CardDAV\ContactsManager::class);
$urlGenerator = OC::$server->getURLGenerator();
$cm = OC::$server->getContactsManager();
$cManager->setupContactsProvider($cm, $userId, $urlGenerator);
$contact = $cm->search($contactId, ['UID']);
$entries = array_shift($contact);
} catch (Exception $e) {
}
if (key_exists('FN', $entries) && $entries['FN'] !== '') {
return $entries['FN'];
}
if (key_exists('EMAIL', $entries) && $entries['EMAIL'] !== '') {
return $entries['EMAIL'];
}
}
/**
* @param string $ident
* @param int $type
*
* @return string
* @deprecated
*
*/
public static function getDisplay($ident, $type) {
$display = $ident;
self::getDisplayMember($display, $ident, $type);
self::getDisplayContact($display, $ident, $type);
return $display;
}
/**
* @param string $display
* @param string $ident
* @param int $type
*/
private static function getDisplayMember(&$display, $ident, $type) {
if ($type !== DeprecatedMember::TYPE_USER) {
return;
}
$user = OC::$server->getUserManager()
->get($ident);
if ($user !== null) {
$display = $user->getDisplayName();
}
}
/**
* @param string $display
* @param string $ident
* @param int $type
*/
private static function getDisplayContact(&$display, $ident, $type) {
if ($type !== DeprecatedMember::TYPE_CONTACT) {
return;
}
$contact = self::getContactData($ident);
if ($contact === null) {
return;
}
self::getDisplayContactFromArray($display, $contact);
}
/**
* @param $ident
*
* @return mixed|string
* @deprecated
*
*/
public static function getContactData($ident) {
if (!class_exists(\OCA\DAV\CardDAV\ContactsManager::class) || !strpos($ident, ':')) {
return [];
}
[$userId, $contactId] = explode(':', $ident);
try {
/** @var \OCA\DAV\CardDAV\ContactsManager $cManager */
$cManager = OC::$server->query(\OCA\DAV\CardDAV\ContactsManager::class);
$urlGenerator = OC::$server->getURLGenerator();
$cm = OC::$server->getContactsManager();
$cManager->setupContactsProvider($cm, $userId, $urlGenerator);
$contact = $cm->search($contactId, ['UID']);
return array_shift($contact);
} catch (Exception $e) {
}
return [];
}
/**
* @param string $display
* @param array $contact
*
* @deprecated
*/
private static function getDisplayContactFromArray(string &$display, array $contact) {
if (!is_array($contact)) {
return;
}
if (key_exists('FN', $contact) && $contact['FN'] !== '') {
$display = $contact['FN'];
return;
}
if (key_exists('EMAIL', $contact) && $contact['EMAIL'] !== '') {
$display = $contact['EMAIL'];
return;
}
}
/**
* return Display Name if user exists and display name exists.
* returns Exception if user does not exist.
*
* However, with noException set to true, will return userId even if user does not exist
*
* @param $userId
* @param bool $noException
*
* @return string
* @throws NoUserException
*/
public function getDisplayName($userId, $noException = false) {
$user = $this->userManager->get($userId);
if ($user === null) {
if ($noException) {
return $userId;
} else {
throw new NoUserException();
}
}
return $user->getDisplayName();
}
/**
* @param array $options
*
* @return array
*/
public static function generateClientBodyData($options = []) {
return [
'body' => ['data' => $options],
'timeout' => Application::CLIENT_TIMEOUT,
'connect_timeout' => Application::CLIENT_TIMEOUT
];
}
/**
* Hacky way to async the rest of the process without keeping client on hold.
*
* @param string $result
*/
public function asyncAndLeaveClientOutOfThis($result = '') {
if (ob_get_contents() !== false) {
ob_end_clean();
}
header('Connection: close');
ignore_user_abort();
ob_start();
echo(json_encode($result));
$size = ob_get_length();
header('Content-Length: ' . $size);
ob_end_flush();
flush();
}
/**
* Generate uuid: 2b5a7a87-8db1-445f-a17b-405790f91c80
*
* @param int $length
*
* @return string
*/
public function token(int $length = 0): string {
$chars = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890';
$str = '';
$max = strlen($chars) - 1;
for ($i = 0; $i <= $length; $i++) {
try {
$str .= $chars[random_int(0, $max)];
} catch (Exception $e) {
}
}
return $str;
}
/**
* @param DeprecatedMember $member
*
* @return array
*/
public function getInfosFromContact(DeprecatedMember $member) {
$contact = MiscService::getContactData($member->getUserId());
return [
'memberId' => $member->getMemberId(),
'emails' => $this->getArray('EMAIL', $contact),
'cloudIds' => $this->getArray('CLOUD', $contact)
];
}
}
@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
* @author Daniel Tygel <dtygel@eita.org.br>
*
* @copyright 2017
* @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\Circles\Service;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Model\Member;
use OCP\IURLGenerator;
use OCP\Notification\IManager as INotificationManager;
use OCP\Notification\INotification;
/**
* Class NotificationService
*
* @package OCA\Circles\Service
*/
class NotificationService {
use TNCLogger;
/** @var IURLGenerator */
private $urlGenerator;
/** @var INotificationManager */
private $notificationManager;
/** @var MemberRequest */
private $memberRequest;
/** @var TimezoneService */
private $timezoneService;
/**
* NotificationService constructor.
*
* @param IURLGenerator $urlGenerator
* @param INotificationManager $notificationManager
* @param MemberRequest $memberRequest
* @param TimezoneService $timezoneService
*/
public function __construct(
IURLGenerator $urlGenerator,
INotificationManager $notificationManager,
MemberRequest $memberRequest,
TimezoneService $timezoneService
) {
$this->urlGenerator = $urlGenerator;
$this->notificationManager = $notificationManager;
$this->memberRequest = $memberRequest;
$this->timezoneService = $timezoneService;
$this->setup('app', Application::APP_ID);
}
/**
* @param Member $member
*/
public function notificationInvited(Member $member): void {
if ($member->getUserType() !== Member::TYPE_USER || !$member->isLocal()) {
return;
}
$this->deleteNotification('member', $member->getId());
$notification = $this->createMemberNotification(
$member->getUserId(),
$member->getId(),
'invitation'
);
$declineAction = $notification->createAction();
$declineUrl = $this->linkToOCS('circles.Local.circleLeave', ['circleId' => $member->getCircleId()]);
$declineAction->setLabel('refuse')
->setLink($declineUrl, 'PUT');
$notification->addAction($declineAction);
$acceptAction = $notification->createAction();
$acceptUrl = $this->linkToOCS('circles.Local.circleJoin', ['circleId' => $member->getCircleId()]);
$acceptAction->setLabel('accept')
->setLink($acceptUrl, 'PUT');
$notification->addAction($acceptAction);
$this->notificationManager->notify($notification);
}
/**
* @param Member $member
*
* @throws RequestBuilderException
*/
public function notificationRequested(Member $member): void {
// if ($member->getUserType() !== Member::TYPE_USER || !$member->isLocal()) {
// return;
// }
$this->deleteNotification('member', $member->getId());
$moderators = $this->memberRequest->getInheritedMembers(
$member->getCircleId(),
false,
Member::LEVEL_MODERATOR
);
foreach ($moderators as $moderator) {
if ($moderator->getUserType() !== Member::TYPE_USER || !$moderator->isLocal()) {
continue;
}
$notification = $this->createMemberNotification(
$moderator->getUserId(),
$member->getId(),
'joinRequest'
);
$declineAction = $notification->createAction();
$declineUrl = $this->linkToOCS(
'circles.Local.memberRemove',
[
'circleId' => $member->getCircleId(),
'memberId' => $member->getId()
]
);
$declineAction->setLabel('refuse')
->setLink($declineUrl, 'DELETE');
$notification->addAction($declineAction);
$acceptAction = $notification->createAction();
$acceptUrl = $this->linkToOCS(
'circles.Local.memberConfirm',
[
'circleId' => $member->getCircleId(),
'memberId' => $member->getId()
]
);
$acceptAction->setLabel('accept')
->setLink($acceptUrl, 'PUT');
$notification->addAction($acceptAction);
$this->notificationManager->notify($notification);
}
}
/**
* @param string $object
* @param string $objectId
*/
public function deleteNotification(string $object, string $objectId) {
// if ($objectId === '') {
// return;
// }
//
// $notification = $this->notificationManager->createNotification();
// $notification->setApp('circles')
// ->setObject($object, $objectId);
//
// $this->notificationManager->markProcessed($notification);
}
/**
* @param string $userId
* @param string $memberId
* @param string $subject
*
* @return INotification
*/
private function createMemberNotification(
string $userId,
string $memberId,
string $subject
): INotification {
$notification = $this->notificationManager->createNotification();
$notification->setApp('circles')
->setDateTime($this->timezoneService->getDateTime())
->setUser($userId)
->setObject('member', $memberId)
->setSubject($subject);
return $notification;
}
/**
* @param string $route
* @param array $params
*
* @return string
*/
private function linkToOCS(string $route, array $params = []): string {
$absolute = $this->urlGenerator->linkToOCSRouteAbsolute($route, $params);
return parse_url($absolute, PHP_URL_PATH);
}
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\Migration\IOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class OutputService
*
* @package OCA\Circles\Service
*/
class OutputService {
use TStringTools;
use TNCLogger;
/** @var IOutput */
private $migrationOutput;
/** @var OutputInterface */
private $occOutput;
public function __construct() {
}
/**
* @param OutputInterface $output
*/
public function setOccOutput(OutputInterface $output): void {
$this->occOutput = $output;
}
/**
* @param IOutput $output
*/
public function setMigrationOutput(IOutput $output): void {
$this->migrationOutput = $output;
}
/**
* @param string $message
* @param bool $advance
*/
public function output(string $message, bool $advance = false): void {
if (!is_null($this->occOutput)) {
$this->occOutput->writeln((($advance) ? '+' : '-') . ' ' . $message);
}
if (!is_null($this->migrationOutput)) {
if ($advance) {
$this->migrationOutput->advance(1, '(Circles) ' . $message);
} else {
$this->migrationOutput->info('(Circles) ' . $message);
}
}
}
/**
* @param int $int
*/
public function startMigrationProgress(int $int): void {
if (is_null($this->migrationOutput)) {
return;
}
$this->migrationOutput->startProgress($int);
}
/**
*
*/
public function finishMigrationProgress(): void {
if (is_null($this->migrationOutput)) {
return;
}
$this->migrationOutput->finishProgress();
}
}
@@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2022
* @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\Circles\Service;
use OCA\Circles\Exceptions\InitiatorNotFoundException;
use OCA\Circles\Exceptions\InsufficientPermissionException;
use OCA\Circles\Exceptions\MembershipNotFoundException;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Model\Circle;
use OCP\IL10N;
class PermissionService {
/** @var IL10N */
private $l10n;
/** @var FederatedUserService */
private $federatedUserService;
/** @var ConfigService */
private $configService;
/**
* @param IL10N $l10n
* @param FederatedUserService $federatedUserService
* @param ConfigService $configService
*/
public function __construct(
IL10N $l10n,
FederatedUserService $federatedUserService,
ConfigService $configService
) {
$this->l10n = $l10n;
$this->federatedUserService = $federatedUserService;
$this->configService = $configService;
}
/**
* @throws RequestBuilderException
* @throws InitiatorNotFoundException
* @throws InsufficientPermissionException
*/
public function confirmCircleCreation(): void {
try {
$this->confirm(ConfigService::LIMIT_CIRCLE_CREATION);
} catch (InsufficientPermissionException $e) {
throw new InsufficientPermissionException(
$this->l10n->t('You have no permission to create a new circle')
);
}
}
/**
* @param string $config
*
* @throws InsufficientPermissionException
* @throws RequestBuilderException
* @throws InitiatorNotFoundException
*/
private function confirm(string $config): void {
$singleId = $this->configService->getAppValue($config);
if ($singleId === '') {
return;
}
$this->federatedUserService->mustHaveCurrentUser();
$federatedUser = $this->federatedUserService->getCurrentUser();
try {
$federatedUser->getLink($singleId);
} catch (MembershipNotFoundException $e) {
throw new InsufficientPermissionException();
}
}
/**
* @param Circle $circle
*
* @return bool
* @throws RequestBuilderException
*/
private function canBypassCircleTypes(Circle $circle): bool {
try {
if (!$circle->hasInitiator()) {
throw new MembershipNotFoundException();
}
$circle->getInitiator()->getLink(
$this->configService->getAppValue(ConfigService::BYPASS_CIRCLE_TYPES)
);
return true;
} catch (MembershipNotFoundException $e) {
}
return false;
}
/**
* Enforce or Block circle's config/type
*
* @param Circle $circle
* @param Circle|null $previous
*
* @throws RequestBuilderException
*/
public function confirmAllowedCircleTypes(Circle $circle, ?Circle $previous = null): void {
if ($this->canBypassCircleTypes($circle)) {
return;
}
$config = $circle->getConfig();
$force = $this->configService->getAppValueInt(ConfigService::CIRCLE_TYPES_FORCE);
$block = $this->configService->getAppValueInt(ConfigService::CIRCLE_TYPES_BLOCK);
if (is_null($previous)) {
$config |= $force;
$config &= ~$block;
} else {
// if we have a previous entry, we compare old and new config.
foreach (array_merge($this->extractBitwise($force), $this->extractBitwise($block)) as $bit) {
if ($previous->isConfig($bit)) {
$config |= $bit;
} else {
$config &= ~$bit;
}
}
}
$circle->setConfig($config);
}
/**
* @return int[]
*/
private function extractBitwise(int $bitwise): array {
$values = [];
$b = 1;
while ($b <= $bitwise) {
if (($bitwise & $b) !== 0) {
$values[] = $b;
}
$b = $b << 1;
}
return $values;
}
}
@@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Tools\Exceptions\InvalidItemException;
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
use OCA\Circles\Tools\Exceptions\SignatoryException;
use OCA\Circles\Tools\Traits\TAsync;
use OCA\Circles\Tools\Traits\TNCLogger;
use Exception;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\FederatedEventDSyncException;
use OCA\Circles\Exceptions\FederatedEventException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
use OCA\Circles\Exceptions\InvalidIdException;
use OCA\Circles\Exceptions\MemberNotFoundException;
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\Model\Federated\FederatedEvent;
use OCA\Circles\Model\Probes\CircleProbe;
/**
* Class RemoteDownstreamService
*
* @package OCA\Circles\Service
*/
class RemoteDownstreamService {
use TNCLogger;
use TAsync;
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var FederatedEventService */
private $federatedEventService;
/** @var RemoteService */
private $remoteService;
/** @var ConfigService */
private $configService;
/**
* RemoteDownstreamService constructor.
*
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param FederatedEventService $federatedEventService
* @param ConfigService $configService
*/
public function __construct(
CircleRequest $circleRequest,
MemberRequest $memberRequest,
FederatedEventService $federatedEventService,
RemoteService $remoteService,
ConfigService $configService
) {
$this->setup('app', 'circles');
$this->circleRequest = $circleRequest;
$this->memberRequest = $memberRequest;
$this->federatedEventService = $federatedEventService;
$this->remoteService = $remoteService;
$this->configService = $configService;
}
//
//
// /**
// * @param GSEvent $event
// *
// * @throws CircleDoesNotExistException
// * @throws ConfigNoCircleAvailableException
// * @throws GSKeyException
// * @throws GlobalScaleDSyncException
// * @throws GlobalScaleEventException
// */
// public function statusEvent(GSEvent $event) {
// $this->globalScaleService->checkEvent($event);
//
// $gs = $this->globalScaleService->getGlobalScaleEvent($event);
// $gs->verify($event, false);
// $gs->manage($event);
// }
//
/**
* @param FederatedEvent $event
*
* @throws FederatedEventDSyncException
* @throws FederatedEventException
* @throws InitiatorNotConfirmedException
* @throws OwnerNotFoundException
* @throws FederatedItemException
* @throws RequestBuilderException
*/
public function requestedEvent(FederatedEvent $event): void {
$item = $this->federatedEventService->getFederatedItem($event, true);
if (!$this->configService->isLocalInstance($event->getCircle()->getInstance())) {
throw new FederatedEventDSyncException('Circle is not from this instance');
}
if ($event->isLimitedToInstanceWithMember()) {
$instances = $this->memberRequest->getMemberInstances($event->getCircle()->getSingleId());
if (!in_array($event->getSender(), $instances)) {
throw new FederatedEventException('Instance have no members in this Circle');
}
}
$event->setOrigin($event->getSender());
$event->resetData();
$this->federatedEventService->confirmInitiator($event, false);
$this->confirmContent($event, true);
$item->verify($event);
$event->resetResult();
if ($event->isDataRequestOnly()) {
return;
}
if (!$event->isAsync()) {
$item->manage($event);
}
$this->federatedEventService->initBroadcast($event);
}
/**
* @param FederatedEvent $event
*/
public function incomingEvent(FederatedEvent $event): void {
try {
$gs = $this->federatedEventService->getFederatedItem($event, false);
$this->confirmOriginEvent($event);
$this->confirmContent($event, false);
$gs->manage($event);
} catch (Exception $e) {
$this->e($e, ['event' => $event->getWrapperToken()]);
}
}
/**
* @param FederatedEvent $event
* @param bool $full
*
* @throws FederatedEventDSyncException
* @throws OwnerNotFoundException
*/
private function confirmContent(FederatedEvent $event, bool $full = true): void {
$this->confirmCircle($event);
$this->confirmMember($event, $full);
}
/**
* @param FederatedEvent $event
*
* @throws CircleNotFoundException
* @throws FederatedEventDSyncException
* @throws InvalidIdException
* @throws InvalidItemException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws RequestNetworkException
* @throws SignatoryException
* @throws UnknownRemoteException
*/
private function confirmCircle(FederatedEvent $event): void {
if ($event->canBypass(FederatedEvent::BYPASS_LOCALCIRCLECHECK) || $this->verifyCircle($event)) {
return;
}
throw new FederatedEventDSyncException('Could not verify Circle');
}
/**
* @param FederatedEvent $event
*
* @return bool
* @throws CircleNotFoundException
* @throws OwnerNotFoundException
* @throws RequestBuilderException
* @throws InvalidIdException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws InvalidItemException
* @throws RequestNetworkException
* @throws SignatoryException
*/
private function verifyCircle(FederatedEvent $event): bool {
$circle = $event->getCircle();
try {
$probe = new CircleProbe();
$probe->includeSystemCircles()
->includePersonalCircles();
$localCircle = $this->circleRequest->getCircle($circle->getSingleId(), null, $probe);
} catch (CircleNotFoundException $e) {
try {
$this->remoteService->syncRemoteCircle(
$circle->getSingleId(),
$circle->getOwner()->getInstance()
);
return true;
} catch (Exception $e) {
return false;
}
}
if (!$localCircle->compareWith($circle)) {
$this->debug(
'failed to compare Circles',
['localCircle' => json_encode($localCircle), 'circle' => json_encode($circle)]
);
return false;
}
return true;
}
/**
* @param FederatedEvent $event
* @param bool $full
* // TODO: Check IFederatedItemMember*
*
* @throws FederatedEventDSyncException
*/
private function confirmMember(FederatedEvent $event, bool $full): void {
if ($event->canBypass(FederatedEvent::BYPASS_LOCALMEMBERCHECK) || !$event->hasMember()
|| $this->verifyMember($event, $full)) {
return;
}
throw new FederatedEventDSyncException('Could not verify Member');
}
/**
* @param FederatedEvent $event
* @param bool $full
*
* @return bool
*/
private function verifyMember(FederatedEvent $event, bool $full): bool {
$this->debug('verifyMember()', ['event' => $event]);
$member = $event->getMember();
try {
$localMember = $this->memberRequest->getMemberById($member->getId());
} catch (MemberNotFoundException $e) {
$this->debug('Member not found', ['member' => $member]);
return false;
}
if (!$localMember->compareWith($member, $full)) {
$this->debug(
'failed to compare Members',
['localMember' => json_encode($localMember), 'member' => json_encode($member)]
);
return false;
}
return true;
}
/**
* @param FederatedEvent $event
*
* @throws FederatedEventException
* @throws OwnerNotFoundException
*/
private function confirmOriginEvent(FederatedEvent $event): void {
if ($event->getSender() !== $event->getCircle()->getInstance()) {
$this->debug('invalid origin', ['event' => $event]);
throw new FederatedEventException('invalid origin');
}
}
}
@@ -0,0 +1,418 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\FederatedUserException;
use OCA\Circles\Exceptions\FederatedUserNotFoundException;
use OCA\Circles\Exceptions\InvalidIdException;
use OCA\Circles\Exceptions\OwnerNotFoundException;
use OCA\Circles\Exceptions\RemoteInstanceException;
use OCA\Circles\Exceptions\RemoteNotFoundException;
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Federated\RemoteInstance;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Membership;
use OCA\Circles\Tools\ActivityPub\NCSignature;
use OCA\Circles\Tools\Exceptions\InvalidItemException;
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
use OCA\Circles\Tools\Exceptions\SignatoryException;
use OCA\Circles\Tools\Model\Request;
use OCA\Circles\Tools\Model\SimpleDataStore;
/**
* Class RemoteService
*
* @package OCA\Circles\Service
*/
class RemoteService extends NCSignature {
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var RemoteStreamService */
private $remoteStreamService;
/** @var MembershipService */
private $membershipService;
/** @var ShareService */
private $shareService;
/**
* RemoteService constructor.
*
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param RemoteStreamService $remoteStreamService
* @param MembershipService $membershipService
* @param ShareService $shareService
*/
public function __construct(
CircleRequest $circleRequest, MemberRequest $memberRequest, RemoteStreamService $remoteStreamService,
MembershipService $membershipService, ShareService $shareService
) {
$this->setup('app', 'circles');
$this->circleRequest = $circleRequest;
$this->memberRequest = $memberRequest;
$this->remoteStreamService = $remoteStreamService;
$this->membershipService = $membershipService;
$this->shareService = $shareService;
}
/**
* @param string $instance
* @param array $data
*
* @return Circle[]
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestNetworkException
* @throws SignatoryException
* @throws UnknownRemoteException
*/
public function getCirclesFromInstance(string $instance, array $data = []): array {
$result = $this->remoteStreamService->resultRequestRemoteInstance(
$instance,
RemoteInstance::CIRCLES,
Request::TYPE_GET,
new SimpleDataStore($data)
);
$circles = [];
foreach ($result as $item) {
try {
$circle = new Circle();
$circle->import($item);
$circles[] = $circle;
} catch (InvalidItemException $e) {
}
}
return $circles;
}
/**
* @param string $circleId
* @param string $instance
* @param array $data
*
* @return Circle
* @throws CircleNotFoundException
* @throws InvalidItemException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws FederatedItemException
*/
public function getCircleFromInstance(string $circleId, string $instance, array $data = []): Circle {
// TODO: check that $instance is not Local !!
$result = $this->remoteStreamService->resultRequestRemoteInstance(
$instance,
RemoteInstance::CIRCLE,
Request::TYPE_GET,
new SimpleDataStore($data),
['circleId' => $circleId]
);
if (empty($result)) {
throw new CircleNotFoundException();
}
$circle = new Circle();
$circle->import($result);
return $circle;
}
/**
* @param string $circleId
* @param string $instance
* @param array $data
*
* @return Member[]
* @throws FederatedItemException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
*/
public function getMembersFromInstance(string $circleId, string $instance, array $data = []): array {
$result = $this->remoteStreamService->resultRequestRemoteInstance(
$instance,
RemoteInstance::MEMBERS,
Request::TYPE_GET,
new SimpleDataStore($data),
['circleId' => $circleId]
);
$members = [];
foreach ($result as $item) {
try {
$member = new Member();
$member->import($item);
$members[] = $member;
} catch (InvalidItemException $e) {
}
}
return $members;
}
/**
* @param string $circleId
* @param string $instance
* @param array $data
*
* @return Member[]
* @throws FederatedItemException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
*/
public function getInheritedFromInstance(string $circleId, string $instance, array $data = []): array {
$result = $this->remoteStreamService->resultRequestRemoteInstance(
$instance,
RemoteInstance::INHERITED,
Request::TYPE_GET,
new SimpleDataStore($data),
['circleId' => $circleId]
);
$members = [];
foreach ($result as $item) {
try {
$member = new Member();
$member->import($item);
$members[] = $member;
} catch (InvalidItemException $e) {
}
}
return $members;
}
/**
* @param string $circleId
* @param string $instance
* @param array $data
*
* @return Membership[]
* @throws FederatedItemException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
*/
public function getMembershipsFromInstance(string $circleId, string $instance, array $data = []): array {
$result = $this->remoteStreamService->resultRequestRemoteInstance(
$instance,
RemoteInstance::MEMBERSHIPS,
Request::TYPE_GET,
new SimpleDataStore($data),
['circleId' => $circleId]
);
$members = [];
foreach ($result as $item) {
try {
$member = new Membership();
$member->import($item);
$members[] = $member;
} catch (InvalidItemException $e) {
}
}
return $members;
}
/**
* @param Circle $circle
*
* @throws CircleNotFoundException
* @throws InvalidIdException
* @throws InvalidItemException
* @throws OwnerNotFoundException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestNetworkException
* @throws SignatoryException
* @throws UnknownRemoteException
* @throws RemoteInstanceException
*/
public function syncCircle(Circle $circle): void {
// if ($this->configService->isLocalInstance($circle->getInstance())) {
// $this->syncLocalCircle($circle);
// } else {
$this->syncRemoteCircle($circle->getSingleId(), $circle->getInstance());
// }
}
/**
* @param Circle $circle
*/
private function syncLocalCircle(Circle $circle): void {
}
/**
* @param string $circleId
* @param string $instance
*
* @throws InvalidItemException
* @throws OwnerNotFoundException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestNetworkException
* @throws SignatoryException
* @throws UnknownRemoteException
* @throws CircleNotFoundException
* @throws InvalidIdException
* @throws RemoteInstanceException
*/
public function syncRemoteCircle(string $circleId, string $instance): void {
$loop = 0;
$knownInstance = [];
while (true) {
$loop++;
if ($loop > 10 || in_array($instance, $knownInstance)) {
throw new CircleNotFoundException(
'circle not found after browsing ' . implode(', ', $knownInstance)
);
}
$knownInstance[] = $instance;
$circle = $this->getCircleFromInstance($circleId, $instance);
if ($circle->getInstance() === $instance) {
break;
}
$instance = $circle->getInstance();
}
$this->circleRequest->insertOrUpdate($circle);
$this->memberRequest->insertOrUpdate($circle->getOwner());
$this->syncRemoteMembers($circle);
$this->membershipService->onUpdate($circle->getSingleId());
$this->shareService->syncRemoteShares($circle);
}
/**
* @param Circle $circle
*
* @throws OwnerNotFoundException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestNetworkException
* @throws SignatoryException
* @throws UnknownRemoteException
* @throws RemoteInstanceException
*/
public function syncRemoteMembers(Circle $circle) {
$members = $this->getMembersFromInstance($circle->getSingleId(), $circle->getInstance());
foreach ($members as $member) {
try {
$this->memberRequest->insertOrUpdate($member);
} catch (InvalidIdException $e) {
}
}
$this->membershipService->onUpdate($circle->getSingleId());
}
/**
* @param string $userId
* @param string $instance
* @param int $type
*
* @return FederatedUser
* @throws FederatedUserNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws FederatedUserException
* @throws FederatedItemException
*/
public function getFederatedUserFromInstance(
string $userId,
string $instance,
int $type = Member::TYPE_USER
): FederatedUser {
$result = $this->remoteStreamService->resultRequestRemoteInstance(
$instance,
RemoteInstance::MEMBER,
Request::TYPE_GET,
null,
['type' => Member::$TYPE[$type], 'userId' => $userId]
);
if (empty($result)) {
throw new FederatedUserNotFoundException();
}
$federatedUser = new FederatedUser();
try {
$federatedUser->import($result);
} catch (InvalidItemException $e) {
throw new FederatedUserException('incorrect federated user returned from instance');
}
if ($federatedUser->getInstance() !== $instance) {
throw new FederatedUserException('incorrect instance on returned federated user');
}
return $federatedUser;
}
}
@@ -0,0 +1,628 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use JsonSerializable;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\RemoteRequest;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\RemoteAlreadyExistsException;
use OCA\Circles\Exceptions\RemoteInstanceException;
use OCA\Circles\Exceptions\RemoteNotFoundException;
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
use OCA\Circles\Exceptions\RemoteUidException;
use OCA\Circles\Exceptions\UnknownInterfaceException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\Model\Federated\RemoteInstance;
use OCA\Circles\Tools\ActivityPub\NCSignature;
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
use OCA\Circles\Tools\Exceptions\SignatoryException;
use OCA\Circles\Tools\Exceptions\SignatureException;
use OCA\Circles\Tools\Exceptions\WellKnownLinkNotFoundException;
use OCA\Circles\Tools\Model\NCRequest;
use OCA\Circles\Tools\Model\NCRequestResult;
use OCA\Circles\Tools\Model\NCSignatory;
use OCA\Circles\Tools\Model\NCSignedRequest;
use OCA\Circles\Tools\Model\Request;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TDeserialize;
use OCA\Circles\Tools\Traits\TNCLocalSignatory;
use OCA\Circles\Tools\Traits\TNCWellKnown;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\AppFramework\Http;
use OCP\IURLGenerator;
use ReflectionClass;
use ReflectionException;
/**
* Class RemoteStreamService
*
* @package OCA\Circles\Service
*/
class RemoteStreamService extends NCSignature {
use TDeserialize;
use TNCLocalSignatory;
use TStringTools;
use TNCWellKnown;
public const UPDATE_DATA = 'data';
public const UPDATE_ITEM = 'item';
public const UPDATE_TYPE = 'type';
public const UPDATE_INSTANCE = 'instance';
public const UPDATE_HREF = 'href';
/** @var IURLGenerator */
private $urlGenerator;
/** @var RemoteRequest */
private $remoteRequest;
/** @var InterfaceService */
private $interfaceService;
/** @var ConfigService */
private $configService;
/**
* RemoteStreamService constructor.
*
* @param IURLGenerator $urlGenerator
* @param RemoteRequest $remoteRequest
* @param InterfaceService $interfaceService
* @param ConfigService $configService
*/
public function __construct(
IURLGenerator $urlGenerator,
RemoteRequest $remoteRequest,
InterfaceService $interfaceService,
ConfigService $configService
) {
$this->setup('app', 'circles');
$this->urlGenerator = $urlGenerator;
$this->remoteRequest = $remoteRequest;
$this->interfaceService = $interfaceService;
$this->configService = $configService;
}
/**
* Returns the Signatory model for the Circles app.
* Can be signed with a confirmKey.
*
* @param bool $generate
* @param string $confirmKey
*
* @return RemoteInstance
* @throws SignatoryException
* @throws UnknownInterfaceException
*/
public function getAppSignatory(bool $generate = true, string $confirmKey = ''): RemoteInstance {
$app = new RemoteInstance($this->interfaceService->getCloudPath('circles.Remote.appService'));
$this->fillSimpleSignatory($app, $generate);
$app->setUidFromKey();
if ($this->isUuid($confirmKey)) {
$app->setAuthSigned($this->signString($confirmKey, $app));
}
$app->setRoot($this->interfaceService->getCloudPath());
$app->setEvent($this->interfaceService->getCloudPath('circles.Remote.event'));
$app->setIncoming($this->interfaceService->getCloudPath('circles.Remote.incoming'));
$app->setTest($this->interfaceService->getCloudPath('circles.Remote.test'));
$app->setCircles($this->interfaceService->getCloudPath('circles.Remote.circles'));
$app->setCircle(
urldecode(
$this->interfaceService->getCloudPath('circles.Remote.circle', ['circleId' => '{circleId}'])
)
);
$app->setMembers(
urldecode(
$this->interfaceService->getCloudPath('circles.Remote.members', ['circleId' => '{circleId}'])
)
);
$app->setMember(
urldecode(
$this->interfaceService->getCloudPath(
'circles.Remote.member',
['type' => '{type}', 'userId' => '{userId}']
)
)
);
$app->setInherited(
urldecode(
$this->interfaceService->getCloudPath(
'circles.Remote.inherited',
['circleId' => '{circleId}']
)
)
);
$app->setMemberships(
urldecode(
$this->interfaceService->getCloudPath(
'circles.Remote.memberships',
['circleId' => '{circleId}']
)
)
);
if ($this->interfaceService->isCurrentInterfaceInternal()) {
$app->setAliases(array_values(array_filter($this->interfaceService->getInterfaces(false))));
}
$app->setOrigData($this->serialize($app));
return $app;
}
/**
* Reset the Signatory (and the Identity) for the Circles app.
*/
public function resetAppSignatory(): void {
try {
$app = $this->getAppSignatory();
$this->removeSimpleSignatory($app);
} catch (SignatoryException $e) {
}
}
/**
* shortcut to requestRemoteInstance that return result if available, or exception.
*
* @param string $instance
* @param string $item
* @param int $type
* @param JsonSerializable|null $object
* @param array $params
*
* @return array
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws FederatedItemException
*/
public function resultRequestRemoteInstance(
string $instance,
string $item,
int $type = Request::TYPE_GET,
?JsonSerializable $object = null,
array $params = []
): array {
$this->interfaceService->setCurrentInterfaceFromInstance($instance);
$signedRequest = $this->requestRemoteInstance($instance, $item, $type, $object, $params);
if (!$signedRequest->getOutgoingRequest()->hasResult()) {
throw new RemoteInstanceException();
}
$result = $signedRequest->getOutgoingRequest()->getResult();
if ($result->getStatusCode() === Http::STATUS_OK) {
return $result->getAsArray();
}
throw $this->getFederatedItemExceptionFromResult($result);
}
/**
* Send a request to a remote instance, based on:
* - instance: address as saved in database,
* - item: the item to request (incoming, event, ...)
* - type: GET, POST
* - data: Serializable to be send if needed
*
* @param string $instance
* @param string $item
* @param int $type
* @param JsonSerializable|null $object
* @param array $params
*
* @return NCSignedRequest
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws RemoteInstanceException
* @throws UnknownInterfaceException
*/
private function requestRemoteInstance(
string $instance,
string $item,
int $type = Request::TYPE_GET,
?JsonSerializable $object = null,
array $params = []
): NCSignedRequest {
$request = new NCRequest('', $type);
$this->configService->configureRequest($request);
$link = $this->getRemoteInstanceEntry($instance, $item, $params);
$request->basedOnUrl($link);
// TODO: Work Around: on local, if object is empty, request takes 10s. check on other configuration
if (is_null($object) || empty($object->jsonSerialize())) {
$object = new SimpleDataStore(['empty' => 1]);
}
if (!is_null($object)) {
$request->setDataSerialize($object);
}
try {
$app = $this->getAppSignatory();
// $app->setAlgorithm(NCSignatory::SHA512);
$signedRequest = $this->signOutgoingRequest($request, $app);
$this->doRequest($signedRequest->getOutgoingRequest(), false);
} catch (RequestNetworkException | SignatoryException $e) {
throw new RemoteInstanceException($e->getMessage());
}
return $signedRequest;
}
/**
* get the value of an entry from the Signatory of the RemoteInstance.
*
* @param string $instance
* @param string $item
* @param array $params
*
* @return string
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
*/
private function getRemoteInstanceEntry(string $instance, string $item, array $params = []): string {
$remote = $this->getCachedRemoteInstance($instance);
$value = $this->get($item, $remote->getOrigData());
if ($value === '') {
throw new RemoteResourceNotFoundException();
}
return $this->feedStringWithParams($value, $params);
}
/**
* get RemoteInstance with confirmed and known identity from database.
*
* @param string $instance
*
* @return RemoteInstance
* @throws RemoteNotFoundException
* @throws UnknownRemoteException
*/
public function getCachedRemoteInstance(string $instance): RemoteInstance {
$remoteInstance = $this->remoteRequest->getFromInstance($instance);
if ($remoteInstance->getType() === RemoteInstance::TYPE_UNKNOWN) {
throw new UnknownRemoteException($instance . ' is set as \'unknown\' in database');
}
return $remoteInstance;
}
/**
* Add a remote instance, based on the address
*
* @param string $instance
*
* @return RemoteInstance
* @throws RequestNetworkException
* @throws SignatoryException
* @throws SignatureException
* @throws WellKnownLinkNotFoundException
*/
public function retrieveRemoteInstance(string $instance): RemoteInstance {
$resource = $this->getResourceData($instance, Application::APP_SUBJECT, Application::APP_REL);
/** @var RemoteInstance $remoteInstance */
$remoteInstance = $this->retrieveSignatory($resource->g('id'), true);
$remoteInstance->setInstance($instance);
return $remoteInstance;
}
/**
* retrieve Signatory.
*
* @param string $keyId
* @param bool $refresh
*
* @return RemoteInstance
* @throws SignatoryException
* @throws SignatureException
*/
public function retrieveSignatory(string $keyId, bool $refresh = true): NCSignatory {
if (!$refresh) {
try {
return $this->remoteRequest->getFromHref(NCSignatory::removeFragment($keyId));
} catch (RemoteNotFoundException $e) {
throw new SignatoryException();
}
}
$remoteInstance = new RemoteInstance($keyId);
$confirm = $this->uuid();
$request = new NCRequest();
$this->configService->configureRequest($request);
$this->downloadSignatory($remoteInstance, $keyId, ['auth' => $confirm], $request);
$remoteInstance->setUidFromKey();
$this->confirmAuth($remoteInstance, $confirm);
return $remoteInstance;
}
/**
* Add a remote instance, based on the address
*
* @param string $instance
* @param string $type
* @param int $iface
* @param bool $overwrite
*
* @throws RemoteAlreadyExistsException
* @throws RemoteUidException
* @throws RequestNetworkException
* @throws SignatoryException
* @throws SignatureException
* @throws WellKnownLinkNotFoundException
*/
public function addRemoteInstance(
string $instance,
string $type = RemoteInstance::TYPE_EXTERNAL,
int $iface = InterfaceService::IFACE_FRONTAL,
bool $overwrite = false
): void {
if ($this->configService->isLocalInstance($instance)) {
throw new RemoteAlreadyExistsException('instance is local');
}
$remoteInstance = $this->retrieveRemoteInstance($instance);
$remoteInstance->setType($type)
->setInterface($iface);
if (!$this->interfaceService->isInterfaceInternal($remoteInstance->getInterface())) {
$remoteInstance->setAliases([]);
}
try {
$known = $this->remoteRequest->searchDuplicate($remoteInstance);
if ($overwrite) {
$this->remoteRequest->deleteById($known);
} else {
throw new RemoteAlreadyExistsException('instance is already known');
}
} catch (RemoteNotFoundException $e) {
}
$this->remoteRequest->save($remoteInstance);
}
/**
* @param string $address
*
* @return RemoteInstance
* @throws RemoteNotFoundException
*/
public function getRemoteInstanceFromAddress(string $address): RemoteInstance {
$remotes = $this->remoteRequest->getAllInstances();
foreach ($remotes as $remote) {
if ($remote->getInstance() === $address || in_array($address, $remote->getAliases())) {
return $remote;
}
}
throw new RemoteNotFoundException();
}
/**
* @param string $instance
* @param string $check
*
* @return bool
* @throws RemoteNotFoundException
*/
public function isFromSameInstance(string $instance, string $check): bool {
$remote = $this->getRemoteInstanceFromAddress($instance);
if ($remote->getInstance() === $check || in_array($check, $remote->getAliases())) {
return true;
}
return false;
}
/**
* Confirm the Auth of a RemoteInstance, based on the result from a request
*
* @param RemoteInstance $remote
* @param string $auth
*
* @throws SignatureException
*/
private function confirmAuth(RemoteInstance $remote, string $auth): void {
[$algo, $signed] = explode(':', $this->get(RemoteInstance::AUTH_SIGNED, $remote->getOrigData()));
try {
if ($signed === null) {
throw new SignatureException('invalid auth-signed');
}
$this->verifyString($auth, base64_decode($signed), $remote->getPublicKey(), $algo);
$remote->setIdentityAuthed(true);
} catch (SignatureException $e) {
$this->e($e, [
'auth' => $auth,
'signed' => $signed,
'msg' => 'auth not confirmed'
]
);
throw new SignatureException('auth not confirmed');
}
}
/**
* @param NCRequestResult $result
*
* @return FederatedItemException
*/
private function getFederatedItemExceptionFromResult(NCRequestResult $result): FederatedItemException {
$data = $result->getAsArray();
$message = $this->get('message', $data);
$code = $this->getInt('code', $data);
$class = $this->get('class', $data);
try {
$test = new ReflectionClass($class);
$this->confirmFederatedItemExceptionFromClass($test);
$e = $class;
} catch (ReflectionException | FederatedItemException $_e) {
$e = $this->getFederatedItemExceptionFromStatus($result->getStatusCode());
}
return new $e($message, $code);
}
/**
* @param ReflectionClass $class
*
* @return void
* @throws FederatedItemException
*/
private function confirmFederatedItemExceptionFromClass(ReflectionClass $class): void {
while (true) {
foreach (FederatedItemException::$CHILDREN as $e) {
if ($class->getName() === $e) {
return;
}
}
$class = $class->getParentClass();
if (!$class) {
throw new FederatedItemException();
}
}
}
/**
* @param int $statusCode
*
* @return string
*/
private function getFederatedItemExceptionFromStatus(int $statusCode): string {
foreach (FederatedItemException::$CHILDREN as $e) {
if ($e::STATUS === $statusCode) {
return $e;
}
}
return FederatedItemException::class;
}
/**
* TODO: confirm if method is really needed
*
* @param RemoteInstance $remote
* @param RemoteInstance|null $stored
*
* @throws RemoteNotFoundException
* @throws RemoteUidException
*/
public function confirmValidRemote(RemoteInstance $remote, ?RemoteInstance &$stored = null): void {
try {
$stored = $this->remoteRequest->getFromHref($remote->getId());
} catch (RemoteNotFoundException $e) {
if ($remote->getInstance() === '') {
throw new RemoteNotFoundException();
}
$stored = $this->remoteRequest->getFromInstance($remote->getInstance());
}
if ($stored->getUid() !== $remote->getUid(true)) {
throw new RemoteUidException();
}
}
/**
* TODO: check if this method is not useless
*
* @param RemoteInstance $remote
* @param string $update
*
* @throws RemoteUidException
*/
public function update(RemoteInstance $remote, string $update = self::UPDATE_DATA): void {
if (!$this->interfaceService->isInterfaceInternal($remote->getInterface())) {
$remote->setAliases([]);
}
switch ($update) {
case self::UPDATE_DATA:
$this->remoteRequest->update($remote);
break;
case self::UPDATE_ITEM:
$this->remoteRequest->updateItem($remote);
break;
case self::UPDATE_TYPE:
$this->remoteRequest->updateType($remote);
break;
case self::UPDATE_HREF:
$this->remoteRequest->updateHref($remote);
break;
case self::UPDATE_INSTANCE:
$this->remoteRequest->updateInstance($remote);
break;
}
}
}
@@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Tools\Model\Request;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TNCRequest;
use OCA\Circles\Db\EventWrapperRequest;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\OwnerNotFoundException;
use OCA\Circles\Exceptions\RemoteInstanceException;
use OCA\Circles\Exceptions\RemoteNotFoundException;
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\Model\Federated\EventWrapper;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\Federated\RemoteInstance;
/**
* Class RemoteUpstreamService
*
* @package OCA\Circles\Service
*/
class RemoteUpstreamService {
use TNCRequest;
/** @var EventWrapperRequest */
private $eventWrapperRequest;
/** @var RemoteStreamService */
private $remoteStreamService;
/** @var InterfaceService */
private $interfaceService;
/** @var ConfigService */
private $configService;
/**
* RemoteUpstreamService constructor.
*
* @param EventWrapperRequest $eventWrapperRequest
* @param RemoteStreamService $remoteStreamService
* @param InterfaceService $interfaceService
* @param ConfigService $configService
*/
public function __construct(
EventWrapperRequest $eventWrapperRequest,
RemoteStreamService $remoteStreamService,
InterfaceService $interfaceService,
ConfigService $configService
) {
$this->eventWrapperRequest = $eventWrapperRequest;
$this->remoteStreamService = $remoteStreamService;
$this->interfaceService = $interfaceService;
$this->configService = $configService;
}
/**
* @param string $token
*
* @return EventWrapper[]
*/
public function getEventsByToken(string $token): array {
return $this->eventWrapperRequest->getByToken($token);
}
/**
* @param EventWrapper $wrapper
*
* @throws FederatedItemException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
*/
public function broadcastEvent(EventWrapper $wrapper): void {
$event = clone $wrapper->getEvent();
$event->resetInternal();
$this->interfaceService->setCurrentInterface($wrapper->getInterface());
$data = $this->remoteStreamService->resultRequestRemoteInstance(
$wrapper->getInstance(),
RemoteInstance::INCOMING,
Request::TYPE_POST,
$event
);
$wrapper->getEvent()->setResult(new SimpleDataStore($data));
}
/**
* @param FederatedEvent $event
*
* @throws FederatedItemException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
*/
public function confirmEvent(FederatedEvent $event): void {
$instance = $event->getCircle()->getInstance();
$data = $this->remoteStreamService->resultRequestRemoteInstance(
$instance,
RemoteInstance::EVENT,
Request::TYPE_POST,
$event
);
$event->setOutcome($data);
}
//
//
//
// /**
// * @param array $sync
// *
// * @throws GSStatusException
// */
// public function synchronize(array $sync) {
// $this->configService->getGSStatus();
//
// $this->synchronizeCircles($sync);
// $this->removeDeprecatedCircles();
// $this->removeDeprecatedEvents();
// }
// /**
// * @param array $circles
// */
// public function synchronizeCircles(array $circles): void {
// $event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
// $event->setSource($this->configService->getLocalInstance());
// $event->setData(new SimpleDataStore($circles));
//
// foreach ($this->federatedEventService->getInstances() as $instance) {
// try {
// $this->broadcastEvent($event, $instance);
// } catch (RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException | RequestResultNotJsonException $e) {
// }
// }
// }
//
//
// /**
// *
// */
// private function removeDeprecatedCircles() {
// $knownCircles = $this->circlesRequest->forceGetCircles();
// foreach ($knownCircles as $knownItem) {
// if ($knownItem->getOwner()
// ->getInstance() === '') {
// continue;
// }
//
// try {
// $this->checkCircle($knownItem);
// } catch (GSStatusException $e) {
// }
// }
// }
//
//
// /**
// * @param DeprecatedCircle $circle
// *
// * @throws GSStatusException
// */
// private function checkCircle(DeprecatedCircle $circle): void {
// $status = $this->confirmCircleStatus($circle);
//
// if (!$status) {
// $this->circlesRequest->destroyCircle($circle->getUniqueId());
// $this->membersRequest->removeAllFromCircle($circle->getUniqueId());
// }
// }
//
//
// /**
// * @param DeprecatedCircle $circle
// *
// * @return bool
// * @throws GSStatusException
// */
// public function confirmCircleStatus(DeprecatedCircle $circle): bool {
// $event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
// $event->setSource($this->configService->getLocalInstance());
// $event->setDeprecatedCircle($circle);
//
// $this->signEvent($event);
//
// $path = $this->urlGenerator->linkToRoute('circles.EventWrapper.status');
// $request = new NC22Request($path, Request::TYPE_POST);
// $this->configService->configureRequest($request);
// $request->setDataSerialize($event);
//
// $requestIssue = false;
// $notFound = false;
// $foundWithNoOwner = false;
// foreach ($this->federatedEventService->getInstances() as $instance) {
// $request->setHost($instance);
//
// try {
// $result = $this->retrieveJson($request);
// if ($this->getInt('status', $result, 0) !== 1) {
// throw new RequestContentException('result status is not good');
// }
//
// $status = $this->getInt('success.data.status', $result);
//
// // if error, we assume the circle might still exist.
// if ($status === CircleStatus::STATUS_ERROR) {
// return true;
// }
//
// if ($status === CircleStatus::STATUS_OK) {
// return true;
// }
//
// // TODO: check the data.supposedOwner entry.
// if ($status === CircleStatus::STATUS_NOT_OWNER) {
// $foundWithNoOwner = true;
// }
//
// if ($status === CircleStatus::STATUS_NOT_FOUND) {
// $notFound = true;
// }
//
// } catch (RequestContentException
// | RequestNetworkException
// | RequestResultNotJsonException
// | RequestResultSizeException
// | RequestServerException $e) {
// $requestIssue = true;
// // TODO: log instances that have network issue, after too many tries (7d), remove this circle.
// continue;
// }
// }
//
// // if no request issue, we can imagine that the instance that owns the circle is down.
// // We'll wait for more information (cf request exceptions management);
// if ($requestIssue) {
// return true;
// }
//
// // circle were not found in any other instances, we can easily says that the circle does not exists anymore
// if ($notFound && !$foundWithNoOwner) {
// return false;
// }
//
// // circle were found everywhere but with no owner on every instance. we need to assign a new owner.
// // This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
// if (!$notFound && $foundWithNoOwner) {
// // TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
// return true;
// }
//
// // some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
// return false;
// }
//
// /**
// * @throws GSStatusException
// */
// public function syncEvents() {
//
// }
//
// /**
// *
// */
// private function removeDeprecatedEvents() {
//// $this->deprecatedEvents();
//
// }
}
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OC;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Exceptions\InitiatorNotFoundException;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\IFederatedUser;
use OCA\Circles\ISearch;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\SearchResult;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Search\FederatedUsers;
use OCA\Circles\Search\UnifiedSearchResult;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCP\IURLGenerator;
class SearchService {
use TArrayTools;
public static $SERVICES = [
FederatedUsers::class
];
/** @var IURLGenerator */
private $urlGenerator;
/** @var CircleService */
private $circleService;
/**
* @param IURLGenerator $urlGenerator
* @param CircleService $circleService
*/
public function __construct(
IURLGenerator $urlGenerator,
CircleService $circleService
) {
$this->urlGenerator = $urlGenerator;
$this->circleService = $circleService;
}
/**
* @param string $needle
*
* @return list<SearchResult|IFederatedUser>
*/
public function search(string $needle): array {
$result = [];
foreach (self::$SERVICES as $entry) {
/** @var ISearch $service */
$service = OC::$server->get($entry);
$result = array_merge($result, $service->search($needle));
}
return $result;
}
/**
* @param string $term
* @param array $options
*
* @return UnifiedSearchResult[]
* @throws RequestBuilderException
*/
public function unifiedSearch(string $term, array $options): array {
$result = [];
$probe = $this->generateSearchProbe($term, $options);
try {
$circles = $this->circleService->probeCircles($probe);
} catch (InitiatorNotFoundException $e) {
return [];
}
$iconPath = $this->urlGenerator->imagePath(Application::APP_ID, 'circles.svg');
$icon = $this->urlGenerator->getAbsoluteURL($iconPath);
foreach ($circles as $circle) {
$result[] = new UnifiedSearchResult(
'',
$circle->getDisplayName(),
$circle->getDescription(),
$circle->getUrl(),
$icon
);
}
return $result;
}
/**
* @param string $term
* @param array $options
*
* @return CircleProbe
*/
private function generateSearchProbe(string $term, array $options): CircleProbe {
$probe = new CircleProbe();
switch ($this->getInt('level', $options)) {
case Member::LEVEL_MEMBER:
$probe->mustBeMember();
break;
case Member::LEVEL_MODERATOR:
$probe->mustBeModerator();
break;
case Member::LEVEL_ADMIN:
$probe->mustBeAdmin();
break;
case Member::LEVEL_OWNER:
$probe->mustBeOwner();
break;
}
$probe->filterHiddenCircles()
->filterBackendCircles();
$circle = new Circle();
$circle->setDisplayName($term);
$probe->setFilterCircle($circle);
return $probe;
}
}
@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use Exception;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\ShareWrapper;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\Defaults;
use OCP\IL10N;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Security\IHasher;
use OCP\Util;
class SendMailService {
use TArrayTools;
use TStringTools;
/** @var IL10N */
private $l10n;
/** @var IHasher */
private $hasher;
/** @var IMailer */
private $mailer;
/** @var Defaults */
private $defaults;
/** @var ConfigService */
private $configService;
/**
* SendMailService constructor.
*
* @param IL10N $l10n
* @param IMailer $mailer
* @param Defaults $defaults
* @param ConfigService $configService
*/
public function __construct(
IL10N $l10n,
IHasher $hasher,
IMailer $mailer,
Defaults $defaults,
ConfigService $configService
) {
$this->l10n = $l10n;
$this->hasher = $hasher;
$this->mailer = $mailer;
$this->defaults = $defaults;
$this->configService = $configService;
}
/**
* @param string $author
* @param Circle $circle
* @param Member $member
* @param ShareWrapper[] $shares
* @param array $mails
* @param string $password
*/
public function generateMail(
string $author,
Circle $circle,
Member $member,
array $shares,
array $mails,
string $password = ''
): void {
if (empty($shares)) {
return;
}
if ($member->getUserType() === Member::TYPE_MAIL) {
$mails = [$member->getUserId()];
}
if (empty($mails)) {
return;
}
$links = [];
foreach ($shares as $share) {
$links[] = [
'filename' => $share->getFileTarget(),
'link' => $share->getShareToken()->getLink()
];
}
$template = $this->generateMailExitingShares(
$author,
$circle->getDisplayName(),
sizeof($links) > 1
);
$this->fillMailExistingShares($template, $links);
foreach ($mails as $mail) {
try {
$this->sendMailExistingShares($template, $author, $mail, sizeof($links) > 1);
} catch (Exception $e) {
}
$this->sendMailPassword($circle, $author, $mail, $password);
}
}
/**
* @param string $author
* @param string $circleName
* @param bool $multiple
*
* @return IEMailTemplate
*/
private function generateMailExitingShares(
string $author,
string $circleName,
bool $multiple = false
): IEMailTemplate {
$emailTemplate = $this->mailer->createEMailTemplate('circles.ExistingShareNotification', []);
$emailTemplate->addHeader();
if ($multiple) {
$text = $this->l10n->t('%s shared multiple files with "%s".', [$author, $circleName]);
} else {
$text = $this->l10n->t('%s shared a file with "%s".', [$author, $circleName]);
}
$text = $this->l10n->t('%s shared multiple files with "%s".', [$author, $circleName]);
$emailTemplate->addBodyText(htmlspecialchars($text), $text);
return $emailTemplate;
}
/**
* @param IEMailTemplate $emailTemplate
* @param array $links
*/
private function fillMailExistingShares(IEMailTemplate $emailTemplate, array $links) {
foreach ($links as $item) {
$emailTemplate->addBodyButton(
$this->l10n->t('Open »%s«', [htmlspecialchars($item['filename'])]), $item['link']
);
}
}
/**
* @param IEMailTemplate $emailTemplate
* @param string $author
* @param string $recipient
* @param bool $multiple
*
* @throws Exception
*/
private function sendMailExistingShares(
IEMailTemplate $emailTemplate,
string $author,
string $recipient,
bool $multiple = false
) {
if ($multiple) {
$subject = $this->l10n->t('%s shared multiple files with you.', [$author]);
} else {
$subject = $this->l10n->t('%s shared a file with you.', [$author]);
}
$instanceName = $this->defaults->getName();
$senderName = $this->l10n->t('%s on %s', [$author, $instanceName]);
$message = $this->mailer->createMessage();
$message->setFrom([Util::getDefaultEmailAddress($instanceName) => $senderName]);
$message->setSubject($subject);
$message->setPlainBody($emailTemplate->renderText());
$message->setHtmlBody($emailTemplate->renderHtml());
$message->setTo([$recipient]);
$this->mailer->send($message);
}
/**
* @param Circle $circle
* @param string $author
* @param string $email
* @param string $password
*
* @throws Exception
*/
private function sendMailPassword(
Circle $circle,
string $author,
string $email,
string $password
): void {
if (!$this->configService->sendPasswordByMail($circle) || $password === '') {
return;
}
$message = $this->mailer->createMessage();
$plainBodyPart = $this->l10n->t(
"%1\$s shared some content with you.\nYou should have already received a separate email with a link to access it.\n",
[$author]
);
$htmlBodyPart = $this->l10n->t(
'%1$s shared some content with you. You should have already received a separate email with a link to access it.',
[$author]
);
$emailTemplate = $this->mailer->createEMailTemplate(
'sharebymail.RecipientPasswordNotification',
[
'filename' => '',
'password' => $password,
'initiator' => $author,
// 'initiatorEmail' => Util::getDefaultEmailAddress(''),
'initiatorEmail' => '',
'shareWith' => $circle->getDisplayName()
]
);
$emailTemplate->setSubject(
$this->l10n->t('Password to access content shared with you by %1$s', [$author])
);
$emailTemplate->addHeader();
$emailTemplate->addHeading($this->l10n->t('Password to access content'), false);
$emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart);
$emailTemplate->addBodyText($this->l10n->t('It is protected with the following password:'));
$emailTemplate->addBodyText($password);
// The "From" contains the sharers name
$instanceName = $this->defaults->getName();
$senderName = $this->l10n->t(
'%1$s via %2$s',
[
$author,
$instanceName
]
);
$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
// if ($initiatorEmailAddress !== null) {
// $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
// $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
// } else {
$emailTemplate->addFooter();
// }
$message->setTo([$email]);
$message->useTemplate($emailTemplate);
$this->mailer->send($message);
}
/**
* @param Circle $circle
*
* @return array
*/
public function getPassword(Circle $circle): array {
$clearPassword = $hashedPassword = '';
if (!$this->configService->sendPasswordByMail($circle)) {
$hashedPassword = $this->get('password_single', $circle->getSettings());
}
if ($hashedPassword === '') {
$clearPassword = $this->token(14);
$hashedPassword = $this->hasher->hash($clearPassword);
}
return [$clearPassword, $hashedPassword];
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Federated\FederatedEvent;
/**
* Class ShareService
*
* @package OCA\Circles\Service
*/
class ShareService {
/** @var FederatedEventService */
private $federatedEventService;
/** @var ConfigService */
private $configService;
/**
* ShareService constructor.
*
* @param FederatedEventService $federatedEventService
* @param ConfigService $configService
*/
public function __construct(FederatedEventService $federatedEventService, ConfigService $configService) {
$this->federatedEventService = $federatedEventService;
$this->configService = $configService;
}
/**
* @param Circle $circle
*/
public function syncRemoteShares(Circle $circle) {
// $event = new FederatedEvent(SharedItemsSync::class);
// $event->setCircle($circle);
// $this->federatedEventService->newEvent($event);
}
}
@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use OCA\Circles\Db\ShareTokenRequest;
use OCA\Circles\Exceptions\ShareTokenAlreadyExistException;
use OCA\Circles\Exceptions\ShareTokenNotFoundException;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\ShareToken;
use OCA\Circles\Model\ShareWrapper;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\IURLGenerator;
use OCP\Share\IShare;
/**
* Class ShareTokenService
*
* @package OCA\Circles\Service
*/
class ShareTokenService {
use TStringTools;
/** @var IURLGenerator */
private $urlGenerator;
/** @var ShareTokenRequest */
private $shareTokenRequest;
/** @var ConfigService */
private $configService;
/** @var InterfaceService */
private $interfaceService;
/**
* ShareTokenService constructor.
*
* @param IURLGenerator $urlGenerator
* @param ShareTokenRequest $shareTokenRequest
* @param InterfaceService $interfaceService
* @param ConfigService $configService
*/
public function __construct(
IURLGenerator $urlGenerator,
ShareTokenRequest $shareTokenRequest,
InterfaceService $interfaceService,
ConfigService $configService
) {
$this->urlGenerator = $urlGenerator;
$this->shareTokenRequest = $shareTokenRequest;
$this->interfaceService = $interfaceService;
$this->configService = $configService;
}
/**
* @param ShareWrapper $share
* @param Member $member
* @param string $hashedPassword
*
* @return ShareToken
* @throws ShareTokenAlreadyExistException
* @throws ShareTokenNotFoundException
*/
public function generateShareToken(
ShareWrapper $share,
Member $member,
string $hashedPassword = ''
): ShareToken {
if ($member->getUserType() !== Member::TYPE_MAIL
&& $member->getUserType() !== Member::TYPE_CONTACT) {
throw new ShareTokenNotFoundException();
}
$token = $this->token(19);
$shareToken = new ShareToken();
$shareToken->setShareId((int)$share->getId())
->setCircleId($share->getSharedWith())
->setSingleId($member->getSingleId())
->setMemberId($member->getId())
->setToken($token)
->setPassword($hashedPassword)
->setAccepted(IShare::STATUS_ACCEPTED);
try {
$this->shareTokenRequest->search($shareToken);
throw new ShareTokenAlreadyExistException();
} catch (ShareTokenNotFoundException $e) {
}
$this->shareTokenRequest->save($shareToken);
$this->setShareTokenLink($shareToken);
return $shareToken;
}
/**
* @param ShareToken $shareToken
*/
public function setShareTokenLink(ShareToken $shareToken): void {
$link = $this->interfaceService->getFrontalPath(
'files_sharing.sharecontroller.showShare',
['token' => $shareToken->getToken()]
);
$shareToken->setLink($link);
}
/**
* update password on files previously shared to circleId
*
* @param string $circleId
* @param string $hashedPassword
*/
public function updateSharePassword(string $circleId, string $hashedPassword): void {
if ($hashedPassword === '') {
return;
}
$this->shareTokenRequest->updateSharePassword($circleId, $hashedPassword);
}
/**
* remove password on files previously shared to circleId
*
* @param string $circleId
*/
public function removeSharePassword(string $circleId): void {
$this->shareTokenRequest->updateSharePassword($circleId, '');
}
/**
* @param string $singleId
* @param string $circleId
*/
public function removeTokens(string $singleId, string $circleId) {
$this->shareTokenRequest->removeTokens($singleId, $circleId);
}
}
@@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use Exception;
use OCA\Circles\Db\ShareWrapperRequest;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Exceptions\ShareWrapperNotFoundException;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\ShareWrapper;
use OCA\Circles\Tools\Exceptions\InvalidItemException;
use OCA\Circles\Tools\Traits\TDeserialize;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\Files\NotFoundException;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\Share\IShare;
use OCP\Files\Folder;
/**
* Class ShareWrapperService
*
* @package OCA\Circles\Service
*/
class ShareWrapperService {
use TStringTools;
use TDeserialize;
public const CACHE_SHARED_WITH = 'circles/getSharedWith';
public const CACHE_SHARED_WITH_TTL = 900;
/** @var ShareWrapperRequest */
private $shareWrapperRequest;
private ICache $cache;
/**
* ShareWrapperService constructor.
*
* @param ICacheFactory $cacheFactory
* @param ShareWrapperRequest $shareWrapperRequest
*/
public function __construct(ICacheFactory $cacheFactory, ShareWrapperRequest $shareWrapperRequest) {
$this->cache = $cacheFactory->createDistributed(self::CACHE_SHARED_WITH);
$this->shareWrapperRequest = $shareWrapperRequest;
}
/**
* @param string $singleId
* @param int $nodeId
*
* @return ShareWrapper
* @throws RequestBuilderException
* @throws ShareWrapperNotFoundException
*/
public function searchShare(string $singleId, int $nodeId): ShareWrapper {
return $this->shareWrapperRequest->searchShare($singleId, $nodeId);
}
/**
* @param IShare $share
*
* @throws NotFoundException
*/
public function save(IShare $share): void {
$this->cache->clear('');
$this->shareWrapperRequest->save($share);
}
/**
* @param ShareWrapper $shareWrapper
*/
public function update(ShareWrapper $shareWrapper): void {
$this->cache->clear('');
$this->shareWrapperRequest->update($shareWrapper);
}
/**
* @param ShareWrapper $shareWrapper
*/
public function delete(ShareWrapper $shareWrapper): void {
$this->cache->clear('');
$this->shareWrapperRequest->delete((int)$shareWrapper->getId());
}
/**
* @param string $circleId
* @param string $userId
*
* @throws Exception
*/
public function deleteUserSharesToCircle(string $circleId, string $userId): void {
if ($userId === '') {
throw new Exception('$initiator cannot be empty');
}
$this->cache->clear('');
$this->shareWrapperRequest->deleteSharesToCircle($circleId, $userId);
}
/**
* @param string $circleId
*/
public function deleteAllSharesToCircle(string $circleId): void {
$this->cache->clear('');
$this->shareWrapperRequest->deleteSharesToCircle($circleId, '');
}
/**
* @param string $circleId
* @param FederatedUser|null $shareRecipient
* @param FederatedUser|null $shareInitiator
* @param bool $completeDetails
*
* @return ShareWrapper[]
* @throws RequestBuilderException
*/
public function getSharesToCircle(
string $circleId,
?FederatedUser $shareRecipient = null,
?FederatedUser $shareInitiator = null,
bool $completeDetails = false
): array {
return $this->shareWrapperRequest->getSharesToCircle(
$circleId,
$shareRecipient,
$shareInitiator,
$completeDetails
);
}
/**
* @param int $shareId
* @param FederatedUser|null $federatedUser
*
* @return ShareWrapper
* @throws ShareWrapperNotFoundException
* @throws RequestBuilderException
*/
public function getShareById(int $shareId, ?FederatedUser $federatedUser = null): ShareWrapper {
return $this->shareWrapperRequest->getShareById($shareId, $federatedUser);
}
/**
* @param int $fileId
* @param bool $getData
*
* @return ShareWrapper[]
* @throws RequestBuilderException
*/
public function getSharesByFileId(int $fileId, bool $getData = false): array {
return $this->shareWrapperRequest->getSharesByFileId($fileId, $getData);
}
/**
* @param string $token
* @param FederatedUser|null $federatedUser
*
* @return ShareWrapper
* @throws RequestBuilderException
* @throws ShareWrapperNotFoundException
*/
public function getShareByToken(string $token, ?FederatedUser $federatedUser = null): ShareWrapper {
return $this->shareWrapperRequest->getShareByToken($token, $federatedUser);
}
/**
* @param FederatedUser $federatedUser
* @param int $nodeId
* @param CircleProbe|null $probe
*
* @return ShareWrapper[]
* @throws RequestBuilderException
*/
public function getSharedWith(
FederatedUser $federatedUser,
int $nodeId,
?CircleProbe $probe
): array {
$key = $this->generateSharedWithCacheKey($federatedUser, $nodeId, $probe->getChecksum());
$cachedData = $this->cache->get($key);
try {
if (!is_string($cachedData)) {
throw new InvalidItemException();
}
return $this->deserializeList($cachedData, ShareWrapper::class);
} catch (InvalidItemException $e) {
}
$shares = $this->shareWrapperRequest->getSharedWith($federatedUser, $nodeId, $probe);
$this->cache->set($key, json_encode($shares), self::CACHE_SHARED_WITH_TTL);
return $shares;
}
/**
* @param FederatedUser $federatedUser
* @param int $nodeId
* @param bool $reshares
* @param int $offset
* @param int $limit
* @param bool $getData
* @param bool $completeDetails
*
* @return ShareWrapper[]
* @throws RequestBuilderException
*/
public function getSharesBy(
FederatedUser $federatedUser,
int $nodeId,
bool $reshares,
int $limit,
int $offset,
bool $getData = false,
bool $completeDetails = false
): array {
return $this->shareWrapperRequest->getSharesBy(
$federatedUser, $nodeId, $reshares, $limit, $offset, $getData, $completeDetails
);
}
/**
* @param FederatedUser $federatedUser
* @param Folder $node
* @param bool $reshares
* @param bool $shallow Whether the method should stop at the first level, or look into sub-folders.
*
* @return ShareWrapper[]
* @throws RequestBuilderException
*/
public function getSharesInFolder(FederatedUser $federatedUser, Folder $node, bool $reshares, bool $shallow = true): array {
return $this->shareWrapperRequest->getSharesInFolder($federatedUser, $node, $reshares, $shallow);
}
/**
* @param FederatedUser $federatedUser
* @param IShare $share
*
* @return ShareWrapper
* @throws NotFoundException
* @throws ShareWrapperNotFoundException
* @throws RequestBuilderException
*/
public function getChild(IShare $share, FederatedUser $federatedUser): ShareWrapper {
try {
return $this->shareWrapperRequest->getChild($federatedUser, (int)$share->getId());
} catch (ShareWrapperNotFoundException $e) {
}
return $this->createChild($share, $federatedUser);
}
public function clearCache(string $singleId): void {
$this->cache->clear($singleId);
}
/**
* @param FederatedUser $federatedUser
* @param IShare $share
*
* @return ShareWrapper
* @throws ShareWrapperNotFoundException
* @throws NotFoundException
* @throws RequestBuilderException
*/
private function createChild(IShare $share, FederatedUser $federatedUser): ShareWrapper {
$this->cache->clear('');
$share->setSharedWith($federatedUser->getSingleId());
$childId = $this->shareWrapperRequest->save($share, (int)$share->getId());
return $this->getShareById($childId, $federatedUser);
}
/**
* @param FederatedUser $federatedUser
* @param int $nodeId
* @param string $probeSum
*
* @return string
*/
private function generateSharedWithCacheKey(
FederatedUser $federatedUser,
int $nodeId,
string $probeSum
): string {
return $federatedUser->getSingleId() . '#'
. $nodeId . '#'
. $probeSum;
}
}
@@ -0,0 +1,525 @@
<?php
declare(strict_types=1);
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2021
* @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\Circles\Service;
use Exception;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Exceptions\ContactAddressBookNotFoundException;
use OCA\Circles\Exceptions\ContactFormatException;
use OCA\Circles\Exceptions\ContactNotFoundException;
use OCA\Circles\Exceptions\FederatedEventException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\FederatedUserException;
use OCA\Circles\Exceptions\FederatedUserNotFoundException;
use OCA\Circles\Exceptions\GroupNotFoundException;
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
use OCA\Circles\Exceptions\InvalidIdException;
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\SingleCircleNotFoundException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\FederatedItems\SingleMemberAdd;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\ManagedModel;
use OCA\Circles\Model\Member;
use OCA\Circles\Tools\Traits\TNCLogger;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\IGroupManager;
use OCP\IUserManager;
/**
* Class SyncService
*
* @package OCA\Circles\Service
*/
class SyncService {
use TStringTools;
use TNCLogger;
public const SYNC_APPS = 1;
public const SYNC_USERS = 2;
public const SYNC_GROUPS = 4;
public const SYNC_GLOBALSCALE = 8;
public const SYNC_REMOTES = 16;
public const SYNC_CONTACTS = 32;
public const SYNC_ALL = 63;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var CircleRequest */
private $circleRequest;
/** @var MemberRequest */
private $memberRequest;
/** @var FederatedUserService */
private $federatedUserService;
/** @var federatedEventService */
private $federatedEventService;
/** @var CircleService */
private $circleService;
/** @var MembershipService */
private $membershipService;
/** @var OutputService */
private $outputService;
/** @var ConfigService */
private $configService;
/**
* SyncService constructor.
*
* @param IUserManager $userManager
* @param IGroupManager $groupManager
* @param CircleRequest $circleRequest
* @param MemberRequest $memberRequest
* @param FederatedUserService $federatedUserService
* @param federatedEventService $federatedEventService
* @param CircleService $circleService
* @param MembershipService $membershipService
* @param OutputService $outputService
* @param ConfigService $configService
*/
public function __construct(
IUserManager $userManager,
IGroupManager $groupManager,
CircleRequest $circleRequest,
MemberRequest $memberRequest,
FederatedUserService $federatedUserService,
federatedEventService $federatedEventService,
CircleService $circleService,
MembershipService $membershipService,
OutputService $outputService,
ConfigService $configService
) {
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->circleRequest = $circleRequest;
$this->memberRequest = $memberRequest;
$this->federatedUserService = $federatedUserService;
$this->federatedEventService = $federatedEventService;
$this->circleService = $circleService;
$this->membershipService = $membershipService;
$this->outputService = $outputService;
$this->configService = $configService;
$this->setup('app', Application::APP_ID);
}
/**
* @param int $sync
*
* @return void
*/
public function sync(int $sync = self::SYNC_ALL): void {
if ($this->shouldSync(self::SYNC_APPS, $sync)) {
$this->syncApps();
}
if ($this->shouldSync(self::SYNC_USERS, $sync)) {
$this->syncNextcloudUsers();
}
if ($this->shouldSync(self::SYNC_GROUPS, $sync)) {
$this->syncNextcloudGroups();
}
if ($this->shouldSync(self::SYNC_CONTACTS, $sync)) {
$this->syncContacts();
}
if ($this->shouldSync(self::SYNC_GLOBALSCALE, $sync)) {
$this->syncGlobalScale();
}
if ($this->shouldSync(self::SYNC_REMOTES, $sync)) {
$this->syncRemote();
}
}
/**
* @param int $item
* @param int $all
*
* @return bool
*/
private function shouldSync(int $item, int $all): bool {
return (($item & $all) !== 0);
}
/**
*/
public function syncApps(): void {
$this->outputService->output('Syncing Nextcloud Apps');
try {
$this->federatedUserService->getAppInitiator('circles', Member::APP_CIRCLES);
$this->federatedUserService->getAppInitiator('occ', Member::APP_OCC);
} catch (Exception $e) {
$this->e($e);
}
}
/**
* @return void
*/
public function syncNextcloudUsers(): void {
$this->outputService->output('Syncing Nextcloud Accounts');
$users = $this->userManager->search('');
$this->outputService->startMigrationProgress(count($users));
foreach ($users as $user) {
try {
$this->syncNextcloudUser($user->getUID());
} catch (Exception $e) {
}
}
$this->outputService->finishMigrationProgress();
}
/**
* @param string $userId
*
* @return FederatedUser
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws FederatedUserException
* @throws FederatedUserNotFoundException
* @throws InvalidIdException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
*/
public function syncNextcloudUser(string $userId): FederatedUser {
$this->outputService->output('Syncing Nextcloud Account \'' . $userId . '\'', true);
return $this->federatedUserService->getLocalFederatedUser($userId, false, true);
}
/**
* @return void
*/
public function syncNextcloudGroups(): void {
$this->outputService->output('Syncing Nextcloud Groups');
$groups = $this->groupManager->search('');
$this->outputService->startMigrationProgress(count($groups));
foreach ($groups as $group) {
try {
$this->syncNextcloudGroup($group->getGID());
} catch (Exception $e) {
}
}
$this->outputService->finishMigrationProgress();
}
/**
* @param string $groupId
*
* @return Circle
* @throws FederatedUserException
* @throws FederatedUserNotFoundException
* @throws GroupNotFoundException
* @throws InvalidIdException
* @throws SingleCircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws UnknownRemoteException
* @throws RequestBuilderException
*/
public function syncNextcloudGroup(string $groupId): Circle {
$this->outputService->output('Syncing Nextcloud Group \'' . $groupId . '\'', true);
$circle = $this->federatedUserService->getGroupCircle($groupId);
$members = array_map(function (Member $member): string {
return $member->getSingleId();
}, $this->memberRequest->getMembers($circle->getSingleId()));
$group = $this->groupManager->get($groupId);
if ($group->count() <= count($members)) {
return $circle;
}
foreach ($group->getUsers() as $user) {
$member = $this->generateGroupMember($circle, $user->getUID());
if (in_array($member->getSingleId(), $members)) {
continue;
}
$event = new FederatedEvent(SingleMemberAdd::class);
$event->setCircle($circle);
$event->setMember($member);
try {
$this->federatedEventService->newEvent($event);
} catch (Exception $e) {
}
}
return $circle;
}
/**
* @param string $userId
*
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws FederatedUserException
* @throws FederatedUserNotFoundException
* @throws InvalidIdException
* @throws RequestBuilderException
*/
public function userDeleted(string $userId): void {
try {
$federatedUser = $this->federatedUserService->getLocalFederatedUser($userId, false);
} catch (SingleCircleNotFoundException $e) {
return;
}
$this->federatedUserService->setCurrentUser($federatedUser);
$memberships = $federatedUser->getMemberships();
foreach ($memberships as $membership) {
if ($membership->getInheritanceDepth() > 1) {
continue;
}
try {
$this->circleService->circleLeave($membership->getCircleId(), true);
} catch (Exception $e) {
}
}
$this->federatedUserService->deleteFederatedUser($federatedUser);
}
/**
* @param string $groupId
*
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws FederatedUserException
* @throws InvalidIdException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
*/
public function groupDeleted(string $groupId): void {
$circle = new Circle();
$circle->setName('group:' . $groupId)
->setConfig(Circle::CFG_SYSTEM | Circle::CFG_NO_OWNER | Circle::CFG_HIDDEN)
->setSource(Member::TYPE_GROUP);
$owner = $this->federatedUserService->getAppInitiator(
Application::APP_ID,
Member::APP_CIRCLES,
Application::APP_NAME
);
$member = new Member();
$member->importFromIFederatedUser($owner);
$member->setLevel(Member::LEVEL_OWNER)
->setStatus(Member::STATUS_MEMBER);
$circle->setOwner($member);
try {
$circle = $this->circleRequest->searchCircle($circle);
} catch (CircleNotFoundException $e) {
return;
}
$this->circleRequest->delete($circle);
$this->memberRequest->deleteAllFromCircle($circle);
$this->membershipService->onUpdate($circle->getSingleId());
}
/**
* @param Circle $circle
* @param string $userId
*
* @return Member
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws FederatedUserException
* @throws FederatedUserNotFoundException
* @throws InvalidIdException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
*/
private function generateGroupMember(Circle $circle, string $userId): Member {
$federatedUser = $this->federatedUserService->getLocalFederatedUser($userId);
$member = new Member();
$member->importFromIFederatedUser($federatedUser);
$member->setId($this->token(ManagedModel::ID_LENGTH));
$member->setCircleId($circle->getSingleId());
$member->setLevel(Member::LEVEL_MEMBER);
$member->setStatus(Member::STATUS_MEMBER);
$member->setInvitedBy($this->federatedUserService->getCurrentApp());
return $member;
}
/**
* @param string $groupId
* @param string $userId
*
* @return void
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws FederatedUserException
* @throws FederatedUserNotFoundException
* @throws GroupNotFoundException
* @throws InitiatorNotConfirmedException
* @throws InvalidIdException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
* @throws UnknownRemoteException
*/
public function groupMemberAdded(string $groupId, string $userId): void {
$circle = $this->federatedUserService->getGroupCircle($groupId);
$member = $this->generateGroupMember($circle, $userId);
$event = new FederatedEvent(SingleMemberAdd::class);
$event->setCircle($circle);
$event->setMember($member);
$this->federatedEventService->newEvent($event);
}
/**
* @param string $groupId
* @param string $userId
*
* @throws FederatedEventException
* @throws FederatedItemException
* @throws FederatedUserException
* @throws FederatedUserNotFoundException
* @throws GroupNotFoundException
* @throws InitiatorNotConfirmedException
* @throws InvalidIdException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
* @throws UnknownRemoteException
*/
public function groupMemberRemoved(string $groupId, string $userId): void {
$circle = $this->federatedUserService->getGroupCircle($groupId);
$federatedUser = $this->federatedUserService->getLocalFederatedUser($userId);
$this->memberRequest->deleteFederatedUserFromCircle($federatedUser, $circle);
$this->membershipService->onUpdate($federatedUser->getSingleId());
}
/**
* @return void
*/
public function syncContacts(): void {
$this->outputService->output('Syncing Contacts');
}
/**
* @return void
*/
public function syncGlobalScale(): void {
$this->outputService->output('Syncing GlobalScale');
}
/**
* @return void
*/
public function syncRemote(): void {
$this->outputService->output('Syncing Remote Instance');
}
/**
* @param string $circleId
*
* @return void
*/
public function syncRemoteCircle(string $circleId): void {
}
}
@@ -0,0 +1,116 @@
<?php
/**
* Circles - Bring cloud-users closer together.
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Flávio Gomes da Silva Lisboa <flavio.lisboa@fgsl.eti.br>
* @copyright 2017
* @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 AVolu ffero 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\Circles\Service;
use DateTime;
use OC\AppFramework\Utility\TimeFactory;
class TimezoneService {
/** @var string */
private $userId;
/** @var TimeFactory */
private $timeFactory;
/** @var ConfigService */
private $configService;
/**
* TimezoneService constructor.
*
* @param string $userId
* @param TimeFactory $timeFactory
* @param ConfigService $configService
*/
public function __construct(
$userId,
TimeFactory $timeFactory,
ConfigService $configService
) {
$this->userId = $userId;
$this->timeFactory = $timeFactory;
$this->configService = $configService;
}
/**
* @param string $time
*
* @return string
*/
public function convertTimeForCurrentUser($time) {
return $this->convertTimeForUserId($this->userId, $time);
}
public function convertToTimestamp($time) {
return strtotime($time);
}
/**
* @param string $userId
* @param string $time
*
* @return string
*/
public function convertTimeForUserId($userId, $time) {
$timezone = $this->configService->getCoreValueForUser($userId, 'timezone', 'UTC');
$date = \DateTime::createFromFormat('Y-m-d H:i:s', $time);
if ($date === false) {
return $time;
}
$date->setTimezone(new \DateTimeZone($timezone));
return $date->format('Y-m-d H:i:s');
}
/**
* @param string $time
*
* @return DateTime
*/
public function getDateTime(string $time = 'now'): DateTime {
return $this->timeFactory->getDateTime($time);
}
/**
* @return string
*/
public function getUTCDate(): string {
$defaultTimezone = date_default_timezone_get();
date_default_timezone_set('UTC');
$format = date('Y-m-d H:i:s');
date_default_timezone_set($defaultTimezone);
return $format;
}
}