%PDF- %PDF-
| Direktori : /www/varak.net/nextcloud.varak.net/apps_old/apps/related_resources/lib/Service/ |
| Current File : /www/varak.net/nextcloud.varak.net/apps_old/apps/related_resources/lib/Service/RelatedService.php |
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\RelatedResources\Service;
use Exception;
use OCA\Circles\CirclesManager;
use OCA\Circles\Exceptions\FederatedUserNotFoundException;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCA\RelatedResources\Exceptions\CacheNotFoundException;
use OCA\RelatedResources\Exceptions\RelatedResourceNotFound;
use OCA\RelatedResources\Exceptions\RelatedResourceProviderNotFound;
use OCA\RelatedResources\ILinkWeightCalculator;
use OCA\RelatedResources\IRelatedResource;
use OCA\RelatedResources\IRelatedResourceProvider;
use OCA\RelatedResources\LinkWeightCalculators\AncienShareWeightCalculator;
use OCA\RelatedResources\LinkWeightCalculators\KeywordWeightCalculator;
use OCA\RelatedResources\LinkWeightCalculators\TimeWeightCalculator;
use OCA\RelatedResources\Model\RelatedResource;
use OCA\RelatedResources\RelatedResourceProviders\AccountRelatedResourceProvider;
use OCA\RelatedResources\RelatedResourceProviders\CalendarRelatedResourceProvider;
use OCA\RelatedResources\RelatedResourceProviders\DeckRelatedResourceProvider;
use OCA\RelatedResources\RelatedResourceProviders\FilesRelatedResourceProvider;
use OCA\RelatedResources\RelatedResourceProviders\GroupFoldersRelatedResourceProvider;
use OCA\RelatedResources\RelatedResourceProviders\TalkRelatedResourceProvider;
use OCA\RelatedResources\Tools\Exceptions\InvalidItemException;
use OCA\RelatedResources\Tools\Traits\TDeserialize;
use OCP\App\IAppManager;
use OCP\AutoloadNotAllowedException;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\Server;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use ReflectionException;
class RelatedService {
use TDeserialize;
public const CACHE_RELATED = 'related/related';
public const CACHE_RELATED_TTL = 600;
public const CACHE_ITEMS_TTL = 600;
private IAppManager $appManager;
private ICache $cache;
private LoggerInterface $logger;
private ?CirclesManager $circlesManager = null;
private ConfigService $configService;
/** @var ILinkWeightCalculator[] */
private array $weightCalculators = [];
/** @var string[] */
private static array $weightCalculators_ = [
TimeWeightCalculator::class,
KeywordWeightCalculator::class,
AncienShareWeightCalculator::class
];
public function __construct(
IAppManager $appManager,
ICacheFactory $cacheFactory,
LoggerInterface $logger,
ConfigService $configService
) {
$this->appManager = $appManager;
$this->cache = $cacheFactory->createDistributed(self::CACHE_RELATED);
$this->logger = $logger;
$this->configService = $configService;
try {
$this->circlesManager = Server::get(CirclesManager::class);
} catch (ContainerExceptionInterface | AutoloadNotAllowedException $e) {
$this->logger->notice($e->getMessage());
}
}
/**
* @param string $providerId
* @param string $itemId
* @param int $chunk
*
* @return IRelatedResource[]
* @throws RelatedResourceProviderNotFound
*/
public function getRelatedToItem(
string $providerId,
string $itemId,
int $chunk = -1,
string $resourceType = ''
): array {
if ($this->circlesManager === null) {
return [];
}
$result = $this->retrieveRelatedToItem($providerId, $itemId, $resourceType);
usort($result, function (IRelatedResource $r1, IRelatedResource $r2): int {
$a = $r1->getScore();
$b = $r2->getScore();
return ($a === $b) ? 0 : (($a > $b) ? -1 : 1);
});
return ($chunk > -1) ? array_slice($result, 0, $chunk) : $result;
}
/**
* Main method that will return resource related an item (identified by providerId and itemId)
*
* @param string $providerId
* @param string $itemId
*
* @return IRelatedResource[]
* @throws RelatedResourceProviderNotFound
*/
private function retrieveRelatedToItem(
string $providerId,
string $itemId,
string $resourceType = ''
): array {
$this->logger->debug('retrieving related to item ' . $providerId . '.' . $itemId);
try {
// we generate a related resource for current item, including a full
// list of recipients and virtual group
$current = $this->getRelatedFromItem($providerId, $itemId);
} catch (Exception $e) {
return [];
}
if ($current->isGroupShared()) {
$recipients = $current->getRecipients();
} else {
$recipients = $current->getVirtualGroup();
}
if ($resourceType === '') {
$providers = $this->getRelatedResourceProviders();
} else {
$providers = [$this->getRelatedResourceProvider($resourceType)];
}
$result = [];
foreach ($providers as $provider) {
$known = [];
if ($provider->getProviderId() === $providerId) {
$known[] = $current->getItemId();
}
foreach ($recipients as $recipient) {
// foreach provider, we get a list of items available to each recipient of the 'current' item
// we only needs itemIds because, at this point, the full list of recipient each
// item is shared to is not important
// However, if 'current' item contains a group share, we do not need to waste resource to get
// details about items available to single users as they are ignored in current scope of the app.
try {
$entity = $this->circlesManager->getFederatedUser($recipient);
} catch (Exception $e) {
continue;
}
if ($current->isGroupShared() && $entity->getBasedOn()->getSource() === Member::TYPE_USER) {
continue;
}
foreach ($this->getItemsAvailableToEntity($provider, $entity) as $itemId) {
if (in_array($itemId, $known)) {
continue; // we don't want duplicate details
}
$known[] = $itemId;
// foreach itemId, we get full details about it
try {
// cast to string is mandatory in here !
$related = $this->getRelatedFromItem($provider->getProviderId(), (string)$itemId);
} catch (RelatedResourceNotFound $e) {
continue;
}
$result[] = $related;
}
}
}
$result = $this->strictMatching($current, $result);
$result = $this->filterUnavailableResults($result);
$result = $this->improveResult($result);
$this->weightResult($current, $result);
return $result;
}
/**
* get the RelatedResource from an item. including all recipient/virtual groups
*
* @param string $providerId
* @param string $itemId
*
* @return RelatedResource
* @throws RelatedResourceNotFound
* @throws RelatedResourceProviderNotFound
*/
public function getRelatedFromItem(string $providerId, string $itemId): RelatedResource {
try {
return $this->getCachedRelatedFromItem($providerId, $itemId);
} catch (CacheNotFoundException $e) {
}
$result = $this->getRelatedResourceProvider($providerId)
->getRelatedFromItem($this->circlesManager, $itemId);
$this->logger->debug('get related to ' . $providerId . '.' . $itemId . ' - ' . json_encode($result));
if ($result === null) {
throw new RelatedResourceNotFound();
}
$this->cacheRelatedFromItem($providerId, $itemId, $result);
return $result;
}
/**
* @param string $providerId
* @param string $itemId
*
* @return RelatedResource
* @throws CacheNotFoundException
*/
private function getCachedRelatedFromItem(
string $providerId,
string $itemId
): RelatedResource {
$key = $this->generateRelatedFromItemCacheKey($providerId, $itemId);
$cachedData = $this->cache->get($key);
if (!is_string($cachedData) || empty($cachedData)) {
throw new CacheNotFoundException();
}
/** @var RelatedResource $result */
try {
$result = $this->deserializeJson($cachedData, RelatedResource::class);
} catch (InvalidItemException $e) {
throw new CacheNotFoundException();
}
$this->logger->debug(
'existing cache on related from ' . $providerId . '.' . $itemId . ' - ' . json_encode($result)
);
return $result;
}
/**
* @param string $providerId
* @param string $itemId
* @param RelatedResource $related
*/
private function cacheRelatedFromItem(
string $providerId,
string $itemId,
RelatedResource $related
): void {
$this->logger->debug(
'caching related from ' . $providerId . '.' . $itemId . ' - ' . json_encode($related)
);
$key = $this->generateRelatedFromItemCacheKey($providerId, $itemId);
$this->cache->set($key, json_encode($related), self::CACHE_RELATED_TTL);
}
/**
* @param string $providerId
* @param string $itemId
*
* @return string
*/
private function generateRelatedFromItemCacheKey(
string $providerId,
string $itemId
): string {
return 'relatedFromItem/' . $providerId . '::' . $itemId;
}
/**
* @param IRelatedResourceProvider $provider
* @param FederatedUser $entity
*
* @return string[]
*/
private function getItemsAvailableToEntity(
IRelatedResourceProvider $provider,
FederatedUser $entity
): array {
try {
return $this->getCachedItemsAvailableToEntity($provider->getProviderId(), $entity->getSingleId());
} catch (CacheNotFoundException $e) {
}
$result = $provider->getItemsAvailableToEntity($entity);
$this->logger->debug(
'get available items to ' . $entity->getSingleId() . ' from ' . $provider->getProviderId() . ' - '
. json_encode($result)
);
$this->cacheItemsAvailableToEntity($provider->getProviderId(), $entity->getSingleId(), $result);
return $result;
}
/**
* @param string $providerId
* @param string $singleId
*
* @return string[]
* @throws CacheNotFoundException
*/
private function getCachedItemsAvailableToEntity(
string $providerId,
string $singleId
): array {
$key = $this->generateItemsAvailableToEntityCacheKey($providerId, $singleId);
$cachedData = $this->cache->get($key);
if (!is_string($cachedData) || empty($cachedData)) {
throw new CacheNotFoundException();
}
$result = json_decode($cachedData, true);
if (!is_array($result)) {
throw new CacheNotFoundException();
}
$this->logger->debug(
'existing cache on available items to ' . $singleId . ' from ' . $providerId . ' - '
. json_encode($result)
);
return $result;
}
/**
* @param string $providerId
* @param string $singleId
* @param array $result
*/
private function cacheItemsAvailableToEntity(
string $providerId,
string $singleId,
array $result
): void {
$this->logger->debug(
'caching available items to ' . $singleId . ' from ' . $providerId . ' - ' . json_encode($result)
);
$key = $this->generateItemsAvailableToEntityCacheKey($providerId, $singleId);
$this->cache->set($key, json_encode($result), self::CACHE_ITEMS_TTL);
}
/**
* @param string $providerId
* @param string $singleId
*
* @return string
*/
private function generateItemsAvailableToEntityCacheKey(
string $providerId,
string $singleId
): string {
return 'availableItem/' . $providerId . '::' . $singleId;
}
/**
* @param RelatedResource $current
* @param RelatedResource[] $result
*
* @return RelatedResource[]
*/
private function strictMatching(RelatedResource $current, array $result): array {
return array_filter($result, function (IRelatedResource $res) use ($current): bool {
if ($current->isGroupShared()) {
if (!$res->isGroupShared()) {
return false;
}
if ($this->isStrict($current->getRecipients(), $res->getRecipients())) {
return true;
}
} else {
if ($res->isGroupShared()) {
return false;
}
if ($this->isStrict($current->getVirtualGroup(), $res->getVirtualGroup())) {
return true;
}
}
return false;
});
}
/**
* @param IRelatedResource[] $result
*
* @return IRelatedResource[]
*/
private function filterUnavailableResults(array $result): array {
try {
$current = $this->circlesManager->getCurrentFederatedUser();
} catch (FederatedUserNotFoundException $e) {
$this->circlesManager->startSession(); // in case session is lost, restart fresh one
$current = $this->circlesManager->getCurrentFederatedUser();
}
return array_filter($result, function (IRelatedResource $res) use ($current): bool {
$all = array_values(array_unique(array_merge($res->getVirtualGroup(), $res->getRecipients())));
// is current user in the list already ?
if (in_array($current->getSingleId(), $all)) {
return true;
}
// or a member of an entity from the list ?
foreach ($res->getRecipients() as $circleId) {
try {
$this->circlesManager->getLink($circleId, $current->getSingleId());
return true;
} catch (Exception $e) {
}
}
return false;
});
}
/**
* @param IRelatedResource[] $result
*
* @return array
* @throws RelatedResourceProviderNotFound
*/
private function improveResult(array $result): array {
foreach ($result as $entry) {
$this->getRelatedResourceProvider($entry->getProviderId())
->improveRelatedResource($this->circlesManager, $entry);
}
return $result;
}
/**
* @param IRelatedResource $current
* @param IRelatedResource[] $result
*
* @return void
*/
private function weightResult(IRelatedResource $current, array &$result): void {
foreach ($this->getWeightCalculators() as $weightCalculator) {
$weightCalculator->weight($current, $result);
}
}
/**
* @return ILinkWeightCalculator[]
*/
private function getWeightCalculators(): array {
if (empty($this->weightCalculators)) {
$classes = self::$weightCalculators_;
foreach ($this->getRelatedResourceProviders() as $provider) {
foreach ($provider->loadWeightCalculator() as $class) {
$classes[] = $class;
}
}
foreach ($classes as $class) {
try {
$test = new ReflectionClass($class);
if (!in_array(ILinkWeightCalculator::class, $test->getInterfaceNames())) {
throw new ReflectionException(
$class . ' does not implements ILinkWeightCalculator'
);
}
$this->weightCalculators[] = Server::get($class);
} catch (NotFoundExceptionInterface | ContainerExceptionInterface | ReflectionException $e) {
$this->logger->notice($e->getMessage());
}
}
}
return $this->weightCalculators;
}
/**
* @return IRelatedResourceProvider[]
*/
private function getRelatedResourceProviders(): array {
$providers = [];
try {
$providers[] = Server::get(FilesRelatedResourceProvider::class);
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
$this->logger->notice($e->getMessage());
}
try {
$providers[] = Server::get(AccountRelatedResourceProvider::class);
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
$this->logger->notice($e->getMessage());
}
if ($this->appManager->isInstalled('deck')) {
try {
$providers[] = Server::get(DeckRelatedResourceProvider::class);
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $e) {
$this->logger->notice($e->getMessage());
}
}
if ($this->appManager->isInstalled('calendar')) {
try {
$providers[] = Server::get(CalendarRelatedResourceProvider::class);
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $e) {
$this->logger->notice($e->getMessage());
}
}
if ($this->appManager->isInstalled('spreed')) {
try {
$providers[] = Server::get(TalkRelatedResourceProvider::class);
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $e) {
$this->logger->notice($e->getMessage());
}
}
if ($this->appManager->isInstalled('groupfolders')) {
try {
$providers[] = Server::get(GroupFoldersRelatedResourceProvider::class);
} catch (NotFoundExceptionInterface | ContainerExceptionInterface $e) {
$this->logger->notice($e->getMessage());
}
}
return $providers;
}
/**
* @param string $relatedProviderId
*
* @return IRelatedResourceProvider
* @throws RelatedResourceProviderNotFound
*/
public function getRelatedResourceProvider(string $relatedProviderId): IRelatedResourceProvider {
foreach ($this->getRelatedResourceProviders() as $provider) {
if ($provider->getProviderId() === $relatedProviderId) {
return $provider;
}
}
throw new RelatedResourceProviderNotFound();
}
/**
* @param array $arr1
* @param array $arr2
*
* @return bool
*/
private function isStrict(array $arr1, array $arr2): bool {
return empty(array_merge(array_diff($arr1, $arr2), array_diff($arr2, $arr1)));
}
/**
* when a share is created/deleted, flush all
*/
public function flushCache(): void {
$this->logger->debug('flush cache');
$this->cache->clear();
}
}