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,80 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 OC\Collaboration\AutoComplete;
use OCP\Collaboration\AutoComplete\IManager;
use OCP\Collaboration\AutoComplete\ISorter;
use OCP\IServerContainer;
class Manager implements IManager {
/** @var string[] */
protected array $sorters = [];
/** @var ISorter[] */
protected array $sorterInstances = [];
public function __construct(
private IServerContainer $container,
) {
}
public function runSorters(array $sorters, array &$sortArray, array $context): void {
$sorterInstances = $this->getSorters();
while ($sorter = array_shift($sorters)) {
if (isset($sorterInstances[$sorter])) {
$sorterInstances[$sorter]->sort($sortArray, $context);
} else {
$this->container->getLogger()->warning('No sorter for ID "{id}", skipping', [
'app' => 'core', 'id' => $sorter
]);
}
}
}
public function registerSorter($className): void {
$this->sorters[] = $className;
}
protected function getSorters(): array {
if (count($this->sorterInstances) === 0) {
foreach ($this->sorters as $sorter) {
/** @var ISorter $instance */
$instance = $this->container->resolve($sorter);
if (!$instance instanceof ISorter) {
$this->container->getLogger()->notice('Skipping sorter which is not an instance of ISorter. Class name: {class}',
['app' => 'core', 'class' => $sorter]);
continue;
}
$sorterId = trim($instance->getId());
if (trim($sorterId) === '') {
$this->container->getLogger()->notice('Skipping sorter with empty ID. Class name: {class}',
['app' => 'core', 'class' => $sorter]);
continue;
}
$this->sorterInstances[$sorterId] = $instance;
}
}
return $this->sorterInstances;
}
}
@@ -0,0 +1,143 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
*
* @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 OC\Collaboration\Collaborators;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUserSession;
use OCP\Share\IShare;
class GroupPlugin implements ISearchPlugin {
protected bool $shareeEnumeration;
protected bool $shareWithGroupOnly;
protected bool $shareeEnumerationInGroupOnly;
protected bool $groupSharingDisabled;
public function __construct(
private IConfig $config,
private IGroupManager $groupManager,
private IUserSession $userSession,
) {
$this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
$this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
$this->groupSharingDisabled = $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'no';
}
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
if ($this->groupSharingDisabled) {
return false;
}
$hasMoreResults = false;
$result = ['wide' => [], 'exact' => []];
$groups = $this->groupManager->search($search, $limit, $offset);
$groupIds = array_map(function (IGroup $group) {
return $group->getGID();
}, $groups);
if (!$this->shareeEnumeration || count($groups) < $limit) {
$hasMoreResults = true;
}
$userGroups = [];
if (!empty($groups) && ($this->shareWithGroupOnly || $this->shareeEnumerationInGroupOnly)) {
// Intersect all the groups that match with the groups this user is a member of
$userGroups = $this->groupManager->getUserGroups($this->userSession->getUser());
$userGroups = array_map(function (IGroup $group) {
return $group->getGID();
}, $userGroups);
$groupIds = array_intersect($groupIds, $userGroups);
}
$lowerSearch = strtolower($search);
foreach ($groups as $group) {
if ($group->hideFromCollaboration()) {
continue;
}
// FIXME: use a more efficient approach
$gid = $group->getGID();
if (!in_array($gid, $groupIds)) {
continue;
}
if (strtolower($gid) === $lowerSearch || strtolower($group->getDisplayName()) === $lowerSearch) {
$result['exact'][] = [
'label' => $group->getDisplayName(),
'value' => [
'shareType' => IShare::TYPE_GROUP,
'shareWith' => $gid,
],
];
} else {
if ($this->shareeEnumerationInGroupOnly && !in_array($group->getGID(), $userGroups, true)) {
continue;
}
$result['wide'][] = [
'label' => $group->getDisplayName(),
'value' => [
'shareType' => IShare::TYPE_GROUP,
'shareWith' => $gid,
],
];
}
}
if ($offset === 0 && empty($result['exact'])) {
// On page one we try if the search result has a direct hit on the
// user id and if so, we add that to the exact match list
$group = $this->groupManager->get($search);
if ($group instanceof IGroup && !$group->hideFromCollaboration() && (!$this->shareWithGroupOnly || in_array($group->getGID(), $userGroups))) {
$result['exact'][] = [
'label' => $group->getDisplayName(),
'value' => [
'shareType' => IShare::TYPE_GROUP,
'shareWith' => $group->getGID(),
],
];
}
}
if (!$this->shareeEnumeration) {
$result['wide'] = [];
}
$type = new SearchResultType('groups');
$searchResult->addResultSet($type, $result['wide'], $result['exact']);
return $hasMoreResults;
}
}
@@ -0,0 +1,116 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 OC\Collaboration\Collaborators;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\Federation\ICloudIdManager;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IUserSession;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
class LookupPlugin implements ISearchPlugin {
/** @var string remote part of the current user's cloud id */
private string $currentUserRemote;
public function __construct(
private IConfig $config,
private IClientService $clientService,
IUserSession $userSession,
private ICloudIdManager $cloudIdManager,
private LoggerInterface $logger,
) {
$currentUserCloudId = $userSession->getUser()->getCloudId();
$this->currentUserRemote = $cloudIdManager->resolveCloudId($currentUserCloudId)->getRemote();
}
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
$isGlobalScaleEnabled = $this->config->getSystemValueBool('gs.enabled', false);
$isLookupServerEnabled = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'yes') === 'yes';
$hasInternetConnection = $this->config->getSystemValueBool('has_internet_connection', true);
// if case of Global Scale we always search the lookup server
if (!$isGlobalScaleEnabled && (!$isLookupServerEnabled || !$hasInternetConnection)) {
return false;
}
$lookupServerUrl = $this->config->getSystemValueString('lookup_server', 'https://lookup.nextcloud.com');
if (empty($lookupServerUrl)) {
return false;
}
$lookupServerUrl = rtrim($lookupServerUrl, '/');
$result = [];
try {
$client = $this->clientService->newClient();
$response = $client->get(
$lookupServerUrl . '/users?search=' . urlencode($search),
[
'timeout' => 10,
'connect_timeout' => 3,
]
);
$body = json_decode($response->getBody(), true);
foreach ($body as $lookup) {
try {
$remote = $this->cloudIdManager->resolveCloudId($lookup['federationId'])->getRemote();
} catch (\Exception $e) {
$this->logger->error('Can not parse federated cloud ID "' . $lookup['federationId'] . '"', [
'exception' => $e,
]);
continue;
}
if ($this->currentUserRemote === $remote) {
continue;
}
$name = $lookup['name']['value'] ?? '';
$label = empty($name) ? $lookup['federationId'] : $name . ' (' . $lookup['federationId'] . ')';
$result[] = [
'label' => $label,
'value' => [
'shareType' => IShare::TYPE_REMOTE,
'globalScale' => $isGlobalScaleEnabled,
'shareWith' => $lookup['federationId'],
],
'extra' => $lookup,
];
}
} catch (\Exception $e) {
}
$type = new SearchResultType('lookup');
$searchResult->addResultSet($type, $result, []);
return false;
}
}
@@ -0,0 +1,268 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Tobia De Koninck <tobia@ledfan.be>
*
* @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 OC\Collaboration\Collaborators;
use OC\KnownUser\KnownUserService;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\Contacts\IManager;
use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Mail\IMailer;
use OCP\Share\IShare;
class MailPlugin implements ISearchPlugin {
protected bool $shareWithGroupOnly;
protected bool $shareeEnumeration;
protected bool $shareeEnumerationInGroupOnly;
protected bool $shareeEnumerationPhone;
protected bool $shareeEnumerationFullMatch;
protected bool $shareeEnumerationFullMatchEmail;
public function __construct(
private IManager $contactsManager,
private ICloudIdManager $cloudIdManager,
private IConfig $config,
private IGroupManager $groupManager,
private KnownUserService $knownUserService,
private IUserSession $userSession,
private IMailer $mailer,
) {
$this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
$this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
$this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
$this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
$this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
}
/**
* {@inheritdoc}
*/
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
if ($this->shareeEnumerationFullMatch && !$this->shareeEnumerationFullMatchEmail) {
return false;
}
// Extract the email address from "Foo Bar <foo.bar@example.tld>" and then search with "foo.bar@example.tld" instead
$result = preg_match('/<([^@]+@.+)>$/', $search, $matches);
if ($result && filter_var($matches[1], FILTER_VALIDATE_EMAIL)) {
return $this->search($matches[1], $limit, $offset, $searchResult);
}
$currentUserId = $this->userSession->getUser()->getUID();
$result = $userResults = ['wide' => [], 'exact' => []];
$userType = new SearchResultType('users');
$emailType = new SearchResultType('emails');
// Search in contacts
$addressBookContacts = $this->contactsManager->search(
$search,
['EMAIL', 'FN'],
[
'limit' => $limit,
'offset' => $offset,
'enumeration' => $this->shareeEnumeration,
'fullmatch' => $this->shareeEnumerationFullMatch,
]
);
$lowerSearch = strtolower($search);
foreach ($addressBookContacts as $contact) {
if (isset($contact['EMAIL'])) {
$emailAddresses = $contact['EMAIL'];
if (\is_string($emailAddresses)) {
$emailAddresses = [$emailAddresses];
}
foreach ($emailAddresses as $type => $emailAddress) {
$displayName = $emailAddress;
$emailAddressType = null;
if (\is_array($emailAddress)) {
$emailAddressData = $emailAddress;
$emailAddress = $emailAddressData['value'];
$emailAddressType = $emailAddressData['type'];
}
if (isset($contact['FN'])) {
$displayName = $contact['FN'] . ' (' . $emailAddress . ')';
}
$exactEmailMatch = strtolower($emailAddress) === $lowerSearch;
if (isset($contact['isLocalSystemBook'])) {
if ($this->shareWithGroupOnly) {
/*
* Check if the user may share with the user associated with the e-mail of the just found contact
*/
$userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
$found = false;
foreach ($userGroups as $userGroup) {
if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) {
$found = true;
break;
}
}
if (!$found) {
continue;
}
}
if ($exactEmailMatch && $this->shareeEnumerationFullMatch) {
try {
$cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0]);
} catch (\InvalidArgumentException $e) {
continue;
}
if (!$this->isCurrentUser($cloud) && !$searchResult->hasResult($userType, $cloud->getUser())) {
$singleResult = [[
'label' => $displayName,
'uuid' => $contact['UID'] ?? $emailAddress,
'name' => $contact['FN'] ?? $displayName,
'value' => [
'shareType' => IShare::TYPE_USER,
'shareWith' => $cloud->getUser(),
],
'shareWithDisplayNameUnique' => !empty($emailAddress) ? $emailAddress : $cloud->getUser()
]];
$searchResult->addResultSet($userType, [], $singleResult);
$searchResult->markExactIdMatch($emailType);
}
return false;
}
if ($this->shareeEnumeration) {
try {
$cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0]);
} catch (\InvalidArgumentException $e) {
continue;
}
$addToWide = !($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone);
if (!$addToWide && $this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $contact['UID'])) {
$addToWide = true;
}
if (!$addToWide && $this->shareeEnumerationInGroupOnly) {
$addToWide = false;
$userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
foreach ($userGroups as $userGroup) {
if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) {
$addToWide = true;
break;
}
}
}
if ($addToWide && !$this->isCurrentUser($cloud) && !$searchResult->hasResult($userType, $cloud->getUser())) {
$userResults['wide'][] = [
'label' => $displayName,
'uuid' => $contact['UID'] ?? $emailAddress,
'name' => $contact['FN'] ?? $displayName,
'value' => [
'shareType' => IShare::TYPE_USER,
'shareWith' => $cloud->getUser(),
],
'shareWithDisplayNameUnique' => !empty($emailAddress) ? $emailAddress : $cloud->getUser()
];
continue;
}
}
continue;
}
if ($exactEmailMatch
|| (isset($contact['FN']) && strtolower($contact['FN']) === $lowerSearch)) {
if ($exactEmailMatch) {
$searchResult->markExactIdMatch($emailType);
}
$result['exact'][] = [
'label' => $displayName,
'uuid' => $contact['UID'] ?? $emailAddress,
'name' => $contact['FN'] ?? $displayName,
'type' => $emailAddressType ?? '',
'value' => [
'shareType' => IShare::TYPE_EMAIL,
'shareWith' => $emailAddress,
],
];
} else {
$result['wide'][] = [
'label' => $displayName,
'uuid' => $contact['UID'] ?? $emailAddress,
'name' => $contact['FN'] ?? $displayName,
'type' => $emailAddressType ?? '',
'value' => [
'shareType' => IShare::TYPE_EMAIL,
'shareWith' => $emailAddress,
],
];
}
}
}
}
$reachedEnd = true;
if ($this->shareeEnumeration) {
$reachedEnd = (count($result['wide']) < $offset + $limit) &&
(count($userResults['wide']) < $offset + $limit);
$result['wide'] = array_slice($result['wide'], $offset, $limit);
$userResults['wide'] = array_slice($userResults['wide'], $offset, $limit);
}
if (!$searchResult->hasExactIdMatch($emailType) && $this->mailer->validateMailAddress($search)) {
$result['exact'][] = [
'label' => $search,
'uuid' => $search,
'value' => [
'shareType' => IShare::TYPE_EMAIL,
'shareWith' => $search,
],
];
}
if (!empty($userResults['wide'])) {
$searchResult->addResultSet($userType, $userResults['wide'], []);
}
$searchResult->addResultSet($emailType, $result['wide'], $result['exact']);
return !$reachedEnd;
}
public function isCurrentUser(ICloudId $cloud): bool {
$currentUser = $this->userSession->getUser();
return $currentUser instanceof IUser && $currentUser->getUID() === $cloud->getUser();
}
}
@@ -0,0 +1,91 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Collaboration\Collaborators;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudIdManager;
use OCP\Share;
use OCP\Share\IShare;
class RemoteGroupPlugin implements ISearchPlugin {
private bool $enabled = false;
public function __construct(
ICloudFederationProviderManager $cloudFederationProviderManager,
private ICloudIdManager $cloudIdManager,
) {
try {
$fileSharingProvider = $cloudFederationProviderManager->getCloudFederationProvider('file');
$supportedShareTypes = $fileSharingProvider->getSupportedShareTypes();
if (in_array('group', $supportedShareTypes)) {
$this->enabled = true;
}
} catch (\Exception $e) {
// do nothing, just don't enable federated group shares
}
}
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
$result = ['wide' => [], 'exact' => []];
$resultType = new SearchResultType('remote_groups');
if ($this->enabled && $this->cloudIdManager->isValidCloudId($search) && $offset === 0) {
[$remoteGroup, $serverUrl] = $this->splitGroupRemote($search);
$result['exact'][] = [
'label' => $remoteGroup . " ($serverUrl)",
'guid' => $remoteGroup,
'name' => $remoteGroup,
'value' => [
'shareType' => IShare::TYPE_REMOTE_GROUP,
'shareWith' => $search,
'server' => $serverUrl,
],
];
}
$searchResult->addResultSet($resultType, $result['wide'], $result['exact']);
return true;
}
/**
* split group and remote from federated cloud id
*
* @param string $address federated share address
* @return array [user, remoteURL]
* @throws \InvalidArgumentException
*/
public function splitGroupRemote($address): array {
try {
$cloudId = $this->cloudIdManager->resolveCloudId($address);
return [$cloudId->getUser(), $cloudId->getRemote()];
} catch (\InvalidArgumentException $e) {
throw new \InvalidArgumentException('Invalid Federated Cloud ID', 0, $e);
}
}
}
@@ -0,0 +1,186 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 OC\Collaboration\Collaborators;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\Contacts\IManager;
use OCP\Federation\ICloudIdManager;
use OCP\IConfig;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IShare;
class RemotePlugin implements ISearchPlugin {
protected bool $shareeEnumeration;
private string $userId;
public function __construct(
private IManager $contactsManager,
private ICloudIdManager $cloudIdManager,
private IConfig $config,
private IUserManager $userManager,
IUserSession $userSession,
) {
$this->userId = $userSession->getUser()?->getUID() ?? '';
$this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
}
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
$result = ['wide' => [], 'exact' => []];
$resultType = new SearchResultType('remotes');
// Search in contacts
$addressBookContacts = $this->contactsManager->search($search, ['CLOUD', 'FN'], [
'limit' => $limit,
'offset' => $offset,
'enumeration' => false,
'fullmatch' => false,
]);
foreach ($addressBookContacts as $contact) {
if (isset($contact['isLocalSystemBook'])) {
continue;
}
if (isset($contact['CLOUD'])) {
$cloudIds = $contact['CLOUD'];
if (is_string($cloudIds)) {
$cloudIds = [$cloudIds];
}
$lowerSearch = strtolower($search);
foreach ($cloudIds as $cloudId) {
$cloudIdType = '';
if (\is_array($cloudId)) {
$cloudIdData = $cloudId;
$cloudId = $cloudIdData['value'];
$cloudIdType = $cloudIdData['type'];
}
try {
[$remoteUser, $serverUrl] = $this->splitUserRemote($cloudId);
} catch (\InvalidArgumentException $e) {
continue;
}
$localUser = $this->userManager->get($remoteUser);
/**
* Add local share if remote cloud id matches a local user ones
*/
if ($localUser !== null && $remoteUser !== $this->userId && $cloudId === $localUser->getCloudId()) {
$result['wide'][] = [
'label' => $contact['FN'],
'uuid' => $contact['UID'],
'value' => [
'shareType' => IShare::TYPE_USER,
'shareWith' => $remoteUser
],
'shareWithDisplayNameUnique' => $contact['EMAIL'] !== null && $contact['EMAIL'] !== '' ? $contact['EMAIL'] : $contact['UID'],
];
}
if (strtolower($contact['FN']) === $lowerSearch || strtolower($cloudId) === $lowerSearch) {
if (strtolower($cloudId) === $lowerSearch) {
$searchResult->markExactIdMatch($resultType);
}
$result['exact'][] = [
'label' => $contact['FN'] . " ($cloudId)",
'uuid' => $contact['UID'],
'name' => $contact['FN'],
'type' => $cloudIdType,
'value' => [
'shareType' => IShare::TYPE_REMOTE,
'shareWith' => $cloudId,
'server' => $serverUrl,
],
];
} else {
$result['wide'][] = [
'label' => $contact['FN'] . " ($cloudId)",
'uuid' => $contact['UID'],
'name' => $contact['FN'],
'type' => $cloudIdType,
'value' => [
'shareType' => IShare::TYPE_REMOTE,
'shareWith' => $cloudId,
'server' => $serverUrl,
],
];
}
}
}
}
if (!$this->shareeEnumeration) {
$result['wide'] = [];
} else {
$result['wide'] = array_slice($result['wide'], $offset, $limit);
}
/**
* Add generic share with remote item for valid cloud ids that are not users of the local instance
*/
if (!$searchResult->hasExactIdMatch($resultType) && $this->cloudIdManager->isValidCloudId($search) && $offset === 0) {
try {
[$remoteUser, $serverUrl] = $this->splitUserRemote($search);
$localUser = $this->userManager->get($remoteUser);
if ($localUser === null || $search !== $localUser->getCloudId()) {
$result['exact'][] = [
'label' => $remoteUser . " ($serverUrl)",
'uuid' => $remoteUser,
'name' => $remoteUser,
'value' => [
'shareType' => IShare::TYPE_REMOTE,
'shareWith' => $search,
'server' => $serverUrl,
],
];
}
} catch (\InvalidArgumentException $e) {
}
}
$searchResult->addResultSet($resultType, $result['wide'], $result['exact']);
return true;
}
/**
* split user and remote from federated cloud id
*
* @param string $address federated share address
* @return array [user, remoteURL]
* @throws \InvalidArgumentException
*/
public function splitUserRemote(string $address): array {
try {
$cloudId = $this->cloudIdManager->resolveCloudId($address);
return [$cloudId->getUser(), $cloudId->getRemote()];
} catch (\InvalidArgumentException $e) {
throw new \InvalidArgumentException('Invalid Federated Cloud ID', 0, $e);
}
}
}
@@ -0,0 +1,141 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author onehappycat <one.happy.cat@gmx.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @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 OC\Collaboration\Collaborators;
use OCP\Collaboration\Collaborators\ISearch;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\IContainer;
use OCP\Share;
class Search implements ISearch {
protected array $pluginList = [];
public function __construct(
private IContainer $container,
) {
}
/**
* @param string $search
* @param bool $lookup
* @param int|null $limit
* @param int|null $offset
* @throws \OCP\AppFramework\QueryException
*/
public function search($search, array $shareTypes, $lookup, $limit, $offset): array {
$hasMoreResults = false;
// Trim leading and trailing whitespace characters, e.g. when query is copy-pasted
$search = trim($search);
/** @var ISearchResult $searchResult */
$searchResult = $this->container->resolve(SearchResult::class);
foreach ($shareTypes as $type) {
if (!isset($this->pluginList[$type])) {
continue;
}
foreach ($this->pluginList[$type] as $plugin) {
/** @var ISearchPlugin $searchPlugin */
$searchPlugin = $this->container->resolve($plugin);
$hasMoreResults = $searchPlugin->search($search, $limit, $offset, $searchResult) || $hasMoreResults;
}
}
// Get from lookup server, not a separate share type
if ($lookup) {
$searchPlugin = $this->container->resolve(LookupPlugin::class);
$hasMoreResults = $searchPlugin->search($search, $limit, $offset, $searchResult) || $hasMoreResults;
}
// sanitizing, could go into the plugins as well
// if we have an exact match, either for the federated cloud id or for the
// email address, we only return the exact match. It is highly unlikely
// that the exact same email address and federated cloud id exists
$emailType = new SearchResultType('emails');
$remoteType = new SearchResultType('remotes');
if ($searchResult->hasExactIdMatch($emailType) && !$searchResult->hasExactIdMatch($remoteType)) {
$searchResult->unsetResult($remoteType);
} elseif (!$searchResult->hasExactIdMatch($emailType) && $searchResult->hasExactIdMatch($remoteType)) {
$searchResult->unsetResult($emailType);
}
$this->dropMailSharesWhereRemoteShareIsPossible($searchResult);
// if we have an exact local user match with an email-a-like query,
// there is no need to show the remote and email matches.
$userType = new SearchResultType('users');
if (str_contains($search, '@') && $searchResult->hasExactIdMatch($userType)) {
$searchResult->unsetResult($remoteType);
$searchResult->unsetResult($emailType);
}
return [$searchResult->asArray(), $hasMoreResults];
}
public function registerPlugin(array $pluginInfo): void {
$shareType = constant(Share::class . '::' . $pluginInfo['shareType']);
if ($shareType === null) {
throw new \InvalidArgumentException('Provided ShareType is invalid');
}
$this->pluginList[$shareType][] = $pluginInfo['class'];
}
protected function dropMailSharesWhereRemoteShareIsPossible(ISearchResult $searchResult): void {
$allResults = $searchResult->asArray();
$emailType = new SearchResultType('emails');
$remoteType = new SearchResultType('remotes');
if (!isset($allResults[$remoteType->getLabel()])
|| !isset($allResults[$emailType->getLabel()])) {
return;
}
$mailIdMap = [];
foreach ($allResults[$emailType->getLabel()] as $mailRow) {
// sure, array_reduce looks nicer, but foreach needs less resources and is faster
if (!isset($mailRow['uuid'])) {
continue;
}
$mailIdMap[$mailRow['uuid']] = $mailRow['value']['shareWith'];
}
foreach ($allResults[$remoteType->getLabel()] as $resultRow) {
if (!isset($resultRow['uuid'])) {
continue;
}
if (isset($mailIdMap[$resultRow['uuid']])) {
$searchResult->removeCollaboratorResult($emailType, $mailIdMap[$resultRow['uuid']]);
}
}
}
}
@@ -0,0 +1,107 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Collaboration\Collaborators;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
class SearchResult implements ISearchResult {
protected array $result = [
'exact' => [],
];
protected array $exactIdMatches = [];
public function addResultSet(SearchResultType $type, array $matches, array $exactMatches = null): void {
$type = $type->getLabel();
if (!isset($this->result[$type])) {
$this->result[$type] = [];
$this->result['exact'][$type] = [];
}
$this->result[$type] = array_merge($this->result[$type], $matches);
if (is_array($exactMatches)) {
$this->result['exact'][$type] = array_merge($this->result['exact'][$type], $exactMatches);
}
}
public function markExactIdMatch(SearchResultType $type): void {
$this->exactIdMatches[$type->getLabel()] = 1;
}
public function hasExactIdMatch(SearchResultType $type): bool {
return isset($this->exactIdMatches[$type->getLabel()]);
}
public function hasResult(SearchResultType $type, $collaboratorId): bool {
$type = $type->getLabel();
if (!isset($this->result[$type])) {
return false;
}
$resultArrays = [$this->result['exact'][$type], $this->result[$type]];
foreach ($resultArrays as $resultArray) {
foreach ($resultArray as $result) {
if ($result['value']['shareWith'] === $collaboratorId) {
return true;
}
}
}
return false;
}
public function asArray(): array {
return $this->result;
}
public function unsetResult(SearchResultType $type): void {
$type = $type->getLabel();
$this->result[$type] = [];
if (isset($this->result['exact'][$type])) {
$this->result['exact'][$type] = [];
}
}
public function removeCollaboratorResult(SearchResultType $type, string $collaboratorId): bool {
$type = $type->getLabel();
if (!isset($this->result[$type])) {
return false;
}
$actionDone = false;
$resultArrays = [&$this->result['exact'][$type], &$this->result[$type]];
foreach ($resultArrays as &$resultArray) {
foreach ($resultArray as $k => $result) {
if ($result['value']['shareWith'] === $collaboratorId) {
unset($resultArray[$k]);
$actionDone = true;
}
}
}
return $actionDone;
}
}
@@ -0,0 +1,282 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @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 OC\Collaboration\Collaborators;
use OC\KnownUser\KnownUserService;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IShare;
use OCP\UserStatus\IManager as IUserStatusManager;
class UserPlugin implements ISearchPlugin {
protected bool $shareWithGroupOnly;
protected bool $shareeEnumeration;
protected bool $shareeEnumerationInGroupOnly;
protected bool $shareeEnumerationPhone;
protected bool $shareeEnumerationFullMatch;
protected bool $shareeEnumerationFullMatchUserId;
protected bool $shareeEnumerationFullMatchEmail;
protected bool $shareeEnumerationFullMatchIgnoreSecondDisplayName;
public function __construct(
private IConfig $config,
private IUserManager $userManager,
private IGroupManager $groupManager,
private IUserSession $userSession,
private KnownUserService $knownUserService,
private IUserStatusManager $userStatusManager,
) {
$this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
$this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
$this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
$this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
$this->shareeEnumerationFullMatchUserId = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes';
$this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
$this->shareeEnumerationFullMatchIgnoreSecondDisplayName = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes';
}
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
$result = ['wide' => [], 'exact' => []];
$users = [];
$hasMoreResults = false;
$currentUserId = $this->userSession->getUser()->getUID();
$currentUserGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
if ($this->shareWithGroupOnly || $this->shareeEnumerationInGroupOnly) {
// Search in all the groups this user is part of
foreach ($currentUserGroups as $userGroupId) {
$usersInGroup = $this->groupManager->displayNamesInGroup($userGroupId, $search, $limit, $offset);
foreach ($usersInGroup as $userId => $displayName) {
$userId = (string) $userId;
$user = $this->userManager->get($userId);
if (!$user->isEnabled()) {
// Ignore disabled users
continue;
}
$users[$userId] = $user;
}
if (count($usersInGroup) >= $limit) {
$hasMoreResults = true;
}
}
if (!$this->shareWithGroupOnly && $this->shareeEnumerationPhone) {
$usersTmp = $this->userManager->searchKnownUsersByDisplayName($currentUserId, $search, $limit, $offset);
if (!empty($usersTmp)) {
foreach ($usersTmp as $user) {
if ($user->isEnabled()) { // Don't keep deactivated users
$users[$user->getUID()] = $user;
}
}
uasort($users, function ($a, $b) {
/**
* @var \OC\User\User $a
* @var \OC\User\User $b
*/
return strcasecmp($a->getDisplayName(), $b->getDisplayName());
});
}
}
} else {
// Search in all users
if ($this->shareeEnumerationPhone) {
$usersTmp = $this->userManager->searchKnownUsersByDisplayName($currentUserId, $search, $limit, $offset);
} else {
$usersTmp = $this->userManager->searchDisplayName($search, $limit, $offset);
}
foreach ($usersTmp as $user) {
if ($user->isEnabled()) { // Don't keep deactivated users
$users[$user->getUID()] = $user;
}
}
}
$this->takeOutCurrentUser($users);
if (!$this->shareeEnumeration || count($users) < $limit) {
$hasMoreResults = true;
}
$foundUserById = false;
$lowerSearch = strtolower($search);
$userStatuses = $this->userStatusManager->getUserStatuses(array_keys($users));
foreach ($users as $uid => $user) {
$userDisplayName = $user->getDisplayName();
$userEmail = $user->getSystemEMailAddress();
$uid = (string) $uid;
$status = [];
if (array_key_exists($uid, $userStatuses)) {
$userStatus = $userStatuses[$uid];
$status = [
'status' => $userStatus->getStatus(),
'message' => $userStatus->getMessage(),
'icon' => $userStatus->getIcon(),
'clearAt' => $userStatus->getClearAt()
? (int)$userStatus->getClearAt()->format('U')
: null,
];
}
if (
$this->shareeEnumerationFullMatch &&
$lowerSearch !== '' && (strtolower($uid) === $lowerSearch ||
strtolower($userDisplayName) === $lowerSearch ||
($this->shareeEnumerationFullMatchIgnoreSecondDisplayName && trim(strtolower(preg_replace('/ \(.*\)$/', '', $userDisplayName))) === $lowerSearch) ||
($this->shareeEnumerationFullMatchEmail && strtolower($userEmail ?? '') === $lowerSearch))
) {
if (strtolower($uid) === $lowerSearch) {
$foundUserById = true;
}
$result['exact'][] = [
'label' => $userDisplayName,
'subline' => $status['message'] ?? '',
'icon' => 'icon-user',
'value' => [
'shareType' => IShare::TYPE_USER,
'shareWith' => $uid,
],
'shareWithDisplayNameUnique' => !empty($userEmail) ? $userEmail : $uid,
'status' => $status,
];
} else {
$addToWideResults = false;
if ($this->shareeEnumeration &&
!($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone)) {
$addToWideResults = true;
}
if ($this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $user->getUID())) {
$addToWideResults = true;
}
if (!$addToWideResults && $this->shareeEnumerationInGroupOnly) {
$commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user));
if (!empty($commonGroups)) {
$addToWideResults = true;
}
}
if ($addToWideResults) {
$result['wide'][] = [
'label' => $userDisplayName,
'subline' => $status['message'] ?? '',
'icon' => 'icon-user',
'value' => [
'shareType' => IShare::TYPE_USER,
'shareWith' => $uid,
],
'shareWithDisplayNameUnique' => !empty($userEmail) ? $userEmail : $uid,
'status' => $status,
];
}
}
}
if ($this->shareeEnumerationFullMatch && $this->shareeEnumerationFullMatchUserId && $offset === 0 && !$foundUserById) {
// On page one we try if the search result has a direct hit on the
// user id and if so, we add that to the exact match list
$user = $this->userManager->get($search);
if ($user instanceof IUser) {
$addUser = true;
if ($this->shareWithGroupOnly) {
// Only add, if we have a common group
$commonGroups = array_intersect($currentUserGroups, $this->groupManager->getUserGroupIds($user));
$addUser = !empty($commonGroups);
}
if ($addUser) {
$status = [];
$uid = $user->getUID();
$userEmail = $user->getSystemEMailAddress();
if (array_key_exists($user->getUID(), $userStatuses)) {
$userStatus = $userStatuses[$user->getUID()];
$status = [
'status' => $userStatus->getStatus(),
'message' => $userStatus->getMessage(),
'icon' => $userStatus->getIcon(),
'clearAt' => $userStatus->getClearAt()
? (int)$userStatus->getClearAt()->format('U')
: null,
];
}
$result['exact'][] = [
'label' => $user->getDisplayName(),
'icon' => 'icon-user',
'subline' => $status['message'] ?? '',
'value' => [
'shareType' => IShare::TYPE_USER,
'shareWith' => $user->getUID(),
],
'shareWithDisplayNameUnique' => $userEmail !== null && $userEmail !== '' ? $userEmail : $uid,
'status' => $status,
];
}
}
}
$type = new SearchResultType('users');
$searchResult->addResultSet($type, $result['wide'], $result['exact']);
if (count($result['exact'])) {
$searchResult->markExactIdMatch($type);
}
return $hasMoreResults;
}
public function takeOutCurrentUser(array &$users): void {
$currentUser = $this->userSession->getUser();
if (!is_null($currentUser)) {
if (isset($users[$currentUser->getUID()])) {
unset($users[$currentUser->getUID()]);
}
}
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 OC\Collaboration\Reference\File;
use OC\Files\Node\NonExistingFile;
use OC\Files\Node\NonExistingFolder;
use OCP\Collaboration\Reference\IReferenceManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Share\Events\ShareCreatedEvent;
use OCP\Share\Events\ShareDeletedEvent;
/** @template-implements IEventListener<Event|NodeDeletedEvent|ShareDeletedEvent|ShareCreatedEvent> */
class FileReferenceEventListener implements IEventListener {
public function __construct(
private IReferenceManager $manager,
) {
}
public static function register(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addServiceListener(NodeDeletedEvent::class, FileReferenceEventListener::class);
$eventDispatcher->addServiceListener(ShareDeletedEvent::class, FileReferenceEventListener::class);
$eventDispatcher->addServiceListener(ShareCreatedEvent::class, FileReferenceEventListener::class);
}
/**
* @inheritDoc
*/
public function handle(Event $event): void {
if ($event instanceof NodeDeletedEvent) {
if ($event->getNode() instanceof NonExistingFolder || $event->getNode() instanceof NonExistingFile) {
return;
}
$this->manager->invalidateCache((string)$event->getNode()->getId());
}
if ($event instanceof ShareDeletedEvent) {
$this->manager->invalidateCache((string)$event->getShare()->getNodeId());
}
if ($event instanceof ShareCreatedEvent) {
$this->manager->invalidateCache((string)$event->getShare()->getNodeId());
}
}
}
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 OC\Collaboration\Reference\File;
use OC\User\NoUserException;
use OCP\Collaboration\Reference\ADiscoverableReferenceProvider;
use OCP\Collaboration\Reference\IReference;
use OCP\Collaboration\Reference\Reference;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\L10N\IFactory;
class FileReferenceProvider extends ADiscoverableReferenceProvider {
private ?string $userId;
private IL10N $l10n;
public function __construct(
private IURLGenerator $urlGenerator,
private IRootFolder $rootFolder,
IUserSession $userSession,
private IMimeTypeDetector $mimeTypeDetector,
private IPreview $previewManager,
IFactory $l10n,
) {
$this->userId = $userSession->getUser()?->getUID();
$this->l10n = $l10n->get('files');
}
public function matchReference(string $referenceText): bool {
return $this->getFilesAppLinkId($referenceText) !== null;
}
private function getFilesAppLinkId(string $referenceText): ?int {
$start = $this->urlGenerator->getAbsoluteURL('/apps/files/');
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/files/');
$fileId = null;
if (mb_strpos($referenceText, $start) === 0) {
$parts = parse_url($referenceText);
parse_str($parts['query'] ?? '', $query);
$fileId = isset($query['fileid']) ? (int)$query['fileid'] : $fileId;
$fileId = isset($query['openfile']) ? (int)$query['openfile'] : $fileId;
}
if (mb_strpos($referenceText, $startIndex) === 0) {
$parts = parse_url($referenceText);
parse_str($parts['query'] ?? '', $query);
$fileId = isset($query['fileid']) ? (int)$query['fileid'] : $fileId;
$fileId = isset($query['openfile']) ? (int)$query['openfile'] : $fileId;
}
if (mb_strpos($referenceText, $this->urlGenerator->getAbsoluteURL('/index.php/f/')) === 0) {
$fileId = str_replace($this->urlGenerator->getAbsoluteURL('/index.php/f/'), '', $referenceText);
}
if (mb_strpos($referenceText, $this->urlGenerator->getAbsoluteURL('/f/')) === 0) {
$fileId = str_replace($this->urlGenerator->getAbsoluteURL('/f/'), '', $referenceText);
}
return $fileId !== null ? (int)$fileId : null;
}
public function resolveReference(string $referenceText): ?IReference {
if ($this->matchReference($referenceText)) {
$reference = new Reference($referenceText);
try {
$this->fetchReference($reference);
} catch (NotFoundException $e) {
$reference->setRichObject('file', null);
$reference->setAccessible(false);
}
return $reference;
}
return null;
}
/**
* @throws NotFoundException
*/
private function fetchReference(Reference $reference): void {
if ($this->userId === null) {
throw new NotFoundException();
}
$fileId = $this->getFilesAppLinkId($reference->getId());
if ($fileId === null) {
throw new NotFoundException();
}
try {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$files = $userFolder->getById($fileId);
if (empty($files)) {
throw new NotFoundException();
}
/** @var Node $file */
$file = array_shift($files);
$reference->setTitle($file->getName());
$reference->setDescription($file->getMimetype());
$reference->setUrl($this->urlGenerator->getAbsoluteURL('/index.php/f/' . $fileId));
if ($this->previewManager->isMimeSupported($file->getMimeType())) {
$reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 1600, 'y' => 630, 'fileId' => $fileId]));
} else {
$fileTypeIconUrl = $this->mimeTypeDetector->mimeTypeIcon($file->getMimeType());
$reference->setImageUrl($fileTypeIconUrl);
}
$reference->setRichObject('file', [
'id' => $file->getId(),
'name' => $file->getName(),
'size' => $file->getSize(),
'path' => $userFolder->getRelativePath($file->getPath()),
'link' => $reference->getUrl(),
'mimetype' => $file->getMimetype(),
'mtime' => $file->getMTime(),
'preview-available' => $this->previewManager->isAvailable($file)
]);
} catch (InvalidPathException|NotFoundException|NotPermittedException|NoUserException $e) {
throw new NotFoundException();
}
}
public function getCachePrefix(string $referenceId): string {
return (string)$this->getFilesAppLinkId($referenceId);
}
public function getCacheKey(string $referenceId): ?string {
return $this->userId ?? '';
}
public function getId(): string {
return 'files';
}
public function getTitle(): string {
return $this->l10n->t('Files');
}
public function getOrder(): int {
return 0;
}
public function getIconUrl(): string {
return $this->urlGenerator->imagePath('files', 'folder.svg');
}
}
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 OC\Collaboration\Reference;
use Fusonic\OpenGraph\Consumer;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\LimitStream;
use GuzzleHttp\Psr7\Utils;
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
use OC\Security\RateLimiting\Limiter;
use OC\SystemConfig;
use OCP\Collaboration\Reference\IReference;
use OCP\Collaboration\Reference\IReferenceProvider;
use OCP\Collaboration\Reference\Reference;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\NotFoundException;
use OCP\Http\Client\IClientService;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
class LinkReferenceProvider implements IReferenceProvider {
public const MAX_PREVIEW_SIZE = 1024 * 1024;
public const ALLOWED_CONTENT_TYPES = [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'image/webp'
];
public function __construct(
private IClientService $clientService,
private LoggerInterface $logger,
private SystemConfig $systemConfig,
private IAppDataFactory $appDataFactory,
private IURLGenerator $urlGenerator,
private Limiter $limiter,
private IUserSession $userSession,
private IRequest $request,
) {
}
public function matchReference(string $referenceText): bool {
if ($this->systemConfig->getValue('reference_opengraph', true) !== true) {
return false;
}
return (bool)preg_match(IURLGenerator::URL_REGEX, $referenceText);
}
public function resolveReference(string $referenceText): ?IReference {
if ($this->matchReference($referenceText)) {
$reference = new Reference($referenceText);
$this->fetchReference($reference);
return $reference;
}
return null;
}
private function fetchReference(Reference $reference): void {
try {
$user = $this->userSession->getUser();
if ($user) {
$this->limiter->registerUserRequest('opengraph', 10, 120, $user);
} else {
$this->limiter->registerAnonRequest('opengraph', 10, 120, $this->request->getRemoteAddress());
}
} catch (RateLimitExceededException $e) {
return;
}
$client = $this->clientService->newClient();
try {
$headResponse = $client->head($reference->getId(), [ 'timeout' => 10 ]);
} catch (\Exception $e) {
$this->logger->debug('Failed to perform HEAD request to get target metadata', ['exception' => $e]);
return;
}
$linkContentLength = $headResponse->getHeader('Content-Length');
if (is_numeric($linkContentLength) && (int) $linkContentLength > 5 * 1024 * 1024) {
$this->logger->debug('Skip resolving links pointing to content length > 5 MB');
return;
}
$linkContentType = $headResponse->getHeader('Content-Type');
$expectedContentType = 'text/html';
$suffixedExpectedContentType = $expectedContentType . ';';
$startsWithSuffixed = str_starts_with($linkContentType, $suffixedExpectedContentType);
// check the header begins with the expected content type
if ($linkContentType !== $expectedContentType && !$startsWithSuffixed) {
$this->logger->debug('Skip resolving links pointing to content type that is not "text/html"');
return;
}
try {
$response = $client->get($reference->getId(), [ 'timeout' => 10 ]);
} catch (\Exception $e) {
$this->logger->debug('Failed to fetch link for obtaining open graph data', ['exception' => $e]);
return;
}
$responseBody = (string)$response->getBody();
// OpenGraph handling
$consumer = new Consumer();
$consumer->useFallbackMode = true;
$object = $consumer->loadHtml($responseBody);
$reference->setUrl($reference->getId());
if ($object->title) {
$reference->setTitle($object->title);
}
if ($object->description) {
$reference->setDescription($object->description);
}
if ($object->images) {
try {
$host = parse_url($object->images[0]->url, PHP_URL_HOST);
if ($host === false || $host === null) {
$this->logger->warning('Could not detect host of open graph image URI for ' . $reference->getId());
} else {
$appData = $this->appDataFactory->get('core');
try {
$folder = $appData->getFolder('opengraph');
} catch (NotFoundException $e) {
$folder = $appData->newFolder('opengraph');
}
$response = $client->get($object->images[0]->url, ['timeout' => 10]);
$contentType = $response->getHeader('Content-Type');
$contentLength = $response->getHeader('Content-Length');
if (in_array($contentType, self::ALLOWED_CONTENT_TYPES, true) && $contentLength < self::MAX_PREVIEW_SIZE) {
$stream = Utils::streamFor($response->getBody());
$bodyStream = new LimitStream($stream, self::MAX_PREVIEW_SIZE, 0);
$reference->setImageContentType($contentType);
$folder->newFile(md5($reference->getId()), $bodyStream->getContents());
$reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Reference.preview', ['referenceId' => md5($reference->getId())]));
}
}
} catch (GuzzleException $e) {
$this->logger->info('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]);
} catch (\Throwable $e) {
$this->logger->error('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]);
}
}
}
public function getCachePrefix(string $referenceId): string {
return $referenceId;
}
public function getCacheKey(string $referenceId): ?string {
return null;
}
}
@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 OC\Collaboration\Reference;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Collaboration\Reference\File\FileReferenceProvider;
use OCP\Collaboration\Reference\IDiscoverableReferenceProvider;
use OCP\Collaboration\Reference\IReference;
use OCP\Collaboration\Reference\IReferenceManager;
use OCP\Collaboration\Reference\IReferenceProvider;
use OCP\Collaboration\Reference\Reference;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\IUserSession;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class ReferenceManager implements IReferenceManager {
public const CACHE_TTL = 3600;
/** @var IReferenceProvider[]|null */
private ?array $providers = null;
private ICache $cache;
public function __construct(
private LinkReferenceProvider $linkReferenceProvider,
ICacheFactory $cacheFactory,
private Coordinator $coordinator,
private ContainerInterface $container,
private LoggerInterface $logger,
private IConfig $config,
private IUserSession $userSession,
) {
$this->cache = $cacheFactory->createDistributed('reference');
}
/**
* Extract a list of URLs from a text
*
* @return string[]
*/
public function extractReferences(string $text): array {
preg_match_all(IURLGenerator::URL_REGEX, $text, $matches);
$references = $matches[0] ?? [];
return array_map(function ($reference) {
return trim($reference);
}, $references);
}
/**
* Try to get a cached reference object from a reference string
*/
public function getReferenceFromCache(string $referenceId): ?IReference {
$matchedProvider = $this->getMatchedProvider($referenceId);
if ($matchedProvider === null) {
return null;
}
$cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId);
return $this->getReferenceByCacheKey($cacheKey);
}
/**
* Try to get a cached reference object from a full cache key
*/
public function getReferenceByCacheKey(string $cacheKey): ?IReference {
$cached = $this->cache->get($cacheKey);
if ($cached) {
return Reference::fromCache($cached);
}
return null;
}
/**
* Get a reference object from a reference string with a matching provider
* Use a cached reference if possible
*/
public function resolveReference(string $referenceId): ?IReference {
$matchedProvider = $this->getMatchedProvider($referenceId);
if ($matchedProvider === null) {
return null;
}
$cacheKey = $this->getFullCacheKey($matchedProvider, $referenceId);
$cached = $this->cache->get($cacheKey);
if ($cached) {
return Reference::fromCache($cached);
}
$reference = $matchedProvider->resolveReference($referenceId);
if ($reference) {
$this->cache->set($cacheKey, Reference::toCache($reference), self::CACHE_TTL);
return $reference;
}
return null;
}
/**
* Try to match a reference string with all the registered providers
* Fallback to the link reference provider (using OpenGraph)
*
* @return IReferenceProvider|null the first matching provider
*/
private function getMatchedProvider(string $referenceId): ?IReferenceProvider {
$matchedProvider = null;
foreach ($this->getProviders() as $provider) {
$matchedProvider = $provider->matchReference($referenceId) ? $provider : null;
if ($matchedProvider !== null) {
break;
}
}
if ($matchedProvider === null && $this->linkReferenceProvider->matchReference($referenceId)) {
$matchedProvider = $this->linkReferenceProvider;
}
return $matchedProvider;
}
/**
* Get a hashed full cache key from a key and prefix given by a provider
*/
private function getFullCacheKey(IReferenceProvider $provider, string $referenceId): string {
$cacheKey = $provider->getCacheKey($referenceId);
return md5($provider->getCachePrefix($referenceId)) . (
$cacheKey !== null ? ('-' . md5($cacheKey)) : ''
);
}
/**
* Remove a specific cache entry from its key+prefix
*/
public function invalidateCache(string $cachePrefix, ?string $cacheKey = null): void {
if ($cacheKey === null) {
$this->cache->clear(md5($cachePrefix));
return;
}
$this->cache->remove(md5($cachePrefix) . '-' . md5($cacheKey));
}
/**
* @return IReferenceProvider[]
*/
public function getProviders(): array {
if ($this->providers === null) {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
return [];
}
$this->providers = array_filter(array_map(function ($registration): ?IReferenceProvider {
try {
/** @var IReferenceProvider $provider */
$provider = $this->container->get($registration->getService());
} catch (Throwable $e) {
$this->logger->error('Could not load reference provider ' . $registration->getService() . ': ' . $e->getMessage(), [
'exception' => $e,
]);
return null;
}
return $provider;
}, $context->getReferenceProviders()));
$this->providers[] = $this->container->get(FileReferenceProvider::class);
}
return $this->providers;
}
/**
* @inheritDoc
*/
public function getDiscoverableProviders(): array {
// preserve 0 based index to avoid returning an object in data responses
return array_values(
array_filter($this->getProviders(), static function (IReferenceProvider $provider) {
return $provider instanceof IDiscoverableReferenceProvider;
})
);
}
/**
* @inheritDoc
*/
public function touchProvider(string $userId, string $providerId, ?int $timestamp = null): bool {
$providers = $this->getDiscoverableProviders();
$matchingProviders = array_filter($providers, static function (IDiscoverableReferenceProvider $provider) use ($providerId) {
return $provider->getId() === $providerId;
});
if (!empty($matchingProviders)) {
if ($timestamp === null) {
$timestamp = time();
}
$configKey = 'provider-last-use_' . $providerId;
$this->config->setUserValue($userId, 'references', $configKey, (string) $timestamp);
return true;
}
return false;
}
/**
* @inheritDoc
*/
public function getUserProviderTimestamps(): array {
$user = $this->userSession->getUser();
if ($user === null) {
return [];
}
$userId = $user->getUID();
$keys = $this->config->getUserKeys($userId, 'references');
$prefix = 'provider-last-use_';
$keys = array_filter($keys, static function (string $key) use ($prefix) {
return str_starts_with($key, $prefix);
});
$timestamps = [];
foreach ($keys as $key) {
$providerId = substr($key, strlen($prefix));
$timestamp = (int) $this->config->getUserValue($userId, 'references', $key);
$timestamps[$providerId] = $timestamp;
}
return $timestamps;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julien Veyssier <eneiluj@posteo.net>
*
* @author Julien Veyssier <eneiluj@posteo.net>
*
* @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 OC\Collaboration\Reference;
use OCP\Collaboration\Reference\IDiscoverableReferenceProvider;
use OCP\Collaboration\Reference\IReferenceManager;
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\IEventListener;
use OCP\IInitialStateService;
/** @template-implements IEventListener<Event|RenderReferenceEvent> */
class RenderReferenceEventListener implements IEventListener {
public function __construct(
private IReferenceManager $manager,
private IInitialStateService $initialStateService,
) {
}
public static function register(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addServiceListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
}
/**
* @inheritDoc
*/
public function handle(Event $event): void {
if (!($event instanceof RenderReferenceEvent)) {
return;
}
$providers = $this->manager->getDiscoverableProviders();
$jsonProviders = array_map(static function (IDiscoverableReferenceProvider $provider) {
return $provider->jsonSerialize();
}, $providers);
$this->initialStateService->provideInitialState('core', 'reference-provider-list', $jsonProviders);
$timestamps = $this->manager->getUserProviderTimestamps();
$this->initialStateService->provideInitialState('core', 'reference-provider-timestamps', $timestamps);
}
}
@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 OC\Collaboration\Resources;
use Doctrine\DBAL\Exception\ConstraintViolationException;
use OCP\Collaboration\Resources\ICollection;
use OCP\Collaboration\Resources\IManager;
use OCP\Collaboration\Resources\IResource;
use OCP\Collaboration\Resources\ResourceException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
class Collection implements ICollection {
/** @var IResource[] */
protected array $resources = [];
public function __construct(
/** @var Manager $manager */
protected IManager $manager,
protected IDBConnection $connection,
protected int $id,
protected string $name,
protected ?IUser $userForAccess = null,
protected ?bool $access = null
) {
}
/**
* @since 16.0.0
*/
public function getId(): int {
return $this->id;
}
/**
* @since 16.0.0
*/
public function getName(): string {
return $this->name;
}
/**
* @since 16.0.0
*/
public function setName(string $name): void {
$query = $this->connection->getQueryBuilder();
$query->update(Manager::TABLE_COLLECTIONS)
->set('name', $query->createNamedParameter($name))
->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
$query->execute();
$this->name = $name;
}
/**
* @return IResource[]
* @since 16.0.0
*/
public function getResources(): array {
if (empty($this->resources)) {
$this->resources = $this->manager->getResourcesByCollectionForUser($this, $this->userForAccess);
}
return $this->resources;
}
/**
* Adds a resource to a collection
*
* @throws ResourceException when the resource is already part of the collection
* @since 16.0.0
*/
public function addResource(IResource $resource): void {
array_map(function (IResource $r) use ($resource) {
if ($this->isSameResource($r, $resource)) {
throw new ResourceException('Already part of the collection');
}
}, $this->getResources());
$this->resources[] = $resource;
$query = $this->connection->getQueryBuilder();
$query->insert(Manager::TABLE_RESOURCES)
->values([
'collection_id' => $query->createNamedParameter($this->id, IQueryBuilder::PARAM_INT),
'resource_type' => $query->createNamedParameter($resource->getType()),
'resource_id' => $query->createNamedParameter($resource->getId()),
]);
try {
$query->execute();
} catch (ConstraintViolationException $e) {
throw new ResourceException('Already part of the collection');
}
$this->manager->invalidateAccessCacheForCollection($this);
}
/**
* Removes a resource from a collection
*
* @since 16.0.0
*/
public function removeResource(IResource $resource): void {
$this->resources = array_filter($this->getResources(), function (IResource $r) use ($resource) {
return !$this->isSameResource($r, $resource);
});
$query = $this->connection->getQueryBuilder();
$query->delete(Manager::TABLE_RESOURCES)
->where($query->expr()->eq('collection_id', $query->createNamedParameter($this->id, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('resource_type', $query->createNamedParameter($resource->getType())))
->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId())));
$query->execute();
if (empty($this->resources)) {
$this->removeCollection();
} else {
$this->manager->invalidateAccessCacheForCollection($this);
}
}
/**
* Can a user/guest access the collection
*
* @since 16.0.0
*/
public function canAccess(?IUser $user): bool {
if ($user instanceof IUser) {
return $this->canUserAccess($user);
}
return $this->canGuestAccess();
}
protected function canUserAccess(IUser $user): bool {
if (\is_bool($this->access) && $this->userForAccess instanceof IUser && $user->getUID() === $this->userForAccess->getUID()) {
return $this->access;
}
$access = $this->manager->canAccessCollection($this, $user);
if ($this->userForAccess instanceof IUser && $user->getUID() === $this->userForAccess->getUID()) {
$this->access = $access;
}
return $access;
}
protected function canGuestAccess(): bool {
if (\is_bool($this->access) && !$this->userForAccess instanceof IUser) {
return $this->access;
}
$access = $this->manager->canAccessCollection($this, null);
if (!$this->userForAccess instanceof IUser) {
$this->access = $access;
}
return $access;
}
protected function isSameResource(IResource $resource1, IResource $resource2): bool {
return $resource1->getType() === $resource2->getType() &&
$resource1->getId() === $resource2->getId();
}
protected function removeCollection(): void {
$query = $this->connection->getQueryBuilder();
$query->delete(Manager::TABLE_COLLECTIONS)
->where($query->expr()->eq('id', $query->createNamedParameter($this->id, IQueryBuilder::PARAM_INT)));
$query->execute();
$this->manager->invalidateAccessCacheForCollection($this);
$this->id = 0;
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Collaboration\Resources;
use OCP\Collaboration\Resources\IManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Group\Events\BeforeGroupDeletedEvent;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\User\Events\UserDeletedEvent;
class Listener {
public static function register(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) {
$user = $event->getUser();
/** @var IManager $resourceManager */
$resourceManager = \OCP\Server::get(IManager::class);
$resourceManager->invalidateAccessCacheForUser($user);
});
$eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) {
$user = $event->getUser();
/** @var IManager $resourceManager */
$resourceManager = \OCP\Server::get(IManager::class);
$resourceManager->invalidateAccessCacheForUser($user);
});
$eventDispatcher->addListener(UserDeletedEvent::class, function (UserDeletedEvent $event) {
$user = $event->getUser();
/** @var IManager $resourceManager */
$resourceManager = \OCP\Server::get(IManager::class);
$resourceManager->invalidateAccessCacheForUser($user);
});
$eventDispatcher->addListener(BeforeGroupDeletedEvent::class, function (BeforeGroupDeletedEvent $event) {
$group = $event->getGroup();
/** @var IManager $resourceManager */
$resourceManager = \OCP\Server::get(IManager::class);
foreach ($group->getUsers() as $user) {
$resourceManager->invalidateAccessCacheForUser($user);
}
});
}
}
@@ -0,0 +1,486 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
*
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 OC\Collaboration\Resources;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OCP\Collaboration\Resources\CollectionException;
use OCP\Collaboration\Resources\ICollection;
use OCP\Collaboration\Resources\IManager;
use OCP\Collaboration\Resources\IProvider;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\Collaboration\Resources\IResource;
use OCP\Collaboration\Resources\ResourceException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
use Psr\Log\LoggerInterface;
class Manager implements IManager {
public const TABLE_COLLECTIONS = 'collres_collections';
public const TABLE_RESOURCES = 'collres_resources';
public const TABLE_ACCESS_CACHE = 'collres_accesscache';
/** @var string[] */
protected array $providers = [];
public function __construct(
protected IDBConnection $connection,
protected IProviderManager $providerManager,
protected LoggerInterface $logger,
) {
}
/**
* @throws CollectionException when the collection could not be found
* @since 16.0.0
*/
public function getCollection(int $id): ICollection {
$query = $this->connection->getQueryBuilder();
$query->select('*')
->from(self::TABLE_COLLECTIONS)
->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
throw new CollectionException('Collection not found');
}
return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name']);
}
/**
* @throws CollectionException when the collection could not be found
* @since 16.0.0
*/
public function getCollectionForUser(int $id, ?IUser $user): ICollection {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->select('*')
->from(self::TABLE_COLLECTIONS, 'c')
->leftJoin(
'c', self::TABLE_ACCESS_CACHE, 'a',
$query->expr()->andX(
$query->expr()->eq('c.id', 'a.collection_id'),
$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
)
)
->where($query->expr()->eq('c.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
throw new CollectionException('Collection not found');
}
$access = $row['access'] === null ? null : (bool) $row['access'];
if ($user instanceof IUser) {
return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access);
}
return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access);
}
/**
* @return ICollection[]
* @since 16.0.0
*/
public function searchCollections(IUser $user, string $filter, int $limit = 50, int $start = 0): array {
$query = $this->connection->getQueryBuilder();
$userId = $user->getUID();
$query->select('c.*', 'a.access')
->from(self::TABLE_COLLECTIONS, 'c')
->leftJoin(
'c', self::TABLE_ACCESS_CACHE, 'a',
$query->expr()->andX(
$query->expr()->eq('c.id', 'a.collection_id'),
$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
)
)
->where($query->expr()->eq('a.access', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
->orderBy('c.id')
->setMaxResults($limit)
->setFirstResult($start);
if ($filter !== '') {
$query->andWhere($query->expr()->iLike('c.name', $query->createNamedParameter('%' . $this->connection->escapeLikeParameter($filter) . '%')));
}
$result = $query->execute();
$collections = [];
$foundResults = 0;
while ($row = $result->fetch()) {
$foundResults++;
$access = $row['access'] === null ? null : (bool) $row['access'];
$collection = new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access);
if ($collection->canAccess($user)) {
$collections[] = $collection;
}
}
$result->closeCursor();
if (empty($collections) && $foundResults === $limit) {
return $this->searchCollections($user, $filter, $limit, $start + $limit);
}
return $collections;
}
/**
* @since 16.0.0
*/
public function newCollection(string $name): ICollection {
$query = $this->connection->getQueryBuilder();
$query->insert(self::TABLE_COLLECTIONS)
->values([
'name' => $query->createNamedParameter($name),
]);
$query->execute();
return new Collection($this, $this->connection, $query->getLastInsertId(), $name);
}
/**
* @since 16.0.0
*/
public function createResource(string $type, string $id): IResource {
return new Resource($this, $this->connection, $type, $id);
}
/**
* @throws ResourceException
* @since 16.0.0
*/
public function getResourceForUser(string $type, string $id, ?IUser $user): IResource {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->select('r.*', 'a.access')
->from(self::TABLE_RESOURCES, 'r')
->leftJoin(
'r', self::TABLE_ACCESS_CACHE, 'a',
$query->expr()->andX(
$query->expr()->eq('r.resource_id', 'a.resource_id'),
$query->expr()->eq('r.resource_type', 'a.resource_type'),
$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
)
)
->where($query->expr()->eq('r.resource_type', $query->createNamedParameter($type, IQueryBuilder::PARAM_STR)))
->andWhere($query->expr()->eq('r.resource_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_STR)));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
throw new ResourceException('Resource not found');
}
$access = $row['access'] === null ? null : (bool) $row['access'];
if ($user instanceof IUser) {
return new Resource($this, $this->connection, $type, $id, $user, $access);
}
return new Resource($this, $this->connection, $type, $id, null, $access);
}
/**
* @return IResource[]
* @since 16.0.0
*/
public function getResourcesByCollectionForUser(ICollection $collection, ?IUser $user): array {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->select('r.*', 'a.access')
->from(self::TABLE_RESOURCES, 'r')
->leftJoin(
'r', self::TABLE_ACCESS_CACHE, 'a',
$query->expr()->andX(
$query->expr()->eq('r.resource_id', 'a.resource_id'),
$query->expr()->eq('r.resource_type', 'a.resource_type'),
$query->expr()->eq('a.user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
)
)
->where($query->expr()->eq('r.collection_id', $query->createNamedParameter($collection->getId(), IQueryBuilder::PARAM_INT)));
$resources = [];
$result = $query->execute();
while ($row = $result->fetch()) {
$access = $row['access'] === null ? null : (bool) $row['access'];
$resources[] = new Resource($this, $this->connection, $row['resource_type'], $row['resource_id'], $user, $access);
}
$result->closeCursor();
return $resources;
}
/**
* Get the rich object data of a resource
*
* @since 16.0.0
*/
public function getResourceRichObject(IResource $resource): array {
foreach ($this->providerManager->getResourceProviders() as $provider) {
if ($provider->getType() === $resource->getType()) {
try {
return $provider->getResourceRichObject($resource);
} catch (ResourceException $e) {
}
}
}
return [];
}
/**
* Can a user/guest access the collection
*
* @since 16.0.0
*/
public function canAccessResource(IResource $resource, ?IUser $user): bool {
$access = $this->checkAccessCacheForUserByResource($resource, $user);
if (\is_bool($access)) {
return $access;
}
$access = false;
foreach ($this->providerManager->getResourceProviders() as $provider) {
if ($provider->getType() === $resource->getType()) {
try {
if ($provider->canAccessResource($resource, $user)) {
$access = true;
break;
}
} catch (ResourceException $e) {
}
}
}
$this->cacheAccessForResource($resource, $user, $access);
return $access;
}
/**
* Can a user/guest access the collection
*
* @since 16.0.0
*/
public function canAccessCollection(ICollection $collection, ?IUser $user): bool {
$access = $this->checkAccessCacheForUserByCollection($collection, $user);
if (\is_bool($access)) {
return $access;
}
$access = null;
// Access is granted when a user can access all resources
foreach ($collection->getResources() as $resource) {
if (!$resource->canAccess($user)) {
$access = false;
break;
}
$access = true;
}
$this->cacheAccessForCollection($collection, $user, $access);
return $access;
}
protected function checkAccessCacheForUserByResource(IResource $resource, ?IUser $user): ?bool {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->select('access')
->from(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId(), IQueryBuilder::PARAM_STR)))
->andWhere($query->expr()->eq('resource_type', $query->createNamedParameter($resource->getType(), IQueryBuilder::PARAM_STR)))
->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
->setMaxResults(1);
$hasAccess = null;
$result = $query->execute();
if ($row = $result->fetch()) {
$hasAccess = (bool) $row['access'];
}
$result->closeCursor();
return $hasAccess;
}
protected function checkAccessCacheForUserByCollection(ICollection $collection, ?IUser $user): ?bool {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->select('access')
->from(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId(), IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
->setMaxResults(1);
$hasAccess = null;
$result = $query->execute();
if ($row = $result->fetch()) {
$hasAccess = (bool) $row['access'];
}
$result->closeCursor();
return $hasAccess;
}
public function cacheAccessForResource(IResource $resource, ?IUser $user, bool $access): void {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->insert(self::TABLE_ACCESS_CACHE)
->values([
'user_id' => $query->createNamedParameter($userId),
'resource_id' => $query->createNamedParameter($resource->getId()),
'resource_type' => $query->createNamedParameter($resource->getType()),
'access' => $query->createNamedParameter($access, IQueryBuilder::PARAM_BOOL),
]);
try {
$query->execute();
} catch (UniqueConstraintViolationException $e) {
}
}
public function cacheAccessForCollection(ICollection $collection, ?IUser $user, bool $access): void {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->insert(self::TABLE_ACCESS_CACHE)
->values([
'user_id' => $query->createNamedParameter($userId),
'collection_id' => $query->createNamedParameter($collection->getId()),
'access' => $query->createNamedParameter($access, IQueryBuilder::PARAM_BOOL),
]);
try {
$query->execute();
} catch (UniqueConstraintViolationException $e) {
}
}
public function invalidateAccessCacheForUser(?IUser $user): void {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
$query->execute();
}
public function invalidateAccessCacheForResource(IResource $resource): void {
$query = $this->connection->getQueryBuilder();
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId())))
->andWhere($query->expr()->eq('resource_type', $query->createNamedParameter($resource->getType(), IQueryBuilder::PARAM_STR)));
$query->execute();
foreach ($resource->getCollections() as $collection) {
$this->invalidateAccessCacheForCollection($collection);
}
}
public function invalidateAccessCacheForAllCollections(): void {
$query = $this->connection->getQueryBuilder();
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->neq('collection_id', $query->createNamedParameter(0)));
$query->execute();
}
public function invalidateAccessCacheForCollection(ICollection $collection): void {
$query = $this->connection->getQueryBuilder();
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId())));
$query->execute();
}
public function invalidateAccessCacheForProvider(IProvider $provider): void {
$query = $this->connection->getQueryBuilder();
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('resource_type', $query->createNamedParameter($provider->getType(), IQueryBuilder::PARAM_STR)));
$query->execute();
}
public function invalidateAccessCacheForResourceByUser(IResource $resource, ?IUser $user): void {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('resource_id', $query->createNamedParameter($resource->getId())))
->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
$query->execute();
foreach ($resource->getCollections() as $collection) {
$this->invalidateAccessCacheForCollectionByUser($collection, $user);
}
}
protected function invalidateAccessCacheForCollectionByUser(ICollection $collection, ?IUser $user): void {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('collection_id', $query->createNamedParameter($collection->getId())))
->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
$query->execute();
}
public function invalidateAccessCacheForProviderByUser(IProvider $provider, ?IUser $user): void {
$query = $this->connection->getQueryBuilder();
$userId = $user instanceof IUser ? $user->getUID() : '';
$query->delete(self::TABLE_ACCESS_CACHE)
->where($query->expr()->eq('resource_type', $query->createNamedParameter($provider->getType(), IQueryBuilder::PARAM_STR)))
->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
$query->execute();
}
public function registerResourceProvider(string $provider): void {
$this->logger->debug('\OC\Collaboration\Resources\Manager::registerResourceProvider is deprecated', ['provider' => $provider]);
$this->providerManager->registerResourceProvider($provider);
}
/**
* Get the resource type of the provider
*
* @since 16.0.0
*/
public function getType(): string {
return '';
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Daniel Kesselberg <mail@danielkesselberg.de>
*
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Collaboration\Resources;
use OCP\AppFramework\QueryException;
use OCP\Collaboration\Resources\IProvider;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\IServerContainer;
use Psr\Log\LoggerInterface;
class ProviderManager implements IProviderManager {
/** @var string[] */
protected array $providers = [];
/** @var IProvider[] */
protected array $providerInstances = [];
public function __construct(
protected IServerContainer $serverContainer,
protected LoggerInterface $logger,
) {
}
public function getResourceProviders(): array {
if ($this->providers !== []) {
foreach ($this->providers as $provider) {
try {
$this->providerInstances[] = $this->serverContainer->query($provider);
} catch (QueryException $e) {
$this->logger->error("Could not query resource provider $provider: " . $e->getMessage(), [
'exception' => $e,
]);
}
}
$this->providers = [];
}
return $this->providerInstances;
}
public function registerResourceProvider(string $provider): void {
$this->providers[] = $provider;
}
}
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 OC\Collaboration\Resources;
use OCP\Collaboration\Resources\ICollection;
use OCP\Collaboration\Resources\IManager;
use OCP\Collaboration\Resources\IResource;
use OCP\IDBConnection;
use OCP\IUser;
class Resource implements IResource {
protected ?array $data = null;
public function __construct(
protected IManager $manager,
protected IDBConnection $connection,
protected string $type,
protected string $id,
protected ?IUser $userForAccess = null,
protected ?bool $access = null
) {
}
/**
* @since 16.0.0
*/
public function getType(): string {
return $this->type;
}
/**
* @since 16.0.0
*/
public function getId(): string {
return $this->id;
}
/**
* @since 16.0.0
*/
public function getRichObject(): array {
if ($this->data === null) {
$this->data = $this->manager->getResourceRichObject($this);
}
return $this->data;
}
/**
* Can a user/guest access the resource
*
* @since 16.0.0
*/
public function canAccess(?IUser $user): bool {
if ($user instanceof IUser) {
return $this->canUserAccess($user);
}
return $this->canGuestAccess();
}
protected function canUserAccess(IUser $user): bool {
if (\is_bool($this->access) && $this->userForAccess instanceof IUser && $user->getUID() === $this->userForAccess->getUID()) {
return $this->access;
}
$access = $this->manager->canAccessResource($this, $user);
if ($this->userForAccess instanceof IUser && $user->getUID() === $this->userForAccess->getUID()) {
$this->access = $access;
}
return $access;
}
protected function canGuestAccess(): bool {
if (\is_bool($this->access) && !$this->userForAccess instanceof IUser) {
return $this->access;
}
$access = $this->manager->canAccessResource($this, null);
if (!$this->userForAccess instanceof IUser) {
$this->access = $access;
}
return $access;
}
/**
* @return ICollection[]
* @since 16.0.0
*/
public function getCollections(): array {
$collections = [];
$query = $this->connection->getQueryBuilder();
$query->select('collection_id')
->from('collres_resources')
->where($query->expr()->eq('resource_type', $query->createNamedParameter($this->getType())))
->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($this->getId())));
$result = $query->execute();
while ($row = $result->fetch()) {
$collections[] = $this->manager->getCollection((int) $row['collection_id']);
}
$result->closeCursor();
return $collections;
}
}