Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9928813a72 | |||
| 6b6214a045 | |||
| 00af1217ff | |||
| 694e2312fd | |||
| 69a5db46bd | |||
| d6eade0119 | |||
| 8d1cb50048 | |||
| b4b328cf61 | |||
| e47195a334 | |||
| a56607e91f | |||
| 4d00f49757 | |||
| 83d795dd18 | |||
| a6b9483a5f | |||
| 636345bac8 | |||
| 79d4953e64 | |||
| ca8050b94e | |||
| 47b08a07d8 | |||
| d50fbfe37d | |||
| 77c070bc93 | |||
| 09c9241b30 | |||
| 711bd2bc6d | |||
| 2a81cba978 | |||
| 6df490942c | |||
| 0b8e7bb4f0 | |||
| 7e264ba58e | |||
| 64f319ab4e | |||
| ec0ed788fa | |||
| 67d1fac6f6 | |||
| af98eed523 | |||
| bfac9e7023 | |||
| 346c4bd69a | |||
| a12808e7c3 | |||
| 237d5156b6 | |||
| 40c39270c0 | |||
| 0d6c551694 | |||
| 14b42076ff | |||
| 3431441180 | |||
| d1f16ffaeb | |||
| f5b18dd7fd | |||
| 1cb826ea83 | |||
| 9b7feee481 | |||
| f885d7292f | |||
| e4244c5fc8 | |||
| 86c0e11a91 | |||
| 57100fe78e | |||
| d400dd4167 |
@@ -23,3 +23,7 @@
|
||||
- [ ] [Backports requested](https://github.com/nextcloud/backportbot/#usage) where applicable (ex: critical bugfixes)
|
||||
- [ ] [Labels added](https://github.com/nextcloud/server/labels) where applicable (ex: bug/enhancement, `3. to review`, feature component)
|
||||
- [ ] [Milestone added](https://github.com/nextcloud/server/milestones) for target branch/version (ex: 32.x for `stable32`)
|
||||
|
||||
## AI (if applicable)
|
||||
|
||||
- [ ] The content of this PR was partly or fully generated using AI
|
||||
|
||||
+1
-1
Submodule 3rdparty updated: 8f97d8cef3...34fdf0b083
@@ -77,6 +77,7 @@ return array(
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => $baseDir . '/../lib/CalDAV/Federation/FederationSharingService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => $baseDir . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
|
||||
|
||||
@@ -92,6 +92,7 @@ class ComposerStaticInitDAV
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederationSharingService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
|
||||
|
||||
@@ -191,6 +191,7 @@ OC.L10N.register(
|
||||
"{actor} updated contact {card} in address book {addressbook}" : "{actor} оновив(-ла) контакт {card} в адресній книзі {addressbook}",
|
||||
"You updated contact {card} in address book {addressbook}" : "Ви оновили контакт {card} в адресній книзі {addressbook}",
|
||||
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контактну</strong> або <strong>адресну книгу</strong> було змінено",
|
||||
"System address book disabled" : "Системну адресну книгу вимкнено",
|
||||
"Accounts" : "Облікові записи",
|
||||
"System address book which holds all accounts" : "Системна адресна книга, в якій містяться всі облікові записи",
|
||||
"File is not updatable: %1$s" : "Файл не оновлюється: %1$s",
|
||||
@@ -203,6 +204,8 @@ OC.L10N.register(
|
||||
"Could not rename part file to final file, canceled by hook" : "Не вдалося перейменувати файл частини на остаточний файл, скасовано підхопленням",
|
||||
"Could not rename part file to final file" : "Не вдалося перейменувати файл частини на остаточний файл",
|
||||
"Failed to check file size: %1$s" : "Не вдалося перевірити розмір файлу: %1$s",
|
||||
"Could not open file: %1$s (%2$d), file does seem to exist" : "Не вдалося відкрити файл: %1$s (%2$d), хоча схоже, що файл наявний",
|
||||
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Не вдалося відкрити файл: %1$s (%2$d), схоже, що файл відсутній",
|
||||
"Encryption not ready: %1$s" : "Шифрування не готове: %1$s",
|
||||
"Failed to open file: %1$s" : "Не вдалося відкрити файл: %1$s",
|
||||
"Failed to unlink: %1$s" : "Не вдалося від’єднати: %1$s",
|
||||
@@ -227,6 +230,10 @@ OC.L10N.register(
|
||||
"DAV system address book" : "Системна адресна книга DAV",
|
||||
"No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
|
||||
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
|
||||
"DAV system address book size" : "Розмір системної адресної книги DAV ",
|
||||
"The system address book is disabled" : "Системну адресну книгу вимкнено",
|
||||
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить більше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить менше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"WebDAV endpoint" : "Точка доступу WebDAV",
|
||||
"Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Неможливо перевірити, чи на вашому вебсервері правильно налаштовано доступ для синхронізації файлів через протокол WebDAV. Перевірте це вручну.",
|
||||
"Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ваш вебсервер не налаштований як треба для синхронізації файлів, схоже інтерфейс WebDAV поламаний.",
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
"{actor} updated contact {card} in address book {addressbook}" : "{actor} оновив(-ла) контакт {card} в адресній книзі {addressbook}",
|
||||
"You updated contact {card} in address book {addressbook}" : "Ви оновили контакт {card} в адресній книзі {addressbook}",
|
||||
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контактну</strong> або <strong>адресну книгу</strong> було змінено",
|
||||
"System address book disabled" : "Системну адресну книгу вимкнено",
|
||||
"Accounts" : "Облікові записи",
|
||||
"System address book which holds all accounts" : "Системна адресна книга, в якій містяться всі облікові записи",
|
||||
"File is not updatable: %1$s" : "Файл не оновлюється: %1$s",
|
||||
@@ -201,6 +202,8 @@
|
||||
"Could not rename part file to final file, canceled by hook" : "Не вдалося перейменувати файл частини на остаточний файл, скасовано підхопленням",
|
||||
"Could not rename part file to final file" : "Не вдалося перейменувати файл частини на остаточний файл",
|
||||
"Failed to check file size: %1$s" : "Не вдалося перевірити розмір файлу: %1$s",
|
||||
"Could not open file: %1$s (%2$d), file does seem to exist" : "Не вдалося відкрити файл: %1$s (%2$d), хоча схоже, що файл наявний",
|
||||
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Не вдалося відкрити файл: %1$s (%2$d), схоже, що файл відсутній",
|
||||
"Encryption not ready: %1$s" : "Шифрування не готове: %1$s",
|
||||
"Failed to open file: %1$s" : "Не вдалося відкрити файл: %1$s",
|
||||
"Failed to unlink: %1$s" : "Не вдалося від’єднати: %1$s",
|
||||
@@ -225,6 +228,10 @@
|
||||
"DAV system address book" : "Системна адресна книга DAV",
|
||||
"No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
|
||||
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
|
||||
"DAV system address book size" : "Розмір системної адресної книги DAV ",
|
||||
"The system address book is disabled" : "Системну адресну книгу вимкнено",
|
||||
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить більше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить менше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"WebDAV endpoint" : "Точка доступу WebDAV",
|
||||
"Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Неможливо перевірити, чи на вашому вебсервері правильно налаштовано доступ для синхронізації файлів через протокол WebDAV. Перевірте це вручну.",
|
||||
"Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ваш вебсервер не налаштований як треба для синхронізації файлів, схоже інтерфейс WebDAV поламаний.",
|
||||
|
||||
@@ -43,9 +43,9 @@ class CalendarProvider implements ICalendarProvider {
|
||||
});
|
||||
}
|
||||
|
||||
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
|
||||
$iCalendars = [];
|
||||
|
||||
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
|
||||
foreach ($calendarInfos as $calendarInfo) {
|
||||
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
|
||||
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
|
||||
@@ -60,9 +60,7 @@ class CalendarProvider implements ICalendarProvider {
|
||||
);
|
||||
}
|
||||
|
||||
$additionalFederatedProps = $this->getAdditionalPropertiesForCalendars(
|
||||
$federatedCalendarInfos,
|
||||
);
|
||||
$additionalFederatedProps = $this->getAdditionalPropertiesForCalendars($federatedCalendarInfos);
|
||||
foreach ($federatedCalendarInfos as $calendarInfo) {
|
||||
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
|
||||
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
|
||||
|
||||
@@ -104,9 +104,10 @@ class CalendarFederationProvider implements ICloudFederationProvider {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: implement read-write sharing
|
||||
// convert access to permissions
|
||||
$permissions = match ($access) {
|
||||
DavSharingBackend::ACCESS_READ => Constants::PERMISSION_READ,
|
||||
DavSharingBackend::ACCESS_READ_WRITE => Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE,
|
||||
default => throw new ProviderCouldNotAddShareException(
|
||||
"Unsupported access value: $access",
|
||||
'',
|
||||
@@ -122,20 +123,27 @@ class CalendarFederationProvider implements ICloudFederationProvider {
|
||||
$sharedWithPrincipal = 'principals/users/' . $share->getShareWith();
|
||||
|
||||
// Delete existing incoming federated share first
|
||||
$this->federatedCalendarMapper->deleteByUri($sharedWithPrincipal, $calendarUri);
|
||||
$calendar = $this->federatedCalendarMapper->findByUri($sharedWithPrincipal, $calendarUri);
|
||||
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setPrincipaluri($sharedWithPrincipal);
|
||||
$calendar->setUri($calendarUri);
|
||||
$calendar->setRemoteUrl($calendarUrl);
|
||||
$calendar->setDisplayName($displayName);
|
||||
$calendar->setColor($color);
|
||||
$calendar->setToken($share->getShareSecret());
|
||||
$calendar->setSharedBy($share->getSharedBy());
|
||||
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
|
||||
$calendar->setPermissions($permissions);
|
||||
$calendar->setComponents($components);
|
||||
$calendar = $this->federatedCalendarMapper->insert($calendar);
|
||||
if ($calendar === null) {
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setPrincipaluri($sharedWithPrincipal);
|
||||
$calendar->setUri($calendarUri);
|
||||
$calendar->setRemoteUrl($calendarUrl);
|
||||
$calendar->setDisplayName($displayName);
|
||||
$calendar->setColor($color);
|
||||
$calendar->setToken($share->getShareSecret());
|
||||
$calendar->setSharedBy($share->getSharedBy());
|
||||
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
|
||||
$calendar->setPermissions($permissions);
|
||||
$calendar->setComponents($components);
|
||||
$calendar = $this->federatedCalendarMapper->insert($calendar);
|
||||
} else {
|
||||
$calendar->setToken($share->getShareSecret());
|
||||
$calendar->setPermissions($permissions);
|
||||
$calendar->setComponents($components);
|
||||
$this->federatedCalendarMapper->update($calendar);
|
||||
}
|
||||
|
||||
$this->jobList->add(FederatedCalendarSyncJob::class, [
|
||||
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
|
||||
|
||||
@@ -10,29 +10,289 @@ declare(strict_types=1);
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Backend;
|
||||
use OCP\Constants;
|
||||
use Sabre\CalDAV\ICalendar;
|
||||
use Sabre\CalDAV\Plugin;
|
||||
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
|
||||
use Sabre\DAV\Exception\MethodNotAllowed;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\IMultiGet;
|
||||
use Sabre\DAV\INode;
|
||||
use Sabre\DAV\IProperties;
|
||||
use Sabre\DAV\PropPatch;
|
||||
|
||||
class FederatedCalendar implements ICalendar, IProperties, IMultiGet {
|
||||
|
||||
private const CALENDAR_TYPE = CalDavBackend::CALENDAR_TYPE_FEDERATED;
|
||||
private const DAV_PROPERTY_CALENDAR_LABEL = '{DAV:}displayname';
|
||||
private const DAV_PROPERTY_CALENDAR_COLOR = '{http://apple.com/ns/ical/}calendar-color';
|
||||
|
||||
private string $principalUri;
|
||||
private string $calendarUri;
|
||||
private ?array $calendarACL = null;
|
||||
private FederatedCalendarEntity $federationInfo;
|
||||
|
||||
class FederatedCalendar extends Calendar {
|
||||
public function __construct(
|
||||
Backend\BackendInterface $caldavBackend,
|
||||
$calendarInfo,
|
||||
IL10N $l10n,
|
||||
IConfig $config,
|
||||
LoggerInterface $logger,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly FederatedCalendarSyncService $federatedCalendarService,
|
||||
private readonly CalDavBackend $caldavBackend,
|
||||
$calendarInfo,
|
||||
) {
|
||||
parent::__construct($caldavBackend, $calendarInfo, $l10n, $config, $logger);
|
||||
$this->principalUri = $calendarInfo['principaluri'];
|
||||
$this->calendarUri = $calendarInfo['uri'];
|
||||
$this->federationInfo = $federatedCalendarMapper->findByUri($this->principalUri, $this->calendarUri);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$this->federatedCalendarMapper->deleteById($this->getResourceId());
|
||||
public function getResourceId(): int {
|
||||
return $this->federationInfo->getId();
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->federationInfo->getUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function setName($name): void {
|
||||
throw new MethodNotAllowed('Renaming federated calendars is not allowed');
|
||||
}
|
||||
|
||||
protected function getCalendarType(): int {
|
||||
return CalDavBackend::CALENDAR_TYPE_FEDERATED;
|
||||
return self::CALENDAR_TYPE;
|
||||
}
|
||||
|
||||
public function getPrincipalURI(): string {
|
||||
return $this->federationInfo->getPrincipaluri();
|
||||
}
|
||||
|
||||
public function getOwner(): ?string {
|
||||
return $this->federationInfo->getSharedByPrincipal();
|
||||
}
|
||||
|
||||
public function getGroup(): ?string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function getACL(): array {
|
||||
if ($this->calendarACL !== null) {
|
||||
return $this->calendarACL;
|
||||
}
|
||||
|
||||
$permissions = $this->federationInfo->getPermissions();
|
||||
// default permission
|
||||
$acl = [
|
||||
// read object permission
|
||||
[
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
],
|
||||
// read acl permission
|
||||
[
|
||||
'privilege' => '{DAV:}read-acl',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
],
|
||||
// write properties permission (calendar name, color)
|
||||
[
|
||||
'privilege' => '{DAV:}write-properties',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
],
|
||||
];
|
||||
// create permission
|
||||
if ($permissions & Constants::PERMISSION_CREATE) {
|
||||
$acl[] = [
|
||||
'privilege' => '{DAV:}bind',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
];
|
||||
}
|
||||
// update permission
|
||||
if ($permissions & Constants::PERMISSION_UPDATE) {
|
||||
$acl[] = [
|
||||
'privilege' => '{DAV:}write-content',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
];
|
||||
}
|
||||
// delete permission
|
||||
if ($permissions & Constants::PERMISSION_DELETE) {
|
||||
$acl[] = [
|
||||
'privilege' => '{DAV:}unbind',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// cache the calculated ACL for later use
|
||||
$this->calendarACL = $acl;
|
||||
|
||||
return $acl;
|
||||
}
|
||||
|
||||
public function setACL(array $acl): void {
|
||||
throw new MethodNotAllowed('Changing ACLs on federated calendars is not allowed');
|
||||
}
|
||||
|
||||
public function getSupportedPrivilegeSet(): ?array {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed> properties array, with property name as key
|
||||
*/
|
||||
public function getProperties($properties): array {
|
||||
return [
|
||||
self::DAV_PROPERTY_CALENDAR_LABEL => $this->federationInfo->getDisplayName(),
|
||||
self::DAV_PROPERTY_CALENDAR_COLOR => $this->federationInfo->getColor(),
|
||||
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(explode(',', $this->federationInfo->getComponents())),
|
||||
];
|
||||
}
|
||||
|
||||
public function propPatch(PropPatch $propPatch): void {
|
||||
$mutations = $propPatch->getMutations();
|
||||
if (count($mutations) > 0) {
|
||||
// evaluate if name was changed
|
||||
if (isset($mutations[self::DAV_PROPERTY_CALENDAR_LABEL])) {
|
||||
$this->federationInfo->setDisplayName($mutations[self::DAV_PROPERTY_CALENDAR_LABEL]);
|
||||
$propPatch->setResultCode(self::DAV_PROPERTY_CALENDAR_LABEL, 200);
|
||||
}
|
||||
// evaluate if color was changed
|
||||
if (isset($mutations[self::DAV_PROPERTY_CALENDAR_COLOR])) {
|
||||
$this->federationInfo->setColor($mutations[self::DAV_PROPERTY_CALENDAR_COLOR]);
|
||||
$propPatch->setResultCode(self::DAV_PROPERTY_CALENDAR_COLOR, 200);
|
||||
}
|
||||
$this->federatedCalendarMapper->update($this->federationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getChildACL(): array {
|
||||
return $this->getACL();
|
||||
}
|
||||
|
||||
public function getLastModified(): ?int {
|
||||
return $this->federationInfo->getLastSync();
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
$this->federatedCalendarMapper->deleteById($this->getResourceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function createDirectory($name): void {
|
||||
throw new MethodNotAllowed('Creating nested collection is not allowed');
|
||||
}
|
||||
|
||||
public function calendarQuery(array $filters): array {
|
||||
$uris = $this->caldavBackend->calendarQuery($this->federationInfo->getId(), $filters, $this->getCalendarType());
|
||||
return $uris;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function getChild($name): INode {
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
|
||||
|
||||
if ($obj === null) {
|
||||
throw new NotFound('Calendar object not found');
|
||||
}
|
||||
|
||||
return new FederatedCalendarObject($this, $obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<INode>
|
||||
*/
|
||||
public function getChildren(): array {
|
||||
$objs = $this->caldavBackend->getCalendarObjects($this->federationInfo->getId(), $this->getCalendarType());
|
||||
|
||||
$children = [];
|
||||
foreach ($objs as $obj) {
|
||||
$children[] = new FederatedCalendarObject($this, $obj);
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $paths Names of the files
|
||||
*
|
||||
* @return array<INode>
|
||||
*/
|
||||
public function getMultipleChildren(array $paths): array {
|
||||
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->federationInfo->getId(), $paths, $this->getCalendarType());
|
||||
|
||||
$children = [];
|
||||
foreach ($objs as $obj) {
|
||||
$children[] = new FederatedCalendarObject($this, $obj);
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function childExists($name): bool {
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
|
||||
return $obj !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
* @param resource|string $data Initial payload
|
||||
*/
|
||||
public function createFile($name, $data = null): string {
|
||||
if (is_resource($data)) {
|
||||
$data = stream_get_contents($data);
|
||||
}
|
||||
|
||||
// Create on remote server first
|
||||
$etag = $this->federatedCalendarService->createCalendarObject($this->federationInfo, $name, $data);
|
||||
|
||||
if (empty($etag)) {
|
||||
throw new \Exception('Failed to create calendar object on remote server');
|
||||
}
|
||||
|
||||
// Then store locally
|
||||
return $this->caldavBackend->createCalendarObject($this->federationInfo->getId(), $name, $data, $this->getCalendarType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
* @param resource|string $data Initial payload
|
||||
*/
|
||||
public function updateFile($name, $data): string {
|
||||
if (is_resource($data)) {
|
||||
$data = stream_get_contents($data);
|
||||
}
|
||||
|
||||
// Update remote calendar first
|
||||
$etag = $this->federatedCalendarService->updateCalendarObject($this->federationInfo, $name, $data);
|
||||
|
||||
if (empty($etag)) {
|
||||
throw new \Exception('Failed to update calendar object on remote server');
|
||||
}
|
||||
|
||||
// Then update locally
|
||||
return $this->caldavBackend->updateCalendarObject($this->federationInfo->getId(), $name, $data, $this->getCalendarType());
|
||||
}
|
||||
|
||||
public function deleteFile(string $name): void {
|
||||
// Delete from remote server first
|
||||
$this->federatedCalendarService->deleteCalendarObject($this->federationInfo, $name);
|
||||
|
||||
// Then delete locally
|
||||
$this->caldavBackend->deleteCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -94,8 +94,8 @@ class FederatedCalendarEntity extends Entity {
|
||||
'{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $this->getSyncTokenForSabre(),
|
||||
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => $this->getSupportedCalendarComponentSet(),
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->getSharedByPrincipal(),
|
||||
// TODO: implement read-write sharing
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => 1
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => ($this->getPermissions() & \OCP\Constants::PERMISSION_UPDATE) === 0 ? 1 : 0,
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}permissions' => $this->getPermissions(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,34 +9,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory as IL10NFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarFactory {
|
||||
private readonly IL10N $l10n;
|
||||
|
||||
public function __construct(
|
||||
private readonly CalDavBackend $caldavBackend,
|
||||
private readonly IConfig $config,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
IL10NFactory $l10nFactory,
|
||||
private readonly FederatedCalendarSyncService $federatedCalendarService,
|
||||
private readonly CalDavBackend $caldavBackend,
|
||||
) {
|
||||
$this->l10n = $l10nFactory->get(Application::APP_ID);
|
||||
}
|
||||
|
||||
public function createFederatedCalendar(array $calendarInfo): FederatedCalendar {
|
||||
return new FederatedCalendar(
|
||||
$this->federatedCalendarMapper,
|
||||
$this->federatedCalendarService,
|
||||
$this->caldavBackend,
|
||||
$calendarInfo,
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->logger,
|
||||
$this->federatedCalendarMapper,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
|
||||
}
|
||||
|
||||
public function getPermissions(): int {
|
||||
// TODO: implement read-write sharing
|
||||
return Constants::PERMISSION_READ;
|
||||
return $this->calendarInfo['{http://owncloud.org/ns}permissions'] ?? Constants::PERMISSION_READ;
|
||||
}
|
||||
|
||||
public function isDeleted(): bool {
|
||||
@@ -64,7 +63,8 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
|
||||
}
|
||||
|
||||
public function isWritable(): bool {
|
||||
return false;
|
||||
$permissions = $this->getPermissions();
|
||||
return ($permissions & Constants::PERMISSION_UPDATE) !== 0;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use Sabre\CalDAV\ICalendarObject;
|
||||
use Sabre\DAV\Exception\MethodNotAllowed;
|
||||
use Sabre\DAVACL\IACL;
|
||||
|
||||
class FederatedCalendarObject implements ICalendarObject, IACL {
|
||||
|
||||
public function __construct(
|
||||
protected FederatedCalendar $calendarObject,
|
||||
protected $objectData,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->objectData['uri'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function setName($name) {
|
||||
throw new \Exception('Not implemented');
|
||||
}
|
||||
|
||||
public function get(): string {
|
||||
return $this->objectData['calendardata'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|string $data contents of the file
|
||||
*/
|
||||
public function put($data): string {
|
||||
|
||||
$etag = $this->calendarObject->updateFile($this->objectData['uri'], $data);
|
||||
$this->objectData['calendardata'] = $data;
|
||||
$this->objectData['etag'] = $etag;
|
||||
|
||||
return $etag;
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
$this->calendarObject->deleteFile($this->objectData['uri']);
|
||||
}
|
||||
|
||||
public function getContentType(): ?string {
|
||||
$mime = 'text/calendar; charset=utf-8';
|
||||
if (isset($this->objectData['component']) && $this->objectData['component']) {
|
||||
$mime .= '; component=' . $this->objectData['component'];
|
||||
}
|
||||
|
||||
return $mime;
|
||||
}
|
||||
|
||||
public function getETag(): string {
|
||||
if (isset($this->objectData['etag'])) {
|
||||
return $this->objectData['etag'];
|
||||
} else {
|
||||
return '"' . md5($this->get()) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
public function getLastModified(): int {
|
||||
return $this->objectData['lastmodified'];
|
||||
}
|
||||
|
||||
public function getSize(): int {
|
||||
if (isset($this->objectData['size'])) {
|
||||
return $this->objectData['size'];
|
||||
} else {
|
||||
return strlen($this->get());
|
||||
}
|
||||
}
|
||||
|
||||
public function getOwner(): ?string {
|
||||
return $this->calendarObject->getPrincipalURI();
|
||||
}
|
||||
|
||||
public function getGroup(): ?string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function getACL(): array {
|
||||
return $this->calendarObject->getACL();
|
||||
}
|
||||
|
||||
public function setACL(array $acl): void {
|
||||
throw new MethodNotAllowed('Changing ACLs on federated events is not allowed');
|
||||
}
|
||||
|
||||
public function getSupportedPrivilegeSet(): ?array {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,20 +9,52 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\Service\ASyncService;
|
||||
use OCP\AppFramework\Db\TTransactional;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarSyncService {
|
||||
class FederatedCalendarSyncService extends ASyncService {
|
||||
use TTransactional;
|
||||
|
||||
private const SYNC_TOKEN_PREFIX = 'http://sabre.io/ns/sync/';
|
||||
|
||||
public function __construct(
|
||||
IClientService $clientService,
|
||||
IConfig $config,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly CalDavSyncService $syncService,
|
||||
private readonly CalDavBackend $backend,
|
||||
private readonly IDBConnection $dbConnection,
|
||||
private readonly ICloudIdManager $cloudIdManager,
|
||||
) {
|
||||
parent::__construct($clientService, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and encode credentials from a federated calendar entity.
|
||||
*
|
||||
* @return array{username: string, remoteUrl: string, token: string}
|
||||
*/
|
||||
private function getCredentials(FederatedCalendarEntity $calendar): array {
|
||||
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
|
||||
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
|
||||
|
||||
// Need to encode the cloud id as it might contain a colon which is not allowed in basic
|
||||
// auth according to RFC 7617
|
||||
$calDavUser = base64_encode($calDavUser);
|
||||
|
||||
return [
|
||||
'username' => $calDavUser,
|
||||
'remoteUrl' => $calendar->getRemoteUrl(),
|
||||
'token' => $calendar->getToken(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,29 +63,77 @@ class FederatedCalendarSyncService {
|
||||
* @throws ClientExceptionInterface If syncing the calendar fails.
|
||||
*/
|
||||
public function syncOne(FederatedCalendarEntity $calendar): int {
|
||||
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
|
||||
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
|
||||
$remoteUrl = $calendar->getRemoteUrl();
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$syncToken = $calendar->getSyncTokenForSabre();
|
||||
|
||||
// Need to encode the cloud id as it might contain a colon which is not allowed in basic
|
||||
// auth according to RFC 7617
|
||||
$calDavUser = base64_encode($calDavUser);
|
||||
try {
|
||||
$response = $this->requestSyncReport(
|
||||
$credentials['remoteUrl'],
|
||||
$credentials['username'],
|
||||
$credentials['token'],
|
||||
$syncToken,
|
||||
);
|
||||
} catch (ClientExceptionInterface $ex) {
|
||||
if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
|
||||
// Remote server revoked access to the calendar => remove it
|
||||
$this->federatedCalendarMapper->delete($calendar);
|
||||
$this->logger->warning("Authorization failed, remove federated calendar: {$credentials['remoteUrl']}", [
|
||||
'app' => 'dav',
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
$this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$syncResponse = $this->syncService->syncRemoteCalendar(
|
||||
$remoteUrl,
|
||||
$calDavUser,
|
||||
$calendar->getToken(),
|
||||
$syncToken,
|
||||
$calendar,
|
||||
);
|
||||
// Process changes from remote
|
||||
$downloadedEvents = 0;
|
||||
foreach ($response['response'] as $resource => $status) {
|
||||
$objectUri = basename($resource);
|
||||
if (isset($status[200])) {
|
||||
// Object created or updated
|
||||
$absoluteUrl = $this->prepareUri($credentials['remoteUrl'], $resource);
|
||||
$calendarData = $this->download($absoluteUrl, $credentials['username'], $credentials['token']);
|
||||
$this->atomic(function () use ($calendar, $objectUri, $calendarData): void {
|
||||
$existingObject = $this->backend->getCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED
|
||||
);
|
||||
if (!$existingObject) {
|
||||
$this->backend->createCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
$calendarData,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED
|
||||
);
|
||||
} else {
|
||||
$this->backend->updateCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
$calendarData,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED
|
||||
);
|
||||
}
|
||||
}, $this->dbConnection);
|
||||
$downloadedEvents++;
|
||||
} else {
|
||||
// Object deleted
|
||||
$this->backend->deleteCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$newSyncToken = $syncResponse->getSyncToken();
|
||||
$newSyncToken = $response['token'];
|
||||
|
||||
// Check sync token format and extract the actual sync token integer
|
||||
$matches = [];
|
||||
if (!preg_match('/^http:\/\/sabre\.io\/ns\/sync\/([0-9]+)$/', $newSyncToken, $matches)) {
|
||||
$this->logger->error("Failed to sync federated calendar at $remoteUrl: New sync token has unexpected format: $newSyncToken", [
|
||||
$this->logger->error("Failed to sync federated calendar at {$credentials['remoteUrl']}: New sync token has unexpected format: $newSyncToken", [
|
||||
'calendar' => $calendar->toCalendarInfo(),
|
||||
'newSyncToken' => $newSyncToken,
|
||||
]);
|
||||
@@ -67,10 +147,58 @@ class FederatedCalendarSyncService {
|
||||
$newSyncToken,
|
||||
);
|
||||
} else {
|
||||
$this->logger->debug("Sync Token for $remoteUrl unchanged from previous sync");
|
||||
$this->logger->debug("Sync Token for {$credentials['remoteUrl']} unchanged from previous sync");
|
||||
$this->federatedCalendarMapper->updateSyncTime($calendar->getId());
|
||||
}
|
||||
|
||||
return $syncResponse->getDownloadedEvents();
|
||||
return $downloadedEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a calendar object on the remote server.
|
||||
*
|
||||
* @throws ClientExceptionInterface If the remote request fails.
|
||||
*/
|
||||
public function createCalendarObject(FederatedCalendarEntity $calendar, string $name, string $data): string {
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
|
||||
|
||||
return $this->requestPut(
|
||||
$objectUrl,
|
||||
$credentials['username'],
|
||||
$credentials['token'],
|
||||
$data,
|
||||
'text/calendar; charset=utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a calendar object on the remote server.
|
||||
*
|
||||
* @throws ClientExceptionInterface If the remote request fails.
|
||||
*/
|
||||
public function updateCalendarObject(FederatedCalendarEntity $calendar, string $name, string $data): string {
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
|
||||
|
||||
return $this->requestPut(
|
||||
$objectUrl,
|
||||
$credentials['username'],
|
||||
$credentials['token'],
|
||||
$data,
|
||||
'text/calendar; charset=utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calendar object on the remote server.
|
||||
*
|
||||
* @throws ClientExceptionInterface If the remote request fails.
|
||||
*/
|
||||
public function deleteCalendarObject(FederatedCalendarEntity $calendar, string $name): void {
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
|
||||
|
||||
$this->requestDelete($objectUrl, $credentials['username'], $credentials['token']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\CalendarHome;
|
||||
use OCA\DAV\CalDAV\CalendarObject;
|
||||
use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendar;
|
||||
use OCA\DAV\CalDAV\TipBroker;
|
||||
use OCP\IConfig;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -173,8 +174,15 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Calendar $calendarNode */
|
||||
/** @var Calendar&ICalendar $calendarNode */
|
||||
$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
|
||||
|
||||
// abort if calendar is federated
|
||||
if ($calendarNode instanceof FederatedCalendar) {
|
||||
$this->logger->debug('Not processing scheduling for federated calendar at path: ' . $calendarPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// extract addresses for owner
|
||||
$addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
|
||||
// determine if request is from a sharee
|
||||
|
||||
@@ -8,7 +8,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\DAV\CardDAV\Notification;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory;
|
||||
@@ -42,7 +41,7 @@ class Notifier implements INotifier {
|
||||
*/
|
||||
public function prepare(INotification $notification, string $languageCode): INotification {
|
||||
if ($notification->getApp() !== Application::APP_ID) {
|
||||
throw new InvalidArgumentException();
|
||||
throw new UnknownNotificationException();
|
||||
}
|
||||
|
||||
$l = $this->l10nFactory->get(Application::APP_ID, $languageCode);
|
||||
|
||||
@@ -61,9 +61,31 @@ class ZipFolderPlugin extends ServerPlugin {
|
||||
$this->server->on('afterMethod:GET', $this->afterDownload(...), 999);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<NcNode>
|
||||
*/
|
||||
protected function createIterator(array $rootNodes): iterable {
|
||||
foreach ($rootNodes as $rootNode) {
|
||||
yield from $this->iterateNodes($rootNode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively iterate over all nodes in a folder.
|
||||
* @return iterable<NcNode>
|
||||
*/
|
||||
protected function iterateNodes(NcNode $node): iterable {
|
||||
yield $node;
|
||||
|
||||
if ($node instanceof NcFolder) {
|
||||
foreach ($node->getDirectoryListing() as $childNode) {
|
||||
yield from $this->iterateNodes($childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding a node to the archive streamer.
|
||||
* This will recursively add new nodes to the stream if the node is a directory.
|
||||
*/
|
||||
protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath): void {
|
||||
// Remove the root path from the filename to make it relative to the requested folder
|
||||
@@ -79,10 +101,6 @@ class ZipFolderPlugin extends ServerPlugin {
|
||||
$streamer->addFileFromStream($resource, $filename, $node->getSize(), $mtime);
|
||||
} elseif ($node instanceof NcFolder) {
|
||||
$streamer->addEmptyDir($filename, $mtime);
|
||||
$content = $node->getDirectoryListing();
|
||||
foreach ($content as $subNode) {
|
||||
$this->streamNode($streamer, $subNode, $rootPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +155,14 @@ class ZipFolderPlugin extends ServerPlugin {
|
||||
}
|
||||
|
||||
$folder = $node->getNode();
|
||||
$event = new BeforeZipCreatedEvent($folder, $files);
|
||||
$rootNodes = empty($files) ? $folder->getDirectoryListing() : [];
|
||||
foreach ($files as $path) {
|
||||
$child = $node->getChild($path);
|
||||
assert($child instanceof Node);
|
||||
$rootNodes[] = $child->getNode();
|
||||
}
|
||||
|
||||
$event = new BeforeZipCreatedEvent($folder, $files, $this->createIterator($rootNodes));
|
||||
$this->eventDispatcher->dispatchTyped($event);
|
||||
if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
|
||||
$errorMessage = $event->getErrorMessage();
|
||||
@@ -150,13 +175,6 @@ class ZipFolderPlugin extends ServerPlugin {
|
||||
throw new Forbidden($errorMessage);
|
||||
}
|
||||
|
||||
$content = empty($files) ? $folder->getDirectoryListing() : [];
|
||||
foreach ($files as $path) {
|
||||
$child = $node->getChild($path);
|
||||
assert($child instanceof Node);
|
||||
$content[] = $child->getNode();
|
||||
}
|
||||
|
||||
$archiveName = $folder->getName();
|
||||
if (count(explode('/', trim($folder->getPath(), '/'), 3)) === 2) {
|
||||
// this is a download of the root folder
|
||||
@@ -169,13 +187,13 @@ class ZipFolderPlugin extends ServerPlugin {
|
||||
$rootPath = dirname($folder->getPath());
|
||||
}
|
||||
|
||||
$streamer = new Streamer($tarRequest, -1, count($content), $this->timezoneFactory);
|
||||
$streamer = new Streamer($tarRequest, -1, count($rootNodes), $this->timezoneFactory);
|
||||
$streamer->sendHeaders($archiveName);
|
||||
// For full folder downloads we also add the folder itself to the archive
|
||||
if (empty($files)) {
|
||||
$streamer->addEmptyDir($archiveName);
|
||||
}
|
||||
foreach ($content as $node) {
|
||||
foreach ($event->getNodes() as $node) {
|
||||
$this->streamNode($streamer, $node, $rootPath);
|
||||
}
|
||||
$streamer->finalize();
|
||||
|
||||
@@ -191,4 +191,70 @@ abstract class ASyncService {
|
||||
rtrim($responseUri, '/'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push data to the remote server via HTTP PUT.
|
||||
* Used for creating or updating CalDAV/CardDAV objects.
|
||||
*
|
||||
* @param string $url The absolute URL to PUT to
|
||||
* @param string $username The username for authentication
|
||||
* @param string $token The authentication token/password
|
||||
* @param string $data The data to upload
|
||||
* @param string $contentType The Content-Type header (e.g., 'text/calendar' or 'text/vcard')
|
||||
*
|
||||
* @return string The ETag returned by the server
|
||||
*/
|
||||
protected function requestPut(
|
||||
string $url,
|
||||
string $username,
|
||||
string $token,
|
||||
string $data,
|
||||
string $contentType = 'text/calendar; charset=utf-8',
|
||||
): string {
|
||||
$client = $this->getClient();
|
||||
|
||||
$options = [
|
||||
'auth' => [$username, $token],
|
||||
'body' => $data,
|
||||
'headers' => [
|
||||
'Content-Type' => $contentType,
|
||||
],
|
||||
'verify' => !$this->config->getSystemValue(
|
||||
'sharing.federation.allowSelfSignedCertificates',
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
$response = $client->put($url, $options);
|
||||
|
||||
// Extract and return the ETag from the response
|
||||
$etag = $response->getHeader('ETag');
|
||||
return $etag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a resource from the remote server via HTTP DELETE.
|
||||
* Used for deleting CalDAV/CardDAV objects.
|
||||
*
|
||||
* @param string $url The absolute URL to DELETE
|
||||
* @param string $username The username for authentication
|
||||
* @param string $token The authentication token/password
|
||||
*/
|
||||
protected function requestDelete(
|
||||
string $url,
|
||||
string $username,
|
||||
string $token,
|
||||
): void {
|
||||
$client = $this->getClient();
|
||||
|
||||
$options = [
|
||||
'auth' => [$username, $token],
|
||||
'verify' => !$this->config->getSystemValue(
|
||||
'sharing.federation.allowSelfSignedCertificates',
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
$client->delete($url, $options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,12 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('deleteByUri')
|
||||
->method('findByUri')
|
||||
->with(
|
||||
'principals/users/sharee1',
|
||||
'ae4b8ab904076fff2b955ea21b1a0d92',
|
||||
);
|
||||
)
|
||||
->willReturn(null);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('insert')
|
||||
@@ -123,6 +124,68 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithExistingCalendar(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$share->method('getShareWith')
|
||||
->willReturn('sharee1');
|
||||
$share->method('getShareSecret')
|
||||
->willReturn('new-token');
|
||||
$share->method('getSharedBy')
|
||||
->willReturn('user1@nextcloud.remote');
|
||||
$share->method('getSharedByDisplayName')
|
||||
->willReturn('User 1');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$existingCalendar = new FederatedCalendarEntity();
|
||||
$existingCalendar->setId(10);
|
||||
$existingCalendar->setPrincipaluri('principals/users/sharee1');
|
||||
$existingCalendar->setUri('ae4b8ab904076fff2b955ea21b1a0d92');
|
||||
$existingCalendar->setRemoteUrl('https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1');
|
||||
$existingCalendar->setToken('old-token');
|
||||
$existingCalendar->setPermissions(1);
|
||||
$existingCalendar->setComponents('VEVENT');
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('findByUri')
|
||||
->with(
|
||||
'principals/users/sharee1',
|
||||
'ae4b8ab904076fff2b955ea21b1a0d92',
|
||||
)
|
||||
->willReturn($existingCalendar);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('update')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $calendar) {
|
||||
$this->assertEquals('new-token', $calendar->getToken());
|
||||
$this->assertEquals(1, $calendar->getPermissions());
|
||||
$this->assertEquals('VEVENT,VTODO', $calendar->getComponents());
|
||||
return $calendar;
|
||||
});
|
||||
|
||||
$this->jobList->expects(self::once())
|
||||
->method('add')
|
||||
->with(FederatedCalendarSyncJob::class, ['id' => 10]);
|
||||
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithInvalidProtocolVersion(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
@@ -270,7 +333,7 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
$this->calendarFederationProvider->shareReceived($share);
|
||||
}
|
||||
|
||||
public function testShareReceivedWithUnsupportedAccess(): void {
|
||||
public function testShareReceivedWithReadWriteAccess(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
@@ -296,6 +359,65 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('findByUri')
|
||||
->with(
|
||||
'principals/users/sharee1',
|
||||
'ae4b8ab904076fff2b955ea21b1a0d92',
|
||||
)
|
||||
->willReturn(null);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('insert')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $calendar) {
|
||||
$this->assertEquals('principals/users/sharee1', $calendar->getPrincipaluri());
|
||||
$this->assertEquals('ae4b8ab904076fff2b955ea21b1a0d92', $calendar->getUri());
|
||||
$this->assertEquals('https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1', $calendar->getRemoteUrl());
|
||||
$this->assertEquals('Calendar 1', $calendar->getDisplayName());
|
||||
$this->assertEquals('#ff0000', $calendar->getColor());
|
||||
$this->assertEquals('token', $calendar->getToken());
|
||||
$this->assertEquals('user1@nextcloud.remote', $calendar->getSharedBy());
|
||||
$this->assertEquals('User 1', $calendar->getSharedByDisplayName());
|
||||
$this->assertEquals(15, $calendar->getPermissions()); // READ | CREATE | UPDATE | DELETE
|
||||
$this->assertEquals('VEVENT,VTODO', $calendar->getComponents());
|
||||
|
||||
$calendar->setId(10);
|
||||
return $calendar;
|
||||
});
|
||||
|
||||
$this->jobList->expects(self::once())
|
||||
->method('add')
|
||||
->with(FederatedCalendarSyncJob::class, ['id' => 10]);
|
||||
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithUnsupportedAccess(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 999, // Invalid access value
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$share->method('getShareWith')
|
||||
->willReturn('sharee1');
|
||||
$share->method('getShareSecret')
|
||||
->willReturn('token');
|
||||
$share->method('getSharedBy')
|
||||
->willReturn('user1@nextcloud.remote');
|
||||
$share->method('getSharedByDisplayName')
|
||||
->willReturn('User 1');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
|
||||
@@ -9,13 +9,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
|
||||
use OCA\DAV\CalDAV\SyncServiceResult;
|
||||
use OCP\Federation\ICloudId;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Http\Client\IClient;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -26,21 +30,30 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
|
||||
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private CalDavSyncService&MockObject $calDavSyncService;
|
||||
private CalDavBackend&MockObject $backend;
|
||||
private IDBConnection&MockObject $dbConnection;
|
||||
private ICloudIdManager&MockObject $cloudIdManager;
|
||||
private IClientService&MockObject $clientService;
|
||||
private IConfig&MockObject $config;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->calDavSyncService = $this->createMock(CalDavSyncService::class);
|
||||
$this->backend = $this->createMock(CalDavBackend::class);
|
||||
$this->dbConnection = $this->createMock(IDBConnection::class);
|
||||
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
|
||||
$this->clientService = $this->createMock(IClientService::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
|
||||
$this->federatedCalendarSyncService = new FederatedCalendarSyncService(
|
||||
$this->clientService,
|
||||
$this->config,
|
||||
$this->federatedCalendarMapper,
|
||||
$this->logger,
|
||||
$this->calDavSyncService,
|
||||
$this->backend,
|
||||
$this->dbConnection,
|
||||
$this->cloudIdManager,
|
||||
);
|
||||
}
|
||||
@@ -61,16 +74,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/101', 10));
|
||||
// Mock HTTP client for sync report
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getBody')
|
||||
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>http://sabre.io/ns/sync/101</d:sync-token></d:multistatus>');
|
||||
|
||||
$client->expects(self::once())
|
||||
->method('request')
|
||||
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
|
||||
->willReturn($response);
|
||||
|
||||
$this->clientService->method('newClient')
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->method('getSystemValueInt')
|
||||
->willReturn(30);
|
||||
$this->config->method('getSystemValue')
|
||||
->willReturn(false);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('updateSyncTokenAndTime')
|
||||
@@ -78,7 +99,7 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTime');
|
||||
|
||||
$this->assertEquals(10, $this->federatedCalendarSyncService->syncOne($calendar));
|
||||
$this->assertEquals(0, $this->federatedCalendarSyncService->syncOne($calendar));
|
||||
}
|
||||
|
||||
public function testSyncOneUnchanged(): void {
|
||||
@@ -97,16 +118,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/100', 0));
|
||||
// Mock HTTP client for sync report
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getBody')
|
||||
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>http://sabre.io/ns/sync/100</d:sync-token></d:multistatus>');
|
||||
|
||||
$client->expects(self::once())
|
||||
->method('request')
|
||||
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
|
||||
->willReturn($response);
|
||||
|
||||
$this->clientService->method('newClient')
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->method('getSystemValueInt')
|
||||
->willReturn(30);
|
||||
$this->config->method('getSystemValue')
|
||||
->willReturn(false);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTokenAndTime');
|
||||
@@ -143,16 +172,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult($syncToken, 10));
|
||||
// Mock HTTP client for sync report with unexpected token format
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getBody')
|
||||
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>' . $syncToken . '</d:sync-token></d:multistatus>');
|
||||
|
||||
$client->expects(self::once())
|
||||
->method('request')
|
||||
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
|
||||
->willReturn($response);
|
||||
|
||||
$this->clientService->method('newClient')
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->method('getSystemValueInt')
|
||||
->willReturn(30);
|
||||
$this->config->method('getSystemValue')
|
||||
->willReturn(false);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTokenAndTime');
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendar;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarObject;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use OCP\Constants;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Sabre\DAV\Exception\MethodNotAllowed;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\PropPatch;
|
||||
use Test\TestCase;
|
||||
|
||||
class FederatedCalendarTest extends TestCase {
|
||||
private FederatedCalendar $federatedCalendar;
|
||||
|
||||
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
|
||||
private FederatedCalendarSyncService&MockObject $federatedCalendarService;
|
||||
private CalDavBackend&MockObject $caldavBackend;
|
||||
private FederatedCalendarEntity $federationInfo;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
|
||||
$this->federatedCalendarService = $this->createMock(FederatedCalendarSyncService::class);
|
||||
$this->caldavBackend = $this->createMock(CalDavBackend::class);
|
||||
|
||||
$this->federationInfo = new FederatedCalendarEntity();
|
||||
$this->federationInfo->setId(10);
|
||||
$this->federationInfo->setPrincipaluri('principals/users/user1');
|
||||
$this->federationInfo->setUri('calendar-uri');
|
||||
$this->federationInfo->setDisplayName('Federated Calendar');
|
||||
$this->federationInfo->setColor('#ff0000');
|
||||
$this->federationInfo->setSharedBy('user2@nextcloud.remote');
|
||||
$this->federationInfo->setSharedByDisplayName('User 2');
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ);
|
||||
$this->federationInfo->setLastSync(1234567890);
|
||||
|
||||
$this->federatedCalendarMapper->method('findByUri')
|
||||
->with('principals/users/user1', 'calendar-uri')
|
||||
->willReturn($this->federationInfo);
|
||||
|
||||
$calendarInfo = [
|
||||
'principaluri' => 'principals/users/user1',
|
||||
'id' => 10,
|
||||
'uri' => 'calendar-uri',
|
||||
];
|
||||
|
||||
$this->federatedCalendar = new FederatedCalendar(
|
||||
$this->federatedCalendarMapper,
|
||||
$this->federatedCalendarService,
|
||||
$this->caldavBackend,
|
||||
$calendarInfo,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetResourceId(): void {
|
||||
$this->assertEquals(10, $this->federatedCalendar->getResourceId());
|
||||
}
|
||||
|
||||
public function testGetName(): void {
|
||||
$this->assertEquals('calendar-uri', $this->federatedCalendar->getName());
|
||||
}
|
||||
|
||||
public function testSetName(): void {
|
||||
$this->expectException(MethodNotAllowed::class);
|
||||
$this->expectExceptionMessage('Renaming federated calendars is not allowed');
|
||||
$this->federatedCalendar->setName('new-name');
|
||||
}
|
||||
|
||||
public function testGetPrincipalURI(): void {
|
||||
$this->assertEquals('principals/users/user1', $this->federatedCalendar->getPrincipalURI());
|
||||
}
|
||||
|
||||
public function testGetOwner(): void {
|
||||
$expected = 'principals/remote-users/' . base64_encode('user2@nextcloud.remote');
|
||||
$this->assertEquals($expected, $this->federatedCalendar->getOwner());
|
||||
}
|
||||
|
||||
public function testGetGroup(): void {
|
||||
$this->assertNull($this->federatedCalendar->getGroup());
|
||||
}
|
||||
|
||||
public function testGetACLWithReadOnlyPermissions(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(3, $acl);
|
||||
// Check basic read permissions
|
||||
$this->assertEquals('{DAV:}read', $acl[0]['privilege']);
|
||||
$this->assertTrue($acl[0]['protected']);
|
||||
$this->assertEquals('{DAV:}read-acl', $acl[1]['privilege']);
|
||||
$this->assertTrue($acl[1]['protected']);
|
||||
$this->assertEquals('{DAV:}write-properties', $acl[2]['privilege']);
|
||||
$this->assertTrue($acl[2]['protected']);
|
||||
}
|
||||
|
||||
public function testGetACLWithCreatePermission(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(4, $acl);
|
||||
// Check that create permission is added
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}bind', $privileges);
|
||||
}
|
||||
|
||||
public function testGetACLWithUpdatePermission(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(4, $acl);
|
||||
// Check that update permission is added (write-content, not write-properties which is already in base ACL)
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}write-content', $privileges);
|
||||
}
|
||||
|
||||
public function testGetACLWithDeletePermission(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_DELETE);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(4, $acl);
|
||||
// Check that delete permission is added
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}unbind', $privileges);
|
||||
}
|
||||
|
||||
public function testGetACLWithAllPermissions(): void {
|
||||
$this->federationInfo->setPermissions(
|
||||
Constants::PERMISSION_READ
|
||||
| Constants::PERMISSION_CREATE
|
||||
| Constants::PERMISSION_UPDATE
|
||||
| Constants::PERMISSION_DELETE
|
||||
);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(6, $acl);
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}read', $privileges);
|
||||
$this->assertContains('{DAV:}bind', $privileges);
|
||||
$this->assertContains('{DAV:}write-content', $privileges);
|
||||
$this->assertContains('{DAV:}write-properties', $privileges);
|
||||
$this->assertContains('{DAV:}unbind', $privileges);
|
||||
}
|
||||
|
||||
public function testSetACL(): void {
|
||||
$this->expectException(MethodNotAllowed::class);
|
||||
$this->expectExceptionMessage('Changing ACLs on federated calendars is not allowed');
|
||||
$this->federatedCalendar->setACL([]);
|
||||
}
|
||||
|
||||
public function testGetSupportedPrivilegeSet(): void {
|
||||
$this->assertNull($this->federatedCalendar->getSupportedPrivilegeSet());
|
||||
}
|
||||
|
||||
public function testGetProperties(): void {
|
||||
$properties = $this->federatedCalendar->getProperties([
|
||||
'{DAV:}displayname',
|
||||
'{http://apple.com/ns/ical/}calendar-color',
|
||||
]);
|
||||
|
||||
$this->assertEquals('Federated Calendar', $properties['{DAV:}displayname']);
|
||||
$this->assertEquals('#ff0000', $properties['{http://apple.com/ns/ical/}calendar-color']);
|
||||
}
|
||||
|
||||
public function testPropPatchWithDisplayName(): void {
|
||||
$propPatch = $this->createMock(PropPatch::class);
|
||||
$propPatch->method('getMutations')
|
||||
->willReturn([
|
||||
'{DAV:}displayname' => 'New Calendar Name',
|
||||
]);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('update')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $entity) {
|
||||
$this->assertEquals('New Calendar Name', $entity->getDisplayName());
|
||||
return $entity;
|
||||
});
|
||||
|
||||
$propPatch->expects(self::once())
|
||||
->method('setResultCode')
|
||||
->with('{DAV:}displayname', 200);
|
||||
|
||||
$this->federatedCalendar->propPatch($propPatch);
|
||||
}
|
||||
|
||||
public function testPropPatchWithColor(): void {
|
||||
$propPatch = $this->createMock(PropPatch::class);
|
||||
$propPatch->method('getMutations')
|
||||
->willReturn([
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#00ff00',
|
||||
]);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('update')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $entity) {
|
||||
$this->assertEquals('#00ff00', $entity->getColor());
|
||||
return $entity;
|
||||
});
|
||||
|
||||
$propPatch->expects(self::once())
|
||||
->method('setResultCode')
|
||||
->with('{http://apple.com/ns/ical/}calendar-color', 200);
|
||||
|
||||
$this->federatedCalendar->propPatch($propPatch);
|
||||
}
|
||||
|
||||
public function testPropPatchWithNoMutations(): void {
|
||||
$propPatch = $this->createMock(PropPatch::class);
|
||||
$propPatch->method('getMutations')
|
||||
->willReturn([]);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('update');
|
||||
|
||||
$propPatch->expects(self::never())
|
||||
->method('handle');
|
||||
|
||||
$this->federatedCalendar->propPatch($propPatch);
|
||||
}
|
||||
|
||||
public function testGetChildACL(): void {
|
||||
$this->assertEquals($this->federatedCalendar->getACL(), $this->federatedCalendar->getChildACL());
|
||||
}
|
||||
|
||||
public function testGetLastModified(): void {
|
||||
$this->assertEquals(1234567890, $this->federatedCalendar->getLastModified());
|
||||
}
|
||||
|
||||
public function testDelete(): void {
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('deleteById')
|
||||
->with(10);
|
||||
|
||||
$this->federatedCalendar->delete();
|
||||
}
|
||||
|
||||
public function testCreateDirectory(): void {
|
||||
$this->expectException(MethodNotAllowed::class);
|
||||
$this->expectExceptionMessage('Creating nested collection is not allowed');
|
||||
$this->federatedCalendar->createDirectory('test');
|
||||
}
|
||||
|
||||
public function testCalendarQuery(): void {
|
||||
$filters = ['comp-filter' => ['name' => 'VEVENT']];
|
||||
$expectedUris = ['event1.ics', 'event2.ics'];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('calendarQuery')
|
||||
->with(10, $filters, 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($expectedUris);
|
||||
|
||||
$result = $this->federatedCalendar->calendarQuery($filters);
|
||||
$this->assertEquals($expectedUris, $result);
|
||||
}
|
||||
|
||||
public function testGetChild(): void {
|
||||
$objectData = [
|
||||
'id' => 1,
|
||||
'uri' => 'event1.ics',
|
||||
'calendardata' => 'BEGIN:VCALENDAR...',
|
||||
];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'event1.ics', 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($objectData);
|
||||
|
||||
$child = $this->federatedCalendar->getChild('event1.ics');
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $child);
|
||||
}
|
||||
|
||||
public function testGetChildNotFound(): void {
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'nonexistent.ics', 2)
|
||||
->willReturn(null);
|
||||
|
||||
$this->expectException(NotFound::class);
|
||||
$this->federatedCalendar->getChild('nonexistent.ics');
|
||||
}
|
||||
|
||||
public function testGetChildren(): void {
|
||||
$objects = [
|
||||
['id' => 1, 'uri' => 'event1.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
['id' => 2, 'uri' => 'event2.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObjects')
|
||||
->with(10, 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($objects);
|
||||
|
||||
$children = $this->federatedCalendar->getChildren();
|
||||
$this->assertCount(2, $children);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[0]);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[1]);
|
||||
}
|
||||
|
||||
public function testGetMultipleChildren(): void {
|
||||
$paths = ['event1.ics', 'event2.ics'];
|
||||
$objects = [
|
||||
['id' => 1, 'uri' => 'event1.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
['id' => 2, 'uri' => 'event2.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getMultipleCalendarObjects')
|
||||
->with(10, $paths, 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($objects);
|
||||
|
||||
$children = $this->federatedCalendar->getMultipleChildren($paths);
|
||||
$this->assertCount(2, $children);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[0]);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[1]);
|
||||
}
|
||||
|
||||
public function testChildExists(): void {
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'event1.ics', 2)
|
||||
->willReturn(['id' => 1, 'uri' => 'event1.ics']);
|
||||
|
||||
$result = $this->federatedCalendar->childExists('event1.ics');
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testChildNotExists(): void {
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'nonexistent.ics', 2)
|
||||
->willReturn(null);
|
||||
|
||||
$result = $this->federatedCalendar->childExists('nonexistent.ics');
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testCreateFile(): void {
|
||||
$calendarData = 'BEGIN:VCALENDAR...END:VCALENDAR';
|
||||
$remoteEtag = '"remote-etag-123"';
|
||||
$localEtag = '"local-etag-456"';
|
||||
|
||||
$this->federatedCalendarService->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with($this->federationInfo, 'event1.ics', $calendarData)
|
||||
->willReturn($remoteEtag);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(10, 'event1.ics', $calendarData, 2)
|
||||
->willReturn($localEtag);
|
||||
|
||||
$result = $this->federatedCalendar->createFile('event1.ics', $calendarData);
|
||||
$this->assertEquals($localEtag, $result);
|
||||
}
|
||||
|
||||
public function testUpdateFile(): void {
|
||||
$calendarData = 'BEGIN:VCALENDAR...UPDATED...END:VCALENDAR';
|
||||
$remoteEtag = '"remote-etag-updated"';
|
||||
$localEtag = '"local-etag-updated"';
|
||||
|
||||
$this->federatedCalendarService->expects(self::once())
|
||||
->method('updateCalendarObject')
|
||||
->with($this->federationInfo, 'event1.ics', $calendarData)
|
||||
->willReturn($remoteEtag);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('updateCalendarObject')
|
||||
->with(10, 'event1.ics', $calendarData, 2)
|
||||
->willReturn($localEtag);
|
||||
|
||||
$result = $this->federatedCalendar->updateFile('event1.ics', $calendarData);
|
||||
$this->assertEquals($localEtag, $result);
|
||||
}
|
||||
|
||||
public function testDeleteFile(): void {
|
||||
$this->federatedCalendarService->expects(self::once())
|
||||
->method('deleteCalendarObject')
|
||||
->with($this->federationInfo, 'event1.ics');
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('deleteCalendarObject')
|
||||
->with(10, 'event1.ics', 2);
|
||||
|
||||
$this->federatedCalendar->deleteFile('event1.ics');
|
||||
}
|
||||
}
|
||||
@@ -545,7 +545,7 @@ class Crypt {
|
||||
$options,
|
||||
$iv);
|
||||
|
||||
if ($plainContent) {
|
||||
if ($plainContent !== false) {
|
||||
return $plainContent;
|
||||
} else {
|
||||
throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
|
||||
|
||||
@@ -42,8 +42,8 @@ OC.L10N.register(
|
||||
"The lookup server is only available for global scale." : "Сервер пошуку доступний тільки для глобального масштабу.",
|
||||
"Search global and public address book for people" : "Шукати користувачів у глобальній та публічній адресних книгах",
|
||||
"Allow people to publish their data to a global and public address book" : "Дозволити користувачам розміщувати власні дані у глобальній публічній адресній книзі",
|
||||
"Trusted federation" : "Довірена федерація",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Типово автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Trusted federation" : "Довірені об'єднані хмари",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Стандартно автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID, див. {url}",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID",
|
||||
"Share with me via Nextcloud" : "Поділіться зі мною у Nextcloud",
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
"The lookup server is only available for global scale." : "Сервер пошуку доступний тільки для глобального масштабу.",
|
||||
"Search global and public address book for people" : "Шукати користувачів у глобальній та публічній адресних книгах",
|
||||
"Allow people to publish their data to a global and public address book" : "Дозволити користувачам розміщувати власні дані у глобальній публічній адресній книзі",
|
||||
"Trusted federation" : "Довірена федерація",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Типово автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Trusted federation" : "Довірені об'єднані хмари",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Стандартно автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID, див. {url}",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID",
|
||||
"Share with me via Nextcloud" : "Поділіться зі мною у Nextcloud",
|
||||
|
||||
@@ -11,7 +11,18 @@ OC.L10N.register(
|
||||
"Federation" : "Об'єднання",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів.",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар.",
|
||||
"Could not add trusted server. Please try again later." : "Не вдалося додати довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Add trusted server" : "Додати довірений сервер",
|
||||
"Server url" : " Посилання на сервер",
|
||||
"Add" : "Додати",
|
||||
"Server ok" : "Сервер ОК",
|
||||
"User list was exchanged at least once successfully with the remote server." : "Принаймні один раз відбувся обмін списком користувачів з віддаленим сервером.",
|
||||
"Server pending" : "Очікування сервера",
|
||||
"Waiting for shared secret or initial user list exchange." : "Очікування парольної фрази спільного доступу або початкового обміну списком користувачів",
|
||||
"Server access revoked" : "Відкликано доступ для сервера",
|
||||
"Server failure" : "Помилка на стороні сервера",
|
||||
"Connection to the remote server failed or the remote server is misconfigured." : "Не вдалося встановити з'єднання з віддаленим сервером, або віддалений сервер має помилки з налаштуванням",
|
||||
"Failed to delete trusted server. Please try again later." : "Не вдалося вилучити довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Delete" : "Видалити",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар. Необов'язково додавати сервер яко довірений для створення спільного ресурсу між об'єднаними хмарами.",
|
||||
"Each server must validate the other. This process may require a few cron cycles." : "Кожен сервер має підтвердити один одного. Цей процес може вимагати кількох циклів виконання cron.",
|
||||
|
||||
@@ -9,7 +9,18 @@
|
||||
"Federation" : "Об'єднання",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів.",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар.",
|
||||
"Could not add trusted server. Please try again later." : "Не вдалося додати довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Add trusted server" : "Додати довірений сервер",
|
||||
"Server url" : " Посилання на сервер",
|
||||
"Add" : "Додати",
|
||||
"Server ok" : "Сервер ОК",
|
||||
"User list was exchanged at least once successfully with the remote server." : "Принаймні один раз відбувся обмін списком користувачів з віддаленим сервером.",
|
||||
"Server pending" : "Очікування сервера",
|
||||
"Waiting for shared secret or initial user list exchange." : "Очікування парольної фрази спільного доступу або початкового обміну списком користувачів",
|
||||
"Server access revoked" : "Відкликано доступ для сервера",
|
||||
"Server failure" : "Помилка на стороні сервера",
|
||||
"Connection to the remote server failed or the remote server is misconfigured." : "Не вдалося встановити з'єднання з віддаленим сервером, або віддалений сервер має помилки з налаштуванням",
|
||||
"Failed to delete trusted server. Please try again later." : "Не вдалося вилучити довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Delete" : "Видалити",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар. Необов'язково додавати сервер яко довірений для створення спільного ресурсу між об'єднаними хмарами.",
|
||||
"Each server must validate the other. This process may require a few cron cycles." : "Кожен сервер має підтвердити один одного. Цей процес може вимагати кількох циклів виконання cron.",
|
||||
|
||||
@@ -79,6 +79,7 @@ OC.L10N.register(
|
||||
"Go to the \"{dir}\" directory" : "\"{dir}\" klasörüne git",
|
||||
"Current directory path" : "Geçerli klasör yolu",
|
||||
"Share" : "Paylaş",
|
||||
"Reload content" : "İçeriği yeniden yükle",
|
||||
"Your have used your space quota and cannot upload files anymore" : "Depolama alanınızın tümünü kullandığınız için başka dosya yüklemezsiniz",
|
||||
"You do not have permission to upload or create files here." : "Buraya dosya yükleme ya da ekleme izniniz yok.",
|
||||
"Drag and drop files here to upload" : "Yüklemek istediğiniz dosyaları sürükleyip buraya bırakın",
|
||||
@@ -109,6 +110,7 @@ OC.L10N.register(
|
||||
"Last 30 days" : "Önceki 30 gün",
|
||||
"This year ({year})" : "Bu yıl ({year})",
|
||||
"Last year ({year})" : "Önceki yıl ({year})",
|
||||
"Custom range" : "Özel aralık",
|
||||
"Custom date range" : "Özel tarih aralığı",
|
||||
"Search everywhere" : "Her yerde ara",
|
||||
"Documents" : "Belgeler",
|
||||
@@ -120,6 +122,7 @@ OC.L10N.register(
|
||||
"Images" : "Görseller",
|
||||
"Videos" : "Görüntüler",
|
||||
"Filters" : "Süzgeçler",
|
||||
"Back to filters" : "Süzgeçlere dön",
|
||||
"Appearance" : "Görünüm",
|
||||
"Show hidden files" : "Gizli dosyaları görüntüle",
|
||||
"Show file type column" : "Dosya türü sütunu görüntülensin",
|
||||
@@ -231,6 +234,9 @@ OC.L10N.register(
|
||||
"Removing the file extension \"{old}\" may render the file unreadable." : "\"{old}\" dosya uzantısının kaldırılması dosyayı okunamaz yapabilir.",
|
||||
"Adding the file extension \"{new}\" may render the file unreadable." : "\"{new}\" dosya uzantısının eklenmesi dosyayı okunamaz yapabilir.",
|
||||
"Do not show this dialog again." : "Bu ileti bir daha görüntülenmesin.",
|
||||
"Rename file to hidden" : "Gizlemek için dosyayı yeniden adlandırın",
|
||||
"Prefixing a filename with a dot may render the file hidden." : "Dosya adının başına nokta koymak onu görünümlerde gizler.",
|
||||
"Are you sure you want to rename the file to \"{filename}\"?" : "Dosyanın adını \"{filename}\" olarak değiştirmek istediğinize emin misiniz?",
|
||||
"Cancel" : "İptal",
|
||||
"Rename" : "Yeniden adlandır",
|
||||
"Select file or folder to link to" : "Bağlantı verilecek dosya ya da klasörü seçin",
|
||||
@@ -245,6 +251,7 @@ OC.L10N.register(
|
||||
"Error during upload: {message}" : "Yükleme sırasında sorun çıktı: {message}",
|
||||
"Error during upload, status code {status}" : "Yüklenirken sorun çıktı, durum kodu {status}",
|
||||
"Unknown error during upload" : "Yükleme sırasında bilinmeyen bir sorun çıktı",
|
||||
"File list is reloading" : "Dosya listesi yeniden yükleniyor",
|
||||
"Loading current folder" : "Geçerli klasör yükleniyor",
|
||||
"Retry" : "Yeniden dene",
|
||||
"No files in here" : "Burada herhangi bir dosya yok",
|
||||
@@ -312,7 +319,9 @@ OC.L10N.register(
|
||||
"The files are locked" : "Dosyalar kilitli",
|
||||
"The file does not exist anymore" : "Dosya artık yok",
|
||||
"Moving \"{source}\" to \"{destination}\" …" : "\"{source}\", \"{destination}\" üzerine taşınıyor…",
|
||||
"Moving {count} files to \"{destination}\" …" : "{count} dosya \"{destination}\" konumuna taşınıyor…",
|
||||
"Copying \"{source}\" to \"{destination}\" …" : "\"{source}\", \"{destination}\" üzerine kopyalanıyor…",
|
||||
"Copying {count} files to \"{destination}\" …" : "{count} dosya \"{destination}\" konumuna kopyalanıyor…",
|
||||
"Choose destination" : "Hedefi seçin",
|
||||
"Copy to {target}" : "{target} içine kopyala",
|
||||
"Move to {target}" : "{target} içine taşı",
|
||||
@@ -337,6 +346,7 @@ OC.L10N.register(
|
||||
"Templates" : "Kalıplar",
|
||||
"New template folder" : "Yeni kalıp klasörü",
|
||||
"In folder" : "Klasörde",
|
||||
"Pick folder to search in" : "Aranacak klasörü seçin",
|
||||
"Search in all files" : "Tüm dosyalarda ara",
|
||||
"Search in folder: {folder}" : "Şu klasörde ara: {folder}",
|
||||
"One of the dropped files could not be processed" : "Bırakılan dosyalardan biri işlenemedi",
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"Go to the \"{dir}\" directory" : "\"{dir}\" klasörüne git",
|
||||
"Current directory path" : "Geçerli klasör yolu",
|
||||
"Share" : "Paylaş",
|
||||
"Reload content" : "İçeriği yeniden yükle",
|
||||
"Your have used your space quota and cannot upload files anymore" : "Depolama alanınızın tümünü kullandığınız için başka dosya yüklemezsiniz",
|
||||
"You do not have permission to upload or create files here." : "Buraya dosya yükleme ya da ekleme izniniz yok.",
|
||||
"Drag and drop files here to upload" : "Yüklemek istediğiniz dosyaları sürükleyip buraya bırakın",
|
||||
@@ -107,6 +108,7 @@
|
||||
"Last 30 days" : "Önceki 30 gün",
|
||||
"This year ({year})" : "Bu yıl ({year})",
|
||||
"Last year ({year})" : "Önceki yıl ({year})",
|
||||
"Custom range" : "Özel aralık",
|
||||
"Custom date range" : "Özel tarih aralığı",
|
||||
"Search everywhere" : "Her yerde ara",
|
||||
"Documents" : "Belgeler",
|
||||
@@ -118,6 +120,7 @@
|
||||
"Images" : "Görseller",
|
||||
"Videos" : "Görüntüler",
|
||||
"Filters" : "Süzgeçler",
|
||||
"Back to filters" : "Süzgeçlere dön",
|
||||
"Appearance" : "Görünüm",
|
||||
"Show hidden files" : "Gizli dosyaları görüntüle",
|
||||
"Show file type column" : "Dosya türü sütunu görüntülensin",
|
||||
@@ -229,6 +232,9 @@
|
||||
"Removing the file extension \"{old}\" may render the file unreadable." : "\"{old}\" dosya uzantısının kaldırılması dosyayı okunamaz yapabilir.",
|
||||
"Adding the file extension \"{new}\" may render the file unreadable." : "\"{new}\" dosya uzantısının eklenmesi dosyayı okunamaz yapabilir.",
|
||||
"Do not show this dialog again." : "Bu ileti bir daha görüntülenmesin.",
|
||||
"Rename file to hidden" : "Gizlemek için dosyayı yeniden adlandırın",
|
||||
"Prefixing a filename with a dot may render the file hidden." : "Dosya adının başına nokta koymak onu görünümlerde gizler.",
|
||||
"Are you sure you want to rename the file to \"{filename}\"?" : "Dosyanın adını \"{filename}\" olarak değiştirmek istediğinize emin misiniz?",
|
||||
"Cancel" : "İptal",
|
||||
"Rename" : "Yeniden adlandır",
|
||||
"Select file or folder to link to" : "Bağlantı verilecek dosya ya da klasörü seçin",
|
||||
@@ -243,6 +249,7 @@
|
||||
"Error during upload: {message}" : "Yükleme sırasında sorun çıktı: {message}",
|
||||
"Error during upload, status code {status}" : "Yüklenirken sorun çıktı, durum kodu {status}",
|
||||
"Unknown error during upload" : "Yükleme sırasında bilinmeyen bir sorun çıktı",
|
||||
"File list is reloading" : "Dosya listesi yeniden yükleniyor",
|
||||
"Loading current folder" : "Geçerli klasör yükleniyor",
|
||||
"Retry" : "Yeniden dene",
|
||||
"No files in here" : "Burada herhangi bir dosya yok",
|
||||
@@ -310,7 +317,9 @@
|
||||
"The files are locked" : "Dosyalar kilitli",
|
||||
"The file does not exist anymore" : "Dosya artık yok",
|
||||
"Moving \"{source}\" to \"{destination}\" …" : "\"{source}\", \"{destination}\" üzerine taşınıyor…",
|
||||
"Moving {count} files to \"{destination}\" …" : "{count} dosya \"{destination}\" konumuna taşınıyor…",
|
||||
"Copying \"{source}\" to \"{destination}\" …" : "\"{source}\", \"{destination}\" üzerine kopyalanıyor…",
|
||||
"Copying {count} files to \"{destination}\" …" : "{count} dosya \"{destination}\" konumuna kopyalanıyor…",
|
||||
"Choose destination" : "Hedefi seçin",
|
||||
"Copy to {target}" : "{target} içine kopyala",
|
||||
"Move to {target}" : "{target} içine taşı",
|
||||
@@ -335,6 +344,7 @@
|
||||
"Templates" : "Kalıplar",
|
||||
"New template folder" : "Yeni kalıp klasörü",
|
||||
"In folder" : "Klasörde",
|
||||
"Pick folder to search in" : "Aranacak klasörü seçin",
|
||||
"Search in all files" : "Tüm dosyalarda ara",
|
||||
"Search in folder: {folder}" : "Şu klasörde ara: {folder}",
|
||||
"One of the dropped files could not be processed" : "Bırakılan dosyalardan biri işlenemedi",
|
||||
|
||||
@@ -387,7 +387,7 @@ OC.L10N.register(
|
||||
"Files that are not shared will show up here." : "Тут показуватимуться файли, які не є у спільному доступі.",
|
||||
"Recent" : "Останні",
|
||||
"List of recently modified files and folders." : "Список нещодавно змінених файлів та каталогів.",
|
||||
"No recently modified files" : "Відсутні файли із нещодавними змінами",
|
||||
"No recently modified files" : "Відсутні файли, які було нещодавно змінено",
|
||||
"Files and folders you recently modified will show up here." : "Тут показуватимуться файли та каталоги, які було нещодавно змінено.",
|
||||
"Search" : "Пошук",
|
||||
"Search results within your files." : "Шукати результати серед ваших файлів.",
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
"Files that are not shared will show up here." : "Тут показуватимуться файли, які не є у спільному доступі.",
|
||||
"Recent" : "Останні",
|
||||
"List of recently modified files and folders." : "Список нещодавно змінених файлів та каталогів.",
|
||||
"No recently modified files" : "Відсутні файли із нещодавними змінами",
|
||||
"No recently modified files" : "Відсутні файли, які було нещодавно змінено",
|
||||
"Files and folders you recently modified will show up here." : "Тут показуватимуться файли та каталоги, які було нещодавно змінено.",
|
||||
"Search" : "Пошук",
|
||||
"Search results within your files." : "Шукати результати серед ваших файлів.",
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('View in folder action conditions tests', () => {
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(80)
|
||||
expect(action.order).toBe(10)
|
||||
expect(action.enabled).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { IFileAction } from '@nextcloud/files'
|
||||
|
||||
import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
|
||||
import FolderEyeSvg from '@mdi/svg/svg/folder-eye-outline.svg?raw'
|
||||
import { FileType, Permission } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
@@ -15,7 +15,7 @@ export const action: IFileAction = {
|
||||
displayName() {
|
||||
return t('files', 'View in folder')
|
||||
},
|
||||
iconSvgInline: () => FolderMoveSvg,
|
||||
iconSvgInline: () => FolderEyeSvg,
|
||||
|
||||
enabled({ nodes, view }) {
|
||||
// Not enabled for public shares
|
||||
@@ -63,5 +63,5 @@ export const action: IFileAction = {
|
||||
return null
|
||||
},
|
||||
|
||||
order: 80,
|
||||
order: 10,
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class Backends extends Base {
|
||||
*/
|
||||
private function formatConfiguration(array $parameters): array {
|
||||
$configuration = array_filter($parameters, function (DefinitionParameter $parameter) {
|
||||
return $parameter->isFlagSet(DefinitionParameter::FLAG_HIDDEN);
|
||||
return !$parameter->isFlagSet(DefinitionParameter::FLAG_HIDDEN);
|
||||
});
|
||||
return array_map(function (DefinitionParameter $parameter) {
|
||||
return $parameter->getTypeName();
|
||||
|
||||
@@ -28,7 +28,7 @@ trait DependencyTrait {
|
||||
*
|
||||
* @return MissingDependency[] Unsatisfied required dependencies
|
||||
*/
|
||||
public function checkRequiredDependencies() {
|
||||
public function checkRequiredDependencies(): array {
|
||||
return array_filter(
|
||||
$this->checkDependencies(),
|
||||
fn (MissingDependency $dependency) => !$dependency->isOptional()
|
||||
|
||||
@@ -270,8 +270,8 @@ class AmazonS3 extends Common {
|
||||
$connection->deleteObjects([
|
||||
'Bucket' => $this->bucket,
|
||||
'Delete' => [
|
||||
'Quiet' => true,
|
||||
'Objects' => array_map(fn (array $object) => [
|
||||
'ETag' => $object['ETag'],
|
||||
'Key' => $object['Key'],
|
||||
], $objects['Contents'])
|
||||
]
|
||||
|
||||
@@ -178,7 +178,7 @@ class BackendService {
|
||||
* @return Backend[]
|
||||
*/
|
||||
public function getAvailableBackends() {
|
||||
return array_filter($this->getBackends(), fn (Backend $backend) => !$backend->checkRequiredDependencies());
|
||||
return array_filter($this->getBackends(), fn (Backend $backend) => $backend->checkRequiredDependencies() === []);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -233,7 +233,7 @@ OC.L10N.register(
|
||||
"Create a new share link" : "Креирајте нов линк за споделување",
|
||||
"Quick share options, the current selected is \"{selectedOption}\"" : "Опции за брзо споделување за , тековната избрана е \"{selectedOption}\"",
|
||||
"View only" : "Само за гледање",
|
||||
"Can edit" : "Може да се уредува",
|
||||
"Can edit" : "Може да уредува",
|
||||
"Custom permissions" : "Прилагодени дозволи",
|
||||
"Resharing is not allowed" : "Повторно споделување не е дозволено",
|
||||
"Name or email …" : "Име или е-пошта …",
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
"Create a new share link" : "Креирајте нов линк за споделување",
|
||||
"Quick share options, the current selected is \"{selectedOption}\"" : "Опции за брзо споделување за , тековната избрана е \"{selectedOption}\"",
|
||||
"View only" : "Само за гледање",
|
||||
"Can edit" : "Може да се уредува",
|
||||
"Can edit" : "Може да уредува",
|
||||
"Custom permissions" : "Прилагодени дозволи",
|
||||
"Resharing is not allowed" : "Повторно споделување не е дозволено",
|
||||
"Name or email …" : "Име или е-пошта …",
|
||||
|
||||
@@ -282,7 +282,7 @@ OC.L10N.register(
|
||||
"Advanced settings" : "Розширені",
|
||||
"Share label" : "Мітка спільного ресурсу",
|
||||
"Share link token" : "Токен спільного ресурсу",
|
||||
"Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information." : "Встановити публічне посилання на спільний ресурс у просту для запам'ятовування назву або створити новий токен. Не рекомендується використовувати токени, які можна легко вгадати для спільних ресурсів, які містять чутливі дані.",
|
||||
"Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information." : "Встановіть токен для публічного посилання на спільний ресурс з простою для запам'ятовування назвою або створіть новий токен. Не рекомендується використовувати токени, які можна легко вгадати, для спільних ресурсів, що містять чутливі дані.",
|
||||
"Generating…" : "Створення...",
|
||||
"Generate new token" : "Створити новий токен",
|
||||
"Set password" : "Встановити пароль",
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
"Advanced settings" : "Розширені",
|
||||
"Share label" : "Мітка спільного ресурсу",
|
||||
"Share link token" : "Токен спільного ресурсу",
|
||||
"Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information." : "Встановити публічне посилання на спільний ресурс у просту для запам'ятовування назву або створити новий токен. Не рекомендується використовувати токени, які можна легко вгадати для спільних ресурсів, які містять чутливі дані.",
|
||||
"Set the public share link token to something easy to remember or generate a new token. It is not recommended to use a guessable token for shares which contain sensitive information." : "Встановіть токен для публічного посилання на спільний ресурс з простою для запам'ятовування назвою або створіть новий токен. Не рекомендується використовувати токени, які можна легко вгадати, для спільних ресурсів, що містять чутливі дані.",
|
||||
"Generating…" : "Створення...",
|
||||
"Generate new token" : "Створити новий токен",
|
||||
"Set password" : "Встановити пароль",
|
||||
|
||||
@@ -24,6 +24,7 @@ class BeforeDirectFileDownloadListener implements IEventListener {
|
||||
public function __construct(
|
||||
private IUserSession $userSession,
|
||||
private IRootFolder $rootFolder,
|
||||
private ViewOnly $viewOnly,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -32,17 +33,17 @@ class BeforeDirectFileDownloadListener implements IEventListener {
|
||||
return;
|
||||
}
|
||||
|
||||
$pathsToCheck = [$event->getPath()];
|
||||
// Check only for user/group shares. Don't restrict e.g. share links
|
||||
$user = $this->userSession->getUser();
|
||||
if ($user) {
|
||||
$viewOnlyHandler = new ViewOnly(
|
||||
$this->rootFolder->getUserFolder($user->getUID())
|
||||
);
|
||||
if (!$viewOnlyHandler->check($pathsToCheck)) {
|
||||
$event->setSuccessful(false);
|
||||
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
|
||||
}
|
||||
// Check only for user/group shares. Don't restrict e.g. share links
|
||||
if (!$user) {
|
||||
return;
|
||||
|
||||
}
|
||||
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
||||
$node = $userFolder->get($event->getPath());
|
||||
if (!$this->viewOnly->isNodeCanBeDownloaded($node)) {
|
||||
$event->setSuccessful(false);
|
||||
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use OCA\Files_Sharing\ViewOnly;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Events\BeforeZipCreatedEvent;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Node;
|
||||
use OCP\IUserSession;
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,7 @@ class BeforeZipCreatedListener implements IEventListener {
|
||||
|
||||
public function __construct(
|
||||
private IUserSession $userSession,
|
||||
private IRootFolder $rootFolder,
|
||||
private ViewOnly $viewOnly,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -32,33 +32,19 @@ class BeforeZipCreatedListener implements IEventListener {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @psalm-suppress DeprecatedMethod should be migrated to getFolder but for now it would just duplicate code */
|
||||
$dir = $event->getDirectory();
|
||||
$files = $event->getFiles();
|
||||
|
||||
if (empty($files)) {
|
||||
$pathsToCheck = [$dir];
|
||||
} else {
|
||||
$pathsToCheck = [];
|
||||
foreach ($files as $file) {
|
||||
$pathsToCheck[] = $dir . '/' . $file;
|
||||
}
|
||||
}
|
||||
|
||||
// Check only for user/group shares. Don't restrict e.g. share links
|
||||
$user = $this->userSession->getUser();
|
||||
if ($user) {
|
||||
$viewOnlyHandler = new ViewOnly(
|
||||
$this->rootFolder->getUserFolder($user->getUID())
|
||||
);
|
||||
if (!$viewOnlyHandler->check($pathsToCheck)) {
|
||||
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
|
||||
$event->setSuccessful(false);
|
||||
} else {
|
||||
$event->setSuccessful(true);
|
||||
}
|
||||
} else {
|
||||
$event->setSuccessful(true);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check whether the user can download the requested folder
|
||||
if (!$this->viewOnly->isNodeCanBeDownloaded($event->getFolder())) {
|
||||
$event->setSuccessful(false);
|
||||
$event->setErrorMessage('Access to this resource has been denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check recursively whether the user can download nested nodes
|
||||
$event->addNodeFilter(fn (Node $node) => $this->viewOnly->isNodeCanBeDownloaded($node));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,80 +8,15 @@
|
||||
|
||||
namespace OCA\Files_Sharing;
|
||||
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\Node;
|
||||
use OCP\Files\NotFoundException;
|
||||
|
||||
/**
|
||||
* Handles restricting for download of files
|
||||
*/
|
||||
class ViewOnly {
|
||||
|
||||
public function __construct(
|
||||
private Folder $userFolder,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $pathsToCheck paths to check, relative to the user folder
|
||||
* @return bool
|
||||
*/
|
||||
public function check(array $pathsToCheck): bool {
|
||||
// If any of elements cannot be downloaded, prevent whole download
|
||||
foreach ($pathsToCheck as $file) {
|
||||
try {
|
||||
$info = $this->userFolder->get($file);
|
||||
if ($info instanceof File) {
|
||||
// access to filecache is expensive in the loop
|
||||
if (!$this->checkFileInfo($info)) {
|
||||
return false;
|
||||
}
|
||||
} elseif ($info instanceof Folder) {
|
||||
// get directory content is rather cheap query
|
||||
if (!$this->dirRecursiveCheck($info)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (NotFoundException $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Folder $dirInfo
|
||||
* @return bool
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function dirRecursiveCheck(Folder $dirInfo): bool {
|
||||
if (!$this->checkFileInfo($dirInfo)) {
|
||||
return false;
|
||||
}
|
||||
// If any of elements cannot be downloaded, prevent whole download
|
||||
$files = $dirInfo->getDirectoryListing();
|
||||
foreach ($files as $file) {
|
||||
if ($file instanceof File) {
|
||||
if (!$this->checkFileInfo($file)) {
|
||||
return false;
|
||||
}
|
||||
} elseif ($file instanceof Folder) {
|
||||
return $this->dirRecursiveCheck($file);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $fileInfo
|
||||
* @return bool
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function checkFileInfo(Node $fileInfo): bool {
|
||||
public function isNodeCanBeDownloaded(Node $node): bool {
|
||||
// Restrict view-only to nodes which are shared
|
||||
$storage = $fileInfo->getStorage();
|
||||
$storage = $node->getStorage();
|
||||
if (!$storage->instanceOfStorage(SharedStorage::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use OCA\Files_Sharing\AppInfo\Application;
|
||||
use OCA\Files_Sharing\Listener\BeforeDirectFileDownloadListener;
|
||||
use OCA\Files_Sharing\Listener\BeforeZipCreatedListener;
|
||||
use OCA\Files_Sharing\SharedStorage;
|
||||
use OCA\Files_Sharing\ViewOnly;
|
||||
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
|
||||
use OCP\Files\Events\BeforeZipCreatedEvent;
|
||||
use OCP\Files\File;
|
||||
@@ -91,7 +92,8 @@ class ApplicationTest extends TestCase {
|
||||
$event = new BeforeDirectFileDownloadEvent($path);
|
||||
$listener = new BeforeDirectFileDownloadListener(
|
||||
$this->userSession,
|
||||
$this->rootFolder
|
||||
$this->rootFolder,
|
||||
new ViewOnly(),
|
||||
);
|
||||
$listener->handle($event);
|
||||
|
||||
@@ -161,6 +163,13 @@ class ApplicationTest extends TestCase {
|
||||
);
|
||||
$folder->method('getDirectoryListing')->willReturn($directoryListing);
|
||||
}
|
||||
|
||||
// If the folder contains any secure-shared files, make it appear as a secure-shared folder
|
||||
// so that ViewOnly::isNodeCanBeDownloaded() will return false
|
||||
$containsSecureSharedFiles = in_array('secureSharedStorage', $directoryListing);
|
||||
if ($containsSecureSharedFiles && $folderStorage === 'nonSharedStorage') {
|
||||
$folder->method('getStorage')->willReturn($secureSharedStorage);
|
||||
}
|
||||
|
||||
$rootFolder = $this->createMock(Folder::class);
|
||||
$rootFolder->method('getStorage')->willReturn($nonSharedStorage);
|
||||
@@ -177,10 +186,11 @@ class ApplicationTest extends TestCase {
|
||||
$this->rootFolder->method('getUserFolder')->with('test')->willReturn($userFolder);
|
||||
|
||||
// Simulate zip download of folder folder
|
||||
$event = new BeforeZipCreatedEvent($dir, $files);
|
||||
$event = new BeforeZipCreatedEvent($folder, $files, $directoryListing);
|
||||
|
||||
$listener = new BeforeZipCreatedListener(
|
||||
$this->userSession,
|
||||
$this->rootFolder
|
||||
new ViewOnly(),
|
||||
);
|
||||
$listener->handle($event);
|
||||
|
||||
@@ -192,10 +202,10 @@ class ApplicationTest extends TestCase {
|
||||
$this->userSession->method('isLoggedIn')->willReturn(false);
|
||||
|
||||
// Simulate zip download of folder folder
|
||||
$event = new BeforeZipCreatedEvent('/test', ['test.txt']);
|
||||
$event = new BeforeZipCreatedEvent('/test', ['test.txt'], []);
|
||||
$listener = new BeforeZipCreatedListener(
|
||||
$this->userSession,
|
||||
$this->rootFolder
|
||||
new ViewOnly(),
|
||||
);
|
||||
$listener->handle($event);
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Profile" : "Профіль",
|
||||
"Searching …" : "Пошук …",
|
||||
"Not found" : "Не знойдзена",
|
||||
"Insert" : "Уставіць",
|
||||
"You have not added any info yet" : "Вы пакуль не дадалі ніякай інфармацыі",
|
||||
"{user} has not added any info yet" : "{user} пакуль не дадаў(-ла) ніякай інфармацыі",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "Памылка пры адкрыцці статусу карыстальніка, паспрабуйце абнавіць старонку",
|
||||
"Edit Profile" : "Рэдагаваць профіль",
|
||||
"Profile not found" : "Профіль не знойдзены",
|
||||
"The profile does not exist." : "Профіль не існуе.",
|
||||
"Back to %s" : "Назад да %s"
|
||||
},
|
||||
"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);");
|
||||
@@ -0,0 +1,14 @@
|
||||
{ "translations": {
|
||||
"Profile" : "Профіль",
|
||||
"Searching …" : "Пошук …",
|
||||
"Not found" : "Не знойдзена",
|
||||
"Insert" : "Уставіць",
|
||||
"You have not added any info yet" : "Вы пакуль не дадалі ніякай інфармацыі",
|
||||
"{user} has not added any info yet" : "{user} пакуль не дадаў(-ла) ніякай інфармацыі",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "Памылка пры адкрыцці статусу карыстальніка, паспрабуйце абнавіць старонку",
|
||||
"Edit Profile" : "Рэдагаваць профіль",
|
||||
"Profile not found" : "Профіль не знойдзены",
|
||||
"The profile does not exist." : "Профіль не існуе.",
|
||||
"Back to %s" : "Назад да %s"
|
||||
},"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Searching …" : "Cercant …",
|
||||
"Not found" : "No s'ha trobat",
|
||||
"Insert" : "Insereix",
|
||||
"You have not added any info yet" : "Encara no heu afegit cap informació",
|
||||
"{user} has not added any info yet" : "{user} encara no ha afegit cap informació",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "S'ha produït un error en obrir el quadre de diàleg modal d'estat de l'usuari, proveu d'actualitzar la pàgina",
|
||||
"Edit Profile" : "Edita el perfil",
|
||||
"The headline and about sections will show up here" : "La capçalera i les seccions d'informació es mostraran aquí",
|
||||
"Profile not found" : "No s'ha trobat el perfil",
|
||||
"The profile does not exist." : "El perfil no existeix.",
|
||||
"Back to %s" : "Torna a %s"
|
||||
},
|
||||
"nplurals=2; plural=(n != 1);");
|
||||
@@ -0,0 +1,14 @@
|
||||
{ "translations": {
|
||||
"Searching …" : "Cercant …",
|
||||
"Not found" : "No s'ha trobat",
|
||||
"Insert" : "Insereix",
|
||||
"You have not added any info yet" : "Encara no heu afegit cap informació",
|
||||
"{user} has not added any info yet" : "{user} encara no ha afegit cap informació",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "S'ha produït un error en obrir el quadre de diàleg modal d'estat de l'usuari, proveu d'actualitzar la pàgina",
|
||||
"Edit Profile" : "Edita el perfil",
|
||||
"The headline and about sections will show up here" : "La capçalera i les seccions d'informació es mostraran aquí",
|
||||
"Profile not found" : "No s'ha trobat el perfil",
|
||||
"The profile does not exist." : "El perfil no existeix.",
|
||||
"Back to %s" : "Torna a %s"
|
||||
},"pluralForm" :"nplurals=2; plural=(n != 1);"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Searching …" : "Leita …",
|
||||
"Not found" : "Fannst ekki",
|
||||
"Insert" : "Setja inn",
|
||||
"You have not added any info yet" : "Þú hefur ekki bætt við neinum upplýsingum ennþá",
|
||||
"{user} has not added any info yet" : "{user} hefur ekki bætt við neinum upplýsingum ennþá",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "Villa við að opna stöðuglugga notandans, prófaðu að þvinga endurlestur síðunnar",
|
||||
"Edit Profile" : "Breyta sniði",
|
||||
"The headline and about sections will show up here" : "Fyrirsögnin og hlutar um hugbúnaðinn munu birtast hér",
|
||||
"Profile not found" : "Sniðið finnst ekki",
|
||||
"The profile does not exist." : "Sniðið er ekki til.",
|
||||
"Back to %s" : "Til baka í %s"
|
||||
},
|
||||
"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);");
|
||||
@@ -0,0 +1,14 @@
|
||||
{ "translations": {
|
||||
"Searching …" : "Leita …",
|
||||
"Not found" : "Fannst ekki",
|
||||
"Insert" : "Setja inn",
|
||||
"You have not added any info yet" : "Þú hefur ekki bætt við neinum upplýsingum ennþá",
|
||||
"{user} has not added any info yet" : "{user} hefur ekki bætt við neinum upplýsingum ennþá",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "Villa við að opna stöðuglugga notandans, prófaðu að þvinga endurlestur síðunnar",
|
||||
"Edit Profile" : "Breyta sniði",
|
||||
"The headline and about sections will show up here" : "Fyrirsögnin og hlutar um hugbúnaðinn munu birtast hér",
|
||||
"Profile not found" : "Sniðið finnst ekki",
|
||||
"The profile does not exist." : "Sniðið er ekki til.",
|
||||
"Back to %s" : "Til baka í %s"
|
||||
},"pluralForm" :"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Profile" : "Профил",
|
||||
"Searching …" : "Пребарување …",
|
||||
"Not found" : "Не е пронајдено",
|
||||
"You have not added any info yet" : "Сè уште немате додадено никакви информации",
|
||||
"{user} has not added any info yet" : "{user} нема додадено никакви информации",
|
||||
"Edit Profile" : "Уреди профил",
|
||||
"The headline and about sections will show up here" : "Насловот и за секциите ќе се појават овде",
|
||||
"Profile not found" : "Профилот не е пронајден",
|
||||
"The profile does not exist." : "Профилот на постои",
|
||||
"Back to %s" : "Врати се на %s"
|
||||
},
|
||||
"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;");
|
||||
@@ -0,0 +1,13 @@
|
||||
{ "translations": {
|
||||
"Profile" : "Профил",
|
||||
"Searching …" : "Пребарување …",
|
||||
"Not found" : "Не е пронајдено",
|
||||
"You have not added any info yet" : "Сè уште немате додадено никакви информации",
|
||||
"{user} has not added any info yet" : "{user} нема додадено никакви информации",
|
||||
"Edit Profile" : "Уреди профил",
|
||||
"The headline and about sections will show up here" : "Насловот и за секциите ќе се појават овде",
|
||||
"Profile not found" : "Профилот не е пронајден",
|
||||
"The profile does not exist." : "Профилот на постои",
|
||||
"Back to %s" : "Врати се на %s"
|
||||
},"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Profile picker" : "Selector de perfil",
|
||||
"Profile" : "Perfil",
|
||||
"Searching …" : "Recèrca…",
|
||||
"Not found" : "Non trobat",
|
||||
"Search for a user profile" : "Cercar un perfil utilizaire",
|
||||
"Search for a user profile. Start typing" : "Cercar un perfil utilizaire. Començatz de picar",
|
||||
"Insert selected user profile link" : "Inserir lo ligam del perfil utilizaire seleccionat",
|
||||
"Insert" : "Inserir",
|
||||
"Edit Profile" : "Modificar perfil",
|
||||
"Profile not found" : "Perfil pas trobat",
|
||||
"The profile does not exist." : "Lo perfil existís pas.",
|
||||
"Back to %s" : "Tornar a %s"
|
||||
},
|
||||
"nplurals=2; plural=(n > 1);");
|
||||
@@ -0,0 +1,15 @@
|
||||
{ "translations": {
|
||||
"Profile picker" : "Selector de perfil",
|
||||
"Profile" : "Perfil",
|
||||
"Searching …" : "Recèrca…",
|
||||
"Not found" : "Non trobat",
|
||||
"Search for a user profile" : "Cercar un perfil utilizaire",
|
||||
"Search for a user profile. Start typing" : "Cercar un perfil utilizaire. Començatz de picar",
|
||||
"Insert selected user profile link" : "Inserir lo ligam del perfil utilizaire seleccionat",
|
||||
"Insert" : "Inserir",
|
||||
"Edit Profile" : "Modificar perfil",
|
||||
"Profile not found" : "Perfil pas trobat",
|
||||
"The profile does not exist." : "Lo perfil existís pas.",
|
||||
"Back to %s" : "Tornar a %s"
|
||||
},"pluralForm" :"nplurals=2; plural=(n > 1);"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Not found" : "Nu a fost găsit",
|
||||
"You have not added any info yet" : "Nu ați adăugat nicio informație",
|
||||
"{user} has not added any info yet" : "{user} nu a adăugat nicio informație",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "Eroare la deschiderea status utilizator, încercați refresh",
|
||||
"Edit Profile" : "Editare profil",
|
||||
"The headline and about sections will show up here" : "Secțiunile titlu și despre vor fi afișate aici",
|
||||
"Profile not found" : "Profil inexistent",
|
||||
"The profile does not exist." : "Profilul nu există",
|
||||
"Back to %s" : "Înapoi la %s"
|
||||
},
|
||||
"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));");
|
||||
@@ -0,0 +1,12 @@
|
||||
{ "translations": {
|
||||
"Not found" : "Nu a fost găsit",
|
||||
"You have not added any info yet" : "Nu ați adăugat nicio informație",
|
||||
"{user} has not added any info yet" : "{user} nu a adăugat nicio informație",
|
||||
"Error opening the user status modal, try hard refreshing the page" : "Eroare la deschiderea status utilizator, încercați refresh",
|
||||
"Edit Profile" : "Editare profil",
|
||||
"The headline and about sections will show up here" : "Secțiunile titlu și despre vor fi afișate aici",
|
||||
"Profile not found" : "Profil inexistent",
|
||||
"The profile does not exist." : "Profilul nu există",
|
||||
"Back to %s" : "Înapoi la %s"
|
||||
},"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Not found" : "ไม่พบ",
|
||||
"You have not added any info yet" : "คุณยังไม่ได้เพิ่มข้อมูลใด ๆ",
|
||||
"{user} has not added any info yet" : "{user} ยังไม่ได้เพิ่มข้อมูลใด ๆ",
|
||||
"Edit Profile" : "แก้ไขโปรไฟล์",
|
||||
"Profile not found" : "ไม่พบโปรไฟล์",
|
||||
"The profile does not exist." : "โปรไฟล์นี้ไม่มีอยู่",
|
||||
"Back to %s" : "กลับสู่ %s"
|
||||
},
|
||||
"nplurals=1; plural=0;");
|
||||
@@ -1,10 +0,0 @@
|
||||
{ "translations": {
|
||||
"Not found" : "ไม่พบ",
|
||||
"You have not added any info yet" : "คุณยังไม่ได้เพิ่มข้อมูลใด ๆ",
|
||||
"{user} has not added any info yet" : "{user} ยังไม่ได้เพิ่มข้อมูลใด ๆ",
|
||||
"Edit Profile" : "แก้ไขโปรไฟล์",
|
||||
"Profile not found" : "ไม่พบโปรไฟล์",
|
||||
"The profile does not exist." : "โปรไฟล์นี้ไม่มีอยู่",
|
||||
"Back to %s" : "กลับสู่ %s"
|
||||
},"pluralForm" :"nplurals=1; plural=0;"
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
OC.L10N.register(
|
||||
"profile",
|
||||
{
|
||||
"Profile picker" : "Profil seçici",
|
||||
"Profile" : "Profil",
|
||||
"This application provides the profile" : "Bu uygulama profili sağlar",
|
||||
"Provides a customisable user profile interface." : "Özelleştirilebilir bir kullanıcı profili arayüzü sağlar.",
|
||||
"Searching …" : "Aranıyor…",
|
||||
"Not found" : "Bulunamadı",
|
||||
"Search for a user profile" : "Kullanıcı profili ara",
|
||||
"Search for a user profile. Start typing" : "Aranacak kullanıcı profilini yazmaya başlayın",
|
||||
"Insert selected user profile link" : "Seçilmiş kullanıcı profili bağlantısını ekle",
|
||||
"Insert" : "Ekle",
|
||||
"You have not added any info yet" : "Henüz herhangi bir bilgi eklememişsiniz",
|
||||
"{user} has not added any info yet" : "{user} henüz herhangi bir bilgi eklememiş",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
{ "translations": {
|
||||
"Profile picker" : "Profil seçici",
|
||||
"Profile" : "Profil",
|
||||
"This application provides the profile" : "Bu uygulama profili sağlar",
|
||||
"Provides a customisable user profile interface." : "Özelleştirilebilir bir kullanıcı profili arayüzü sağlar.",
|
||||
"Searching …" : "Aranıyor…",
|
||||
"Not found" : "Bulunamadı",
|
||||
"Search for a user profile" : "Kullanıcı profili ara",
|
||||
"Search for a user profile. Start typing" : "Aranacak kullanıcı profilini yazmaya başlayın",
|
||||
"Insert selected user profile link" : "Seçilmiş kullanıcı profili bağlantısını ekle",
|
||||
"Insert" : "Ekle",
|
||||
"You have not added any info yet" : "Henüz herhangi bir bilgi eklememişsiniz",
|
||||
"{user} has not added any info yet" : "{user} henüz herhangi bir bilgi eklememiş",
|
||||
|
||||
@@ -593,8 +593,11 @@ OC.L10N.register(
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php ist bei einem Webcron-Dienst registriert, um cron.php alle 5 Minuten über HTTP aufzurufen. Anwendungsfall: Sehr kleine Instanz (1–5 Konten, je nach Nutzung).",
|
||||
"Cron (Recommended)" : "Cron (Empfohlen)",
|
||||
"Unable to update profile default setting" : "Standardeinstellung des Profils kann nicht aktualisiert werden",
|
||||
"Unable to update profile picker setting" : "Einstellungen der Profilauswahl konnten nicht aktualisiert werden",
|
||||
"Profile" : "Profil",
|
||||
"Enable or disable profile by default for new accounts." : "Profil für neue Konten standardmäßig aktivieren oder deaktivieren.",
|
||||
"Enable the profile picker" : "Profilauswahl aktivieren",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Aktivieren oder deaktivieren der Profilauswahl im Smart Picker und die Vorschau der Profilverknüpfungen.",
|
||||
"Password confirmation is required" : "Passwortbestätigung erforderlich",
|
||||
"Failed to save setting" : "Einstellung konnte nicht gespeichert werden",
|
||||
"{app}'s declarative setting field: {name}" : "Deklaratives Einstellungsfeld von {app}: {name}",
|
||||
|
||||
@@ -591,8 +591,11 @@
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php ist bei einem Webcron-Dienst registriert, um cron.php alle 5 Minuten über HTTP aufzurufen. Anwendungsfall: Sehr kleine Instanz (1–5 Konten, je nach Nutzung).",
|
||||
"Cron (Recommended)" : "Cron (Empfohlen)",
|
||||
"Unable to update profile default setting" : "Standardeinstellung des Profils kann nicht aktualisiert werden",
|
||||
"Unable to update profile picker setting" : "Einstellungen der Profilauswahl konnten nicht aktualisiert werden",
|
||||
"Profile" : "Profil",
|
||||
"Enable or disable profile by default for new accounts." : "Profil für neue Konten standardmäßig aktivieren oder deaktivieren.",
|
||||
"Enable the profile picker" : "Profilauswahl aktivieren",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Aktivieren oder deaktivieren der Profilauswahl im Smart Picker und die Vorschau der Profilverknüpfungen.",
|
||||
"Password confirmation is required" : "Passwortbestätigung erforderlich",
|
||||
"Failed to save setting" : "Einstellung konnte nicht gespeichert werden",
|
||||
"{app}'s declarative setting field: {name}" : "Deklaratives Einstellungsfeld von {app}: {name}",
|
||||
|
||||
@@ -593,8 +593,11 @@ OC.L10N.register(
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php ist bei einem Webcron-Dienst registriert, um cron.php alle 5 Minuten über HTTP aufzurufen. Anwendungsfall: Sehr kleine Instanz (1–5 Konten, je nach Nutzung).",
|
||||
"Cron (Recommended)" : "Cron (Empfohlen)",
|
||||
"Unable to update profile default setting" : "Standardeinstellung des Profils kann nicht aktualisiert werden",
|
||||
"Unable to update profile picker setting" : "Einstellungen der Profilauswahl konnten nicht aktualisiert werden",
|
||||
"Profile" : "Profil",
|
||||
"Enable or disable profile by default for new accounts." : "Profil für neue Konten standardmäßig aktivieren oder deaktivieren.",
|
||||
"Enable the profile picker" : "Profilauswahl aktivieren",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Aktivieren oder deaktivieren der Profilauswahl im Smart Picker und die Vorschau der Profilverknüpfungen.",
|
||||
"Password confirmation is required" : "Passwortbestätigung erforderlich",
|
||||
"Failed to save setting" : "Einstellung konnte nicht gespeichert werden",
|
||||
"{app}'s declarative setting field: {name}" : "Deklaratives Einstellungsfeld von {app}: {name}",
|
||||
|
||||
@@ -591,8 +591,11 @@
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php ist bei einem Webcron-Dienst registriert, um cron.php alle 5 Minuten über HTTP aufzurufen. Anwendungsfall: Sehr kleine Instanz (1–5 Konten, je nach Nutzung).",
|
||||
"Cron (Recommended)" : "Cron (Empfohlen)",
|
||||
"Unable to update profile default setting" : "Standardeinstellung des Profils kann nicht aktualisiert werden",
|
||||
"Unable to update profile picker setting" : "Einstellungen der Profilauswahl konnten nicht aktualisiert werden",
|
||||
"Profile" : "Profil",
|
||||
"Enable or disable profile by default for new accounts." : "Profil für neue Konten standardmäßig aktivieren oder deaktivieren.",
|
||||
"Enable the profile picker" : "Profilauswahl aktivieren",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Aktivieren oder deaktivieren der Profilauswahl im Smart Picker und die Vorschau der Profilverknüpfungen.",
|
||||
"Password confirmation is required" : "Passwortbestätigung erforderlich",
|
||||
"Failed to save setting" : "Einstellung konnte nicht gespeichert werden",
|
||||
"{app}'s declarative setting field: {name}" : "Deklaratives Einstellungsfeld von {app}: {name}",
|
||||
|
||||
@@ -593,8 +593,11 @@ OC.L10N.register(
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php está rexistrado nun servizo webcron para chamar a cron.php cada 5 minutos a través de HTTP. Caso de uso: instancia moi pequena (de 1 a 5 contas segundo o uso).",
|
||||
"Cron (Recommended)" : "Cron (Recomendado)",
|
||||
"Unable to update profile default setting" : "Non é posíbel actualizar a configuración predeterminada do perfil",
|
||||
"Unable to update profile picker setting" : "Non é posíbel actualizar a configuración do selector de perfil",
|
||||
"Profile" : "Perfil",
|
||||
"Enable or disable profile by default for new accounts." : "Activar ou desactivar o perfil predeterminado para as novas contas.",
|
||||
"Enable the profile picker" : "Activar o selector de perfil",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Activar ou desactivar o selector de perfís no Selector intelixente e nas vistas previas da ligazón ao perfil.",
|
||||
"Password confirmation is required" : "Requírese a confirmación do contrasinal",
|
||||
"Failed to save setting" : "Produciuse un fallo ao gardar o axuste",
|
||||
"{app}'s declarative setting field: {name}" : "Campo de axuste declarativo de {app}: {name}",
|
||||
|
||||
@@ -591,8 +591,11 @@
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php está rexistrado nun servizo webcron para chamar a cron.php cada 5 minutos a través de HTTP. Caso de uso: instancia moi pequena (de 1 a 5 contas segundo o uso).",
|
||||
"Cron (Recommended)" : "Cron (Recomendado)",
|
||||
"Unable to update profile default setting" : "Non é posíbel actualizar a configuración predeterminada do perfil",
|
||||
"Unable to update profile picker setting" : "Non é posíbel actualizar a configuración do selector de perfil",
|
||||
"Profile" : "Perfil",
|
||||
"Enable or disable profile by default for new accounts." : "Activar ou desactivar o perfil predeterminado para as novas contas.",
|
||||
"Enable the profile picker" : "Activar o selector de perfil",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Activar ou desactivar o selector de perfís no Selector intelixente e nas vistas previas da ligazón ao perfil.",
|
||||
"Password confirmation is required" : "Requírese a confirmación do contrasinal",
|
||||
"Failed to save setting" : "Produciuse un fallo ao gardar o axuste",
|
||||
"{app}'s declarative setting field: {name}" : "Campo de axuste declarativo de {app}: {name}",
|
||||
|
||||
@@ -593,8 +593,11 @@ OC.L10N.register(
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php é registrado em um serviço webcron para chamar cron.php a cada 5 minutos por HTTP. Caso de uso: Instância muito pequena (1–5 contas dependendo do uso).",
|
||||
"Cron (Recommended)" : "Cron (Recomendado)",
|
||||
"Unable to update profile default setting" : "Não foi possível atualizar a configuração padrão do perfil",
|
||||
"Unable to update profile picker setting" : "Não é possível atualizar a configuração do seletor de perfis",
|
||||
"Profile" : "Perfil",
|
||||
"Enable or disable profile by default for new accounts." : "Ativar ou desativar o perfil por padrão para novas contas.",
|
||||
"Enable the profile picker" : "Ativar o seletor de perfis",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Ative ou desative o seletor de perfis no seletor inteligente e as pré-visualizações dos links dos perfis.",
|
||||
"Password confirmation is required" : "A confirmação da senha é necessária",
|
||||
"Failed to save setting" : "Falha ao salvar a configuração",
|
||||
"{app}'s declarative setting field: {name}" : "Campo de configuração declarativa de {app}: {name}",
|
||||
|
||||
@@ -591,8 +591,11 @@
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php é registrado em um serviço webcron para chamar cron.php a cada 5 minutos por HTTP. Caso de uso: Instância muito pequena (1–5 contas dependendo do uso).",
|
||||
"Cron (Recommended)" : "Cron (Recomendado)",
|
||||
"Unable to update profile default setting" : "Não foi possível atualizar a configuração padrão do perfil",
|
||||
"Unable to update profile picker setting" : "Não é possível atualizar a configuração do seletor de perfis",
|
||||
"Profile" : "Perfil",
|
||||
"Enable or disable profile by default for new accounts." : "Ativar ou desativar o perfil por padrão para novas contas.",
|
||||
"Enable the profile picker" : "Ativar o seletor de perfis",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Ative ou desative o seletor de perfis no seletor inteligente e as pré-visualizações dos links dos perfis.",
|
||||
"Password confirmation is required" : "A confirmação da senha é necessária",
|
||||
"Failed to save setting" : "Falha ao salvar a configuração",
|
||||
"{app}'s declarative setting field: {name}" : "Campo de configuração declarativa de {app}: {name}",
|
||||
|
||||
@@ -330,6 +330,10 @@ OC.L10N.register(
|
||||
"Database transaction isolation level" : "Veri tabanı işlemsel yalıtım düzeyi",
|
||||
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "Veri tabanınız \"READ COMMITTED\" işlem yalıtma düzeyinde çalışmıyor. Bu durum aynı anda birden çok işlem yapıldığında sorun çıkmasına yol açabilir.",
|
||||
"Was not able to get transaction isolation level: %s" : "İşlemsel yalıtım düzeyi alınamadı: %s",
|
||||
"Second factor configuration" : "İkinci adım yapılandırması",
|
||||
"This instance has no second factor provider available." : "Bu kopyada kullanılabilecek bir ikinci adım hizmeti sağlayıcısı yok.",
|
||||
"Second factor providers are available but two-factor authentication is not enforced." : "Kullanılabilecek ikinci adım hizmeti sağlayıcıları var, ancak iki adımlı doğrulama zorunlu değil.",
|
||||
"Second factor providers are available and enforced: %s." : "Kullanılabilecek ikinci adım hizmeti sağlayıcıları var ve kullanılması zorunlu: %s.",
|
||||
".well-known URLs" : ".well-known adresler",
|
||||
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "Yapılandırmanızda `check_for_working_wellknown_setup` değerinin false olarak ayarlandığından emin olun. Böylece bu denetim atlanır.",
|
||||
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "Site sunucunuzun `.well.known` bilgisini doğru şekilde sunup sunmadığı denetlenemedi. Lütfen el ile denetleyin.",
|
||||
@@ -382,6 +386,8 @@ OC.L10N.register(
|
||||
"Shares with guessable tokens may be accessed easily" : "Öngörülebilir kodları olan paylaşımlara kolayca erişilebilir",
|
||||
"Limit sharing based on groups" : "Paylaşımlar gruplara göre sınırlansın",
|
||||
"Allow sharing for everyone (default)" : "Herkes ile paylaşım yapılabilsin (varsayılan)",
|
||||
"Exclude some groups" : "Bazı gruplar katılmasın",
|
||||
"Allow some groups" : "Bazı gruplar katılsın",
|
||||
"Groups allowed to share" : "Paylaşım yapılabilecek gruplar",
|
||||
"Groups excluded from sharing" : "Paylaşıma katılmayacak gruplar",
|
||||
"Not allowed groups will still be able to receive shares, but not to initiate them." : "İzin verilmeyen gruplar paylaşımları almayı sürdürebilir ancak paylaşım yapamaz.",
|
||||
@@ -436,9 +442,16 @@ OC.L10N.register(
|
||||
"This app is supported via your current Nextcloud subscription." : "Bu uygulamanın desteği geçerli Nextcloud aboneliğiniz ile sağlanır.",
|
||||
"Featured apps are developed by and within the community. They offer central functionality and are ready for production use." : "Öne çıkarılmış uygulamalar topluluk tarafından geliştirilmiştir. Temel işlevleri yerine getirirler ve üretim ortamında kullanılabilirler.",
|
||||
"Community rating: {score}/5" : "Topluluk değerlendirmesi: {score}/5",
|
||||
"Office suite switching is managed through the Nextcloud All-in-One interface." : "Ofis paketi değişikliği Nextcloud tümü bir arada arayüzünden yapılır.",
|
||||
"Please use the AIO interface to switch between office suites." : "Ofis paketi değişikliği yapmak için tümü bir arada arayüzünü kullanın.",
|
||||
"Select your preferred office suite. Please note that installing requires manual server setup." : "Kullanmak istediğiniz ofis paketini seçin. Kurulum için sunucuda el ile işlem yapılması gerektirdiğini unutmayın.",
|
||||
"installed" : "kurulmuş",
|
||||
"Learn more" : "Ayrıntılı bilgi alın",
|
||||
"Disable office suites" : "Ofis paketlerini kullanımdan kaldır",
|
||||
"Disable all" : "Tümünü kullanımdan kaldır",
|
||||
"Download and enable all" : "İndir ve tümünü kullanıma al",
|
||||
"All office suites disabled" : "Tüm ofis paketleri kullanımdan kaldırıldı",
|
||||
"{name} enabled" : "{name} kullanıma alındı",
|
||||
"All apps are up-to-date." : "Tüm uygulamalar güncel",
|
||||
"Icon" : "Simge",
|
||||
"Name" : "Ad",
|
||||
@@ -580,8 +593,11 @@ OC.L10N.register(
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php, HTTP üzerinden her 5 dakikada bir cron.php sayfasını çağıran bir internet zamanlanmış görevi hizmetinde kayıtlıdır. Kullanım şekli: Çok küçük kopya (kullanıma bağlı olarak 1–5 hesap).",
|
||||
"Cron (Recommended)" : "Cron (önerilen)",
|
||||
"Unable to update profile default setting" : "Profil varsayılan ayarı güncellenemedi",
|
||||
"Unable to update profile picker setting" : "Profil seçici ayarı güncellenemedi",
|
||||
"Profile" : "Profil",
|
||||
"Enable or disable profile by default for new accounts." : "Yeni hesaplar için profilleri varsayılan olarak kullanıma al ya da kaldır.",
|
||||
"Enable the profile picker" : "Profil seçiciyi aç",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Akıllı seçicide profil seçiciyi ve profil bağlantısı ön izlemelerini açar ya da kapatır.",
|
||||
"Password confirmation is required" : "Parola onayının yazılması zorunludur",
|
||||
"Failed to save setting" : "Ayar kaydedilemedi",
|
||||
"{app}'s declarative setting field: {name}" : "{app} uygulamasının bildirdiği ayar alanı: {name}",
|
||||
@@ -898,6 +914,17 @@ OC.L10N.register(
|
||||
"App bundles" : "Uygulama Paketleri",
|
||||
"Featured apps" : "Öne çıkarılmış uygulamalar",
|
||||
"Supported apps" : "Desteklenen uygulamalar",
|
||||
"Best Nextcloud integration" : "En iyi Nextcloud bütünleştirmesi",
|
||||
"Open source" : "Açık kaynaklı",
|
||||
"Good performance" : "İyi başarım",
|
||||
"Best security: documents never leave your server" : "En iyi güvenlik: Belgeler asla sunucunuzdan ayrılmaz",
|
||||
"Best ODF compatibility" : "En iyi ODF uyumluluğu",
|
||||
"Best support for legacy files" : "Eski dosyalar için en iyi destek",
|
||||
"Good Nextcloud integration" : "İyi Nextcloud bütünleştirmesi",
|
||||
"Open core" : "Açık çekirdekli",
|
||||
"Best performance" : "En iyi başarım",
|
||||
"Limited ODF compatibility" : "Sınırlı ODF uyumluluğu",
|
||||
"Best Microsoft compatibility" : "En iyi Microsoft uyumluluğu",
|
||||
"Show to everyone" : "Herkese görüntülensin",
|
||||
"Show to logged in accounts only" : "Yalnızca oturum açmış hesaplara görüntülensin",
|
||||
"Hide" : "Gizlensin",
|
||||
|
||||
@@ -328,6 +328,10 @@
|
||||
"Database transaction isolation level" : "Veri tabanı işlemsel yalıtım düzeyi",
|
||||
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "Veri tabanınız \"READ COMMITTED\" işlem yalıtma düzeyinde çalışmıyor. Bu durum aynı anda birden çok işlem yapıldığında sorun çıkmasına yol açabilir.",
|
||||
"Was not able to get transaction isolation level: %s" : "İşlemsel yalıtım düzeyi alınamadı: %s",
|
||||
"Second factor configuration" : "İkinci adım yapılandırması",
|
||||
"This instance has no second factor provider available." : "Bu kopyada kullanılabilecek bir ikinci adım hizmeti sağlayıcısı yok.",
|
||||
"Second factor providers are available but two-factor authentication is not enforced." : "Kullanılabilecek ikinci adım hizmeti sağlayıcıları var, ancak iki adımlı doğrulama zorunlu değil.",
|
||||
"Second factor providers are available and enforced: %s." : "Kullanılabilecek ikinci adım hizmeti sağlayıcıları var ve kullanılması zorunlu: %s.",
|
||||
".well-known URLs" : ".well-known adresler",
|
||||
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "Yapılandırmanızda `check_for_working_wellknown_setup` değerinin false olarak ayarlandığından emin olun. Böylece bu denetim atlanır.",
|
||||
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "Site sunucunuzun `.well.known` bilgisini doğru şekilde sunup sunmadığı denetlenemedi. Lütfen el ile denetleyin.",
|
||||
@@ -380,6 +384,8 @@
|
||||
"Shares with guessable tokens may be accessed easily" : "Öngörülebilir kodları olan paylaşımlara kolayca erişilebilir",
|
||||
"Limit sharing based on groups" : "Paylaşımlar gruplara göre sınırlansın",
|
||||
"Allow sharing for everyone (default)" : "Herkes ile paylaşım yapılabilsin (varsayılan)",
|
||||
"Exclude some groups" : "Bazı gruplar katılmasın",
|
||||
"Allow some groups" : "Bazı gruplar katılsın",
|
||||
"Groups allowed to share" : "Paylaşım yapılabilecek gruplar",
|
||||
"Groups excluded from sharing" : "Paylaşıma katılmayacak gruplar",
|
||||
"Not allowed groups will still be able to receive shares, but not to initiate them." : "İzin verilmeyen gruplar paylaşımları almayı sürdürebilir ancak paylaşım yapamaz.",
|
||||
@@ -434,9 +440,16 @@
|
||||
"This app is supported via your current Nextcloud subscription." : "Bu uygulamanın desteği geçerli Nextcloud aboneliğiniz ile sağlanır.",
|
||||
"Featured apps are developed by and within the community. They offer central functionality and are ready for production use." : "Öne çıkarılmış uygulamalar topluluk tarafından geliştirilmiştir. Temel işlevleri yerine getirirler ve üretim ortamında kullanılabilirler.",
|
||||
"Community rating: {score}/5" : "Topluluk değerlendirmesi: {score}/5",
|
||||
"Office suite switching is managed through the Nextcloud All-in-One interface." : "Ofis paketi değişikliği Nextcloud tümü bir arada arayüzünden yapılır.",
|
||||
"Please use the AIO interface to switch between office suites." : "Ofis paketi değişikliği yapmak için tümü bir arada arayüzünü kullanın.",
|
||||
"Select your preferred office suite. Please note that installing requires manual server setup." : "Kullanmak istediğiniz ofis paketini seçin. Kurulum için sunucuda el ile işlem yapılması gerektirdiğini unutmayın.",
|
||||
"installed" : "kurulmuş",
|
||||
"Learn more" : "Ayrıntılı bilgi alın",
|
||||
"Disable office suites" : "Ofis paketlerini kullanımdan kaldır",
|
||||
"Disable all" : "Tümünü kullanımdan kaldır",
|
||||
"Download and enable all" : "İndir ve tümünü kullanıma al",
|
||||
"All office suites disabled" : "Tüm ofis paketleri kullanımdan kaldırıldı",
|
||||
"{name} enabled" : "{name} kullanıma alındı",
|
||||
"All apps are up-to-date." : "Tüm uygulamalar güncel",
|
||||
"Icon" : "Simge",
|
||||
"Name" : "Ad",
|
||||
@@ -578,8 +591,11 @@
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php, HTTP üzerinden her 5 dakikada bir cron.php sayfasını çağıran bir internet zamanlanmış görevi hizmetinde kayıtlıdır. Kullanım şekli: Çok küçük kopya (kullanıma bağlı olarak 1–5 hesap).",
|
||||
"Cron (Recommended)" : "Cron (önerilen)",
|
||||
"Unable to update profile default setting" : "Profil varsayılan ayarı güncellenemedi",
|
||||
"Unable to update profile picker setting" : "Profil seçici ayarı güncellenemedi",
|
||||
"Profile" : "Profil",
|
||||
"Enable or disable profile by default for new accounts." : "Yeni hesaplar için profilleri varsayılan olarak kullanıma al ya da kaldır.",
|
||||
"Enable the profile picker" : "Profil seçiciyi aç",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Akıllı seçicide profil seçiciyi ve profil bağlantısı ön izlemelerini açar ya da kapatır.",
|
||||
"Password confirmation is required" : "Parola onayının yazılması zorunludur",
|
||||
"Failed to save setting" : "Ayar kaydedilemedi",
|
||||
"{app}'s declarative setting field: {name}" : "{app} uygulamasının bildirdiği ayar alanı: {name}",
|
||||
@@ -896,6 +912,17 @@
|
||||
"App bundles" : "Uygulama Paketleri",
|
||||
"Featured apps" : "Öne çıkarılmış uygulamalar",
|
||||
"Supported apps" : "Desteklenen uygulamalar",
|
||||
"Best Nextcloud integration" : "En iyi Nextcloud bütünleştirmesi",
|
||||
"Open source" : "Açık kaynaklı",
|
||||
"Good performance" : "İyi başarım",
|
||||
"Best security: documents never leave your server" : "En iyi güvenlik: Belgeler asla sunucunuzdan ayrılmaz",
|
||||
"Best ODF compatibility" : "En iyi ODF uyumluluğu",
|
||||
"Best support for legacy files" : "Eski dosyalar için en iyi destek",
|
||||
"Good Nextcloud integration" : "İyi Nextcloud bütünleştirmesi",
|
||||
"Open core" : "Açık çekirdekli",
|
||||
"Best performance" : "En iyi başarım",
|
||||
"Limited ODF compatibility" : "Sınırlı ODF uyumluluğu",
|
||||
"Best Microsoft compatibility" : "En iyi Microsoft uyumluluğu",
|
||||
"Show to everyone" : "Herkese görüntülensin",
|
||||
"Show to logged in accounts only" : "Yalnızca oturum açmış hesaplara görüntülensin",
|
||||
"Hide" : "Gizlensin",
|
||||
|
||||
+19
-16
@@ -369,18 +369,18 @@ OC.L10N.register(
|
||||
"Restrict users to only share with users in their groups" : "Дозволити надання у спільний доступ тільки в межах власних груп",
|
||||
"Ignore the following groups when checking group membership" : "Ігнорувати такі групи під час перевірки участи в групі",
|
||||
"Allow users to preview files even if download is disabled" : "Дозволити користувачам переглядати файли, навіть якщо завантаження вимкнено",
|
||||
"Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Користувачі все одно зможуть робити скріншоти або записувати екран. Це не забезпечує жодного остаточного захисту.",
|
||||
"Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Користувачі все одно можуть робити скріншоти або записувати екран. Це не забезпечить надійним захистом.",
|
||||
"Allow users to share via link and emails" : "Дозволити користувачам надання у спільний доступ за допомогою посилань та ел. листів",
|
||||
"Allow public uploads" : "Дозволити публічне завантаження",
|
||||
"Allow public shares to be added to other clouds by federation." : "Дозволити додавати публічні ресурси до інших хмар за допомогою федерації.",
|
||||
"Allow public shares to be added to other clouds by federation." : "Дозволити додавати публічні ресурси до інших хмар за допомогою функціоналу об'єднаних хмар.",
|
||||
"This will add share permissions to all newly created link shares." : "Це додасть дозволи на спільний доступ до всіх новостворених спільних ресурсів посилань.",
|
||||
"Always ask for a password" : "Завжди запитувати пароль",
|
||||
"Enforce password protection" : "Захист паролем обов'язковий",
|
||||
"Exclude groups from password requirements" : "Виключення щодо вимог пароля для груп",
|
||||
"Exclude groups from creating link shares" : "Не дозволяти користувачам таких груп створювати посилання спільного доступу",
|
||||
"Allow users to set custom share link tokens" : "Дозволити користвучам встановити власні токени спільних посилань",
|
||||
"Shares with custom tokens will continue to be accessible after this setting has been disabled" : "Частки з власними токенами залишатимуться доступними після вимкнення цього параметра",
|
||||
"Shares with guessable tokens may be accessed easily" : "До спільних ресурсів з токенами, які можна вгадати, можна легко отримати доступ.",
|
||||
"Allow users to set custom share link tokens" : "Дозволити користувачам встановлювати власні токени для спільного доступу",
|
||||
"Shares with custom tokens will continue to be accessible after this setting has been disabled" : "Спільний доступ з токенами користувачів залишатиметься активним після вимкнення цього параметра",
|
||||
"Shares with guessable tokens may be accessed easily" : "До спільних ресурсів з токенами, які можна спробувати вгадати, інші можуть легко отримати доступ.",
|
||||
"Limit sharing based on groups" : "Обмежити надання у спільний доступ на основі груп",
|
||||
"Allow sharing for everyone (default)" : "Дозволити надання у спільний доступ для всіх (типово)",
|
||||
"Exclude some groups" : "Вилучити окремі групи",
|
||||
@@ -400,16 +400,16 @@ OC.L10N.register(
|
||||
"Enforce expiration date for link or mail shares" : "Застосовувати термін дії для посилань або спільного доступу до пошти",
|
||||
"Default expiration time of shares in days" : "Типовий термін дії спільних ресурсів у днях",
|
||||
"Privacy settings for sharing" : "Налаштування конфіденційності для спільного доступу",
|
||||
"Allow account name autocompletion in share dialog and allow access to the system address book" : "Дозволити автозаповнення імени користувача та доступ до системної адресної книги",
|
||||
"Sharing autocompletion restrictions" : "Обмеження автозавершення для спільного доступу",
|
||||
"If autocompletion restrictions for both \"same group\" and \"phonebook integration\" are enabled, a match in either is enough to show the user." : "Якщо задіяно автозавершення як для \"тієї саме групи\" та \"інтеграції з адресною книгою\", достатньо одного збігу, щоби показати ім'я користувача.",
|
||||
"Restrict account name autocompletion and system address book access to users within the same groups" : "Обмежити автозаповнення імени користувача та доступу до системної адресної книги тільки користувачам однієї й тої саме групи",
|
||||
"Restrict account name autocompletion to users based on their phonebook" : "Обмежити автозавершення імени користувача на основі адресних книг користувачів",
|
||||
"Allow autocompletion to full match when entering the full name (ignoring restrictions like group membership or missing phonebook match)" : "Дозволити автозавершення для повного збігу при введенні повного імени (не враховуватимуться такі обмеження, як участь в групах або незбіг з адресною книгою)",
|
||||
"Full match autocompletion restrictions" : "Обмеження повного збігу для автозавершення",
|
||||
"Also allow autocompletion on full match of the user ID" : "Також дозволити автозавершення при повному збігу ідентифікатора користувача",
|
||||
"Also allow autocompletion on full match of the display name" : "Також дозволити автозаповнення при повному збігу імени для показу",
|
||||
"Also allow autocompletion on full match of the user email" : "Також дозволити автозавершення при повному збігу ел. адреси користувача",
|
||||
"Allow account name autocompletion in share dialog and allow access to the system address book" : "Дозволити автоматичне заповнення імени користувача та доступ до системної адресної книги",
|
||||
"Sharing autocompletion restrictions" : "Дозволи автоматичного заповнення для спільного доступу",
|
||||
"If autocompletion restrictions for both \"same group\" and \"phonebook integration\" are enabled, a match in either is enough to show the user." : "Якщо уімкнено автоматичне заповнення \"в межах власної групи\" та \"доступ за адресною книгою\", достатньо, щоби хоч раз було виконано умови, щоб показати ім'я користувача.",
|
||||
"Restrict account name autocompletion and system address book access to users within the same groups" : "Дозволити автоматичне заповнення імени користувача, доступ до системної адресної книги користувачам тільки в межах власних груп",
|
||||
"Restrict account name autocompletion to users based on their phonebook" : "Дозволити автоматичне заповнення імени користувача тільки з власних адресних книг користувачів",
|
||||
"Allow autocompletion to full match when entering the full name (ignoring restrictions like group membership or missing phonebook match)" : "Дозволити автоматичне заповнення, якщо введено ім'я користувача повністю (при цьому не враховуватимуться такі дозволи, як участь в групах або відсутність у адресній книзі)",
|
||||
"Full match autocompletion restrictions" : "Дозволи для автоматичного заповнення, якщо дані введено повністю",
|
||||
"Also allow autocompletion on full match of the user ID" : "Дозволити автоматичне заповнення, якщо введено повний ідентифікатор користувача",
|
||||
"Also allow autocompletion on full match of the display name" : "Дзволити автоматичне заповнення, якщо введено ім'я користувача",
|
||||
"Also allow autocompletion on full match of the user email" : "Дозволити автоматичне заповнення, якщо введено ел. адресу користувача",
|
||||
"Do not use second user displayname for full match" : "Не використовувати друге ім'я для показу для повного збігу",
|
||||
"Show disclaimer text on the public link upload page (only shown when the file list is hidden)" : "Показувати текст застереження на сторінці завантаження публічного посилання (відображається, лише якщо список файлів приховано)",
|
||||
"Disclaimer text" : "Текст відмови від відповідальності",
|
||||
@@ -590,8 +590,11 @@ OC.L10N.register(
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php зареєстровано на сервісі webcron, щоб викликати cron.php кожні 5 хвилин по HTTP. Варіант використання: Дуже маленький екземпляр (1-5 акаунтів залежно від використання).",
|
||||
"Cron (Recommended)" : "Cron (рекомендовано)",
|
||||
"Unable to update profile default setting" : "Не вдалося оновити стандартні налаштування профілю",
|
||||
"Unable to update profile picker setting" : "Не вдалося оновити налаштування вибору профілю",
|
||||
"Profile" : "Профіль",
|
||||
"Enable or disable profile by default for new accounts." : "Увімкнути або вимкнути стандартний профіль для нових акаунтів.",
|
||||
"Enable the profile picker" : "Увімкнути вибір профілю",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Увімкнути або вимкнути вибір профілю для Асистента з вибору та попереднього перегляду посилання на профіль.",
|
||||
"Password confirmation is required" : "Необхідне підтвердження паролем",
|
||||
"Failed to save setting" : "Не вдалося зберегти налаштування",
|
||||
"{app}'s declarative setting field: {name}" : "Декларативне поле налаштувань {app}: {name}",
|
||||
@@ -978,7 +981,7 @@ OC.L10N.register(
|
||||
"Unable to retrieve the group list" : "Неможливо отримати список груп",
|
||||
"Exclude some groups from sharing" : "Не дозволяти таким групам надавати у спільний доступ",
|
||||
"Limit sharing to some groups" : "Дозволити надання у спільний доступ тільки для таких груп",
|
||||
"Also allow autocompletion on full match of the user id" : "Також дозволити автозавершення при повному збігу ідентифікатора користувача",
|
||||
"Also allow autocompletion on full match of the user id" : "Дозволити автоматичне заповнення, якщо введено повністю ідентифікатор користувача",
|
||||
"Loading accounts …" : "Завантаження облікових записів ...",
|
||||
"Set account as admin for …" : "Встановити адміністратором для ...",
|
||||
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} обліковий запис …","{userCount} облікові записи …","{userCount} облікових записів …","{userCount} облікових записів …"],
|
||||
|
||||
+19
-16
@@ -367,18 +367,18 @@
|
||||
"Restrict users to only share with users in their groups" : "Дозволити надання у спільний доступ тільки в межах власних груп",
|
||||
"Ignore the following groups when checking group membership" : "Ігнорувати такі групи під час перевірки участи в групі",
|
||||
"Allow users to preview files even if download is disabled" : "Дозволити користувачам переглядати файли, навіть якщо завантаження вимкнено",
|
||||
"Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Користувачі все одно зможуть робити скріншоти або записувати екран. Це не забезпечує жодного остаточного захисту.",
|
||||
"Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Користувачі все одно можуть робити скріншоти або записувати екран. Це не забезпечить надійним захистом.",
|
||||
"Allow users to share via link and emails" : "Дозволити користувачам надання у спільний доступ за допомогою посилань та ел. листів",
|
||||
"Allow public uploads" : "Дозволити публічне завантаження",
|
||||
"Allow public shares to be added to other clouds by federation." : "Дозволити додавати публічні ресурси до інших хмар за допомогою федерації.",
|
||||
"Allow public shares to be added to other clouds by federation." : "Дозволити додавати публічні ресурси до інших хмар за допомогою функціоналу об'єднаних хмар.",
|
||||
"This will add share permissions to all newly created link shares." : "Це додасть дозволи на спільний доступ до всіх новостворених спільних ресурсів посилань.",
|
||||
"Always ask for a password" : "Завжди запитувати пароль",
|
||||
"Enforce password protection" : "Захист паролем обов'язковий",
|
||||
"Exclude groups from password requirements" : "Виключення щодо вимог пароля для груп",
|
||||
"Exclude groups from creating link shares" : "Не дозволяти користувачам таких груп створювати посилання спільного доступу",
|
||||
"Allow users to set custom share link tokens" : "Дозволити користвучам встановити власні токени спільних посилань",
|
||||
"Shares with custom tokens will continue to be accessible after this setting has been disabled" : "Частки з власними токенами залишатимуться доступними після вимкнення цього параметра",
|
||||
"Shares with guessable tokens may be accessed easily" : "До спільних ресурсів з токенами, які можна вгадати, можна легко отримати доступ.",
|
||||
"Allow users to set custom share link tokens" : "Дозволити користувачам встановлювати власні токени для спільного доступу",
|
||||
"Shares with custom tokens will continue to be accessible after this setting has been disabled" : "Спільний доступ з токенами користувачів залишатиметься активним після вимкнення цього параметра",
|
||||
"Shares with guessable tokens may be accessed easily" : "До спільних ресурсів з токенами, які можна спробувати вгадати, інші можуть легко отримати доступ.",
|
||||
"Limit sharing based on groups" : "Обмежити надання у спільний доступ на основі груп",
|
||||
"Allow sharing for everyone (default)" : "Дозволити надання у спільний доступ для всіх (типово)",
|
||||
"Exclude some groups" : "Вилучити окремі групи",
|
||||
@@ -398,16 +398,16 @@
|
||||
"Enforce expiration date for link or mail shares" : "Застосовувати термін дії для посилань або спільного доступу до пошти",
|
||||
"Default expiration time of shares in days" : "Типовий термін дії спільних ресурсів у днях",
|
||||
"Privacy settings for sharing" : "Налаштування конфіденційності для спільного доступу",
|
||||
"Allow account name autocompletion in share dialog and allow access to the system address book" : "Дозволити автозаповнення імени користувача та доступ до системної адресної книги",
|
||||
"Sharing autocompletion restrictions" : "Обмеження автозавершення для спільного доступу",
|
||||
"If autocompletion restrictions for both \"same group\" and \"phonebook integration\" are enabled, a match in either is enough to show the user." : "Якщо задіяно автозавершення як для \"тієї саме групи\" та \"інтеграції з адресною книгою\", достатньо одного збігу, щоби показати ім'я користувача.",
|
||||
"Restrict account name autocompletion and system address book access to users within the same groups" : "Обмежити автозаповнення імени користувача та доступу до системної адресної книги тільки користувачам однієї й тої саме групи",
|
||||
"Restrict account name autocompletion to users based on their phonebook" : "Обмежити автозавершення імени користувача на основі адресних книг користувачів",
|
||||
"Allow autocompletion to full match when entering the full name (ignoring restrictions like group membership or missing phonebook match)" : "Дозволити автозавершення для повного збігу при введенні повного імени (не враховуватимуться такі обмеження, як участь в групах або незбіг з адресною книгою)",
|
||||
"Full match autocompletion restrictions" : "Обмеження повного збігу для автозавершення",
|
||||
"Also allow autocompletion on full match of the user ID" : "Також дозволити автозавершення при повному збігу ідентифікатора користувача",
|
||||
"Also allow autocompletion on full match of the display name" : "Також дозволити автозаповнення при повному збігу імени для показу",
|
||||
"Also allow autocompletion on full match of the user email" : "Також дозволити автозавершення при повному збігу ел. адреси користувача",
|
||||
"Allow account name autocompletion in share dialog and allow access to the system address book" : "Дозволити автоматичне заповнення імени користувача та доступ до системної адресної книги",
|
||||
"Sharing autocompletion restrictions" : "Дозволи автоматичного заповнення для спільного доступу",
|
||||
"If autocompletion restrictions for both \"same group\" and \"phonebook integration\" are enabled, a match in either is enough to show the user." : "Якщо уімкнено автоматичне заповнення \"в межах власної групи\" та \"доступ за адресною книгою\", достатньо, щоби хоч раз було виконано умови, щоб показати ім'я користувача.",
|
||||
"Restrict account name autocompletion and system address book access to users within the same groups" : "Дозволити автоматичне заповнення імени користувача, доступ до системної адресної книги користувачам тільки в межах власних груп",
|
||||
"Restrict account name autocompletion to users based on their phonebook" : "Дозволити автоматичне заповнення імени користувача тільки з власних адресних книг користувачів",
|
||||
"Allow autocompletion to full match when entering the full name (ignoring restrictions like group membership or missing phonebook match)" : "Дозволити автоматичне заповнення, якщо введено ім'я користувача повністю (при цьому не враховуватимуться такі дозволи, як участь в групах або відсутність у адресній книзі)",
|
||||
"Full match autocompletion restrictions" : "Дозволи для автоматичного заповнення, якщо дані введено повністю",
|
||||
"Also allow autocompletion on full match of the user ID" : "Дозволити автоматичне заповнення, якщо введено повний ідентифікатор користувача",
|
||||
"Also allow autocompletion on full match of the display name" : "Дзволити автоматичне заповнення, якщо введено ім'я користувача",
|
||||
"Also allow autocompletion on full match of the user email" : "Дозволити автоматичне заповнення, якщо введено ел. адресу користувача",
|
||||
"Do not use second user displayname for full match" : "Не використовувати друге ім'я для показу для повного збігу",
|
||||
"Show disclaimer text on the public link upload page (only shown when the file list is hidden)" : "Показувати текст застереження на сторінці завантаження публічного посилання (відображається, лише якщо список файлів приховано)",
|
||||
"Disclaimer text" : "Текст відмови від відповідальності",
|
||||
@@ -588,8 +588,11 @@
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php зареєстровано на сервісі webcron, щоб викликати cron.php кожні 5 хвилин по HTTP. Варіант використання: Дуже маленький екземпляр (1-5 акаунтів залежно від використання).",
|
||||
"Cron (Recommended)" : "Cron (рекомендовано)",
|
||||
"Unable to update profile default setting" : "Не вдалося оновити стандартні налаштування профілю",
|
||||
"Unable to update profile picker setting" : "Не вдалося оновити налаштування вибору профілю",
|
||||
"Profile" : "Профіль",
|
||||
"Enable or disable profile by default for new accounts." : "Увімкнути або вимкнути стандартний профіль для нових акаунтів.",
|
||||
"Enable the profile picker" : "Увімкнути вибір профілю",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "Увімкнути або вимкнути вибір профілю для Асистента з вибору та попереднього перегляду посилання на профіль.",
|
||||
"Password confirmation is required" : "Необхідне підтвердження паролем",
|
||||
"Failed to save setting" : "Не вдалося зберегти налаштування",
|
||||
"{app}'s declarative setting field: {name}" : "Декларативне поле налаштувань {app}: {name}",
|
||||
@@ -976,7 +979,7 @@
|
||||
"Unable to retrieve the group list" : "Неможливо отримати список груп",
|
||||
"Exclude some groups from sharing" : "Не дозволяти таким групам надавати у спільний доступ",
|
||||
"Limit sharing to some groups" : "Дозволити надання у спільний доступ тільки для таких груп",
|
||||
"Also allow autocompletion on full match of the user id" : "Також дозволити автозавершення при повному збігу ідентифікатора користувача",
|
||||
"Also allow autocompletion on full match of the user id" : "Дозволити автоматичне заповнення, якщо введено повністю ідентифікатор користувача",
|
||||
"Loading accounts …" : "Завантаження облікових записів ...",
|
||||
"Set account as admin for …" : "Встановити адміністратором для ...",
|
||||
"_{userCount} account …_::_{userCount} accounts …_" : ["{userCount} обліковий запис …","{userCount} облікові записи …","{userCount} облікових записів …","{userCount} облікових записів …"],
|
||||
|
||||
@@ -593,8 +593,11 @@ OC.L10N.register(
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php 在 webcron 服務中註冊,每五分鐘透過 HTTP 呼叫一次 cron.php。使用情境:非常小的站台(一到五個帳號,取決於使用量)。",
|
||||
"Cron (Recommended)" : "Cron(建議)",
|
||||
"Unable to update profile default setting" : "無法更新個人檔案預設設定",
|
||||
"Unable to update profile picker setting" : "無法更新個人檔案挑選程式設定",
|
||||
"Profile" : "個人檔案",
|
||||
"Enable or disable profile by default for new accounts." : "預設情況下為新帳號啟用或停用個人檔案",
|
||||
"Enable the profile picker" : "啟用個人檔案挑選程式",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "在智慧型挑選程式與個人檔案連結預覽中啟用或停用個人檔案挑選程式。",
|
||||
"Password confirmation is required" : "需要密碼確認",
|
||||
"Failed to save setting" : "儲存設定失敗",
|
||||
"{app}'s declarative setting field: {name}" : "{app} 的聲明性設定欄位:{name}",
|
||||
|
||||
@@ -591,8 +591,11 @@
|
||||
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (1–5 accounts depending on the usage)." : "cron.php 在 webcron 服務中註冊,每五分鐘透過 HTTP 呼叫一次 cron.php。使用情境:非常小的站台(一到五個帳號,取決於使用量)。",
|
||||
"Cron (Recommended)" : "Cron(建議)",
|
||||
"Unable to update profile default setting" : "無法更新個人檔案預設設定",
|
||||
"Unable to update profile picker setting" : "無法更新個人檔案挑選程式設定",
|
||||
"Profile" : "個人檔案",
|
||||
"Enable or disable profile by default for new accounts." : "預設情況下為新帳號啟用或停用個人檔案",
|
||||
"Enable the profile picker" : "啟用個人檔案挑選程式",
|
||||
"Enable or disable the profile picker in the Smart Picker and the profile link previews." : "在智慧型挑選程式與個人檔案連結預覽中啟用或停用個人檔案挑選程式。",
|
||||
"Password confirmation is required" : "需要密碼確認",
|
||||
"Failed to save setting" : "儲存設定失敗",
|
||||
"{app}'s declarative setting field: {name}" : "{app} 的聲明性設定欄位:{name}",
|
||||
|
||||
@@ -41,6 +41,6 @@ OC.L10N.register(
|
||||
"Unable to update share by mail config" : "Не вдається оновити конфігурацію спільного доступу за допомогою пошти",
|
||||
"Allows people to share a personalized link to a file or folder by putting in an email address." : "Дозволяє користувачам надавати персоналізоване посилання на файл або каталог шляхом додавання адреси ел. пошти.",
|
||||
"Send password by mail" : "Надіслати пароль поштою",
|
||||
"Reply to initiator" : "Відповідь ініціатору"
|
||||
"Reply to initiator" : "Відповісти ініціатору"
|
||||
},
|
||||
"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);");
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"Unable to update share by mail config" : "Не вдається оновити конфігурацію спільного доступу за допомогою пошти",
|
||||
"Allows people to share a personalized link to a file or folder by putting in an email address." : "Дозволяє користувачам надавати персоналізоване посилання на файл або каталог шляхом додавання адреси ел. пошти.",
|
||||
"Send password by mail" : "Надіслати пароль поштою",
|
||||
"Reply to initiator" : "Відповідь ініціатору"
|
||||
"Reply to initiator" : "Відповісти ініціатору"
|
||||
},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"
|
||||
}
|
||||
@@ -109,6 +109,7 @@ OC.L10N.register(
|
||||
"Reset primary color" : "Birincil rengi sıfırla",
|
||||
"Reset to default" : "Varsayılanlara dön",
|
||||
"Non image file selected" : "Seçilen dosya bir görsel değil",
|
||||
"Failed to upload image" : "Görsel yüklenemedi",
|
||||
"Preview of the selected image" : "Seçilmiş görselin ön izlemesi",
|
||||
"Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "Uygulamalarımızı herkesin kullanabilmesini çok önemsiyoruz. internet sitesi standartlarını izleyerek, işlemlerin fare olmadan da yapılabilmesini ve ekran okuyucular gibi yardımcı yazılımların kullanılabilmesini sağlıyoruz. AAA düzeyinde yüksek renk karşıtlığı teması ile AA düzeyinde {linkstart}İnternet Sitesi İçeriği Erişilebilirlik Kuralları{linkend} 2.1 ile uyumlu olmayı amaçladık.",
|
||||
"If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Bir sorunla karşılaşırsanız, bunları {issuetracker}sorun izleyicimiz{linkend} üzerinden bildirmekten çekinmeyin. Katkıda bulunmak istiyorsanız {designteam}tasarım ekibimize{linkend} katılın!",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"Reset primary color" : "Birincil rengi sıfırla",
|
||||
"Reset to default" : "Varsayılanlara dön",
|
||||
"Non image file selected" : "Seçilen dosya bir görsel değil",
|
||||
"Failed to upload image" : "Görsel yüklenemedi",
|
||||
"Preview of the selected image" : "Seçilmiş görselin ön izlemesi",
|
||||
"Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "Uygulamalarımızı herkesin kullanabilmesini çok önemsiyoruz. internet sitesi standartlarını izleyerek, işlemlerin fare olmadan da yapılabilmesini ve ekran okuyucular gibi yardımcı yazılımların kullanılabilmesini sağlıyoruz. AAA düzeyinde yüksek renk karşıtlığı teması ile AA düzeyinde {linkstart}İnternet Sitesi İçeriği Erişilebilirlik Kuralları{linkend} 2.1 ile uyumlu olmayı amaçladık.",
|
||||
"If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Bir sorunla karşılaşırsanız, bunları {issuetracker}sorun izleyicimiz{linkend} üzerinden bildirmekten çekinmeyin. Katkıda bulunmak istiyorsanız {designteam}tasarım ekibimize{linkend} katılın!",
|
||||
|
||||
Generated
+21
-21
@@ -6730,9 +6730,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/assert": {
|
||||
@@ -7084,9 +7084,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
||||
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
|
||||
"integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
@@ -8114,9 +8114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-hash": {
|
||||
@@ -8690,9 +8690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diffie-hellman/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
@@ -8889,9 +8889,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/elliptic/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-mart-vue-fast": {
|
||||
@@ -13028,9 +13028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/miller-rabin/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime": {
|
||||
@@ -14750,9 +14750,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/public-encrypt/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
|
||||
@@ -21,6 +21,38 @@ $qb->selectColumnsDistinct('f');
|
||||
$qb->selectAlias('g', 'h');
|
||||
$qb->selectAlias($qb->func()->lower('i'), 'j');
|
||||
|
||||
$qb
|
||||
->setParameter('k', 'l')
|
||||
->setParameters([])
|
||||
->setFirstResult(0)
|
||||
->setMaxResults(0)
|
||||
->delete()
|
||||
->update()
|
||||
->insert()
|
||||
->from('m')
|
||||
->join('n', 'o', 'p')
|
||||
->innerJoin('q', 'r', 's')
|
||||
->leftJoin('t', 'u', 'v')
|
||||
->rightJoin('w', 'x', 'y')
|
||||
->set('z', '1')
|
||||
->where()
|
||||
->andWhere()
|
||||
->orWhere()
|
||||
->groupBy()
|
||||
->addGroupBy()
|
||||
->setValue('2', '3')
|
||||
->values([])
|
||||
->having()
|
||||
->andHaving()
|
||||
->orHaving()
|
||||
->orderBy('4')
|
||||
->addOrderBy('5')
|
||||
->resetQueryParts()
|
||||
->resetQueryPart('6')
|
||||
->hintShardKey('7', '8')
|
||||
->runAcrossAllShards()
|
||||
->forUpdate();
|
||||
|
||||
/** @psalm-check-type-exact $result = \OCP\DB\IResult<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j'> */
|
||||
$result = $qb->executeQuery();
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class StatusCommand extends Command implements CompletionAwareInterface {
|
||||
|
||||
$numExecutedUnavailableMigrations = count($executedUnavailableMigrations);
|
||||
$numNewMigrations = count(array_diff(array_keys($availableMigrations), $executedMigrations));
|
||||
$pending = $ms->describeMigrationStep('lastest');
|
||||
$pending = $ms->describeMigrationStep();
|
||||
|
||||
$infos = [
|
||||
'App' => $ms->getApp(),
|
||||
|
||||
@@ -43,6 +43,12 @@ class ResetPassword extends Base {
|
||||
InputOption::VALUE_NONE,
|
||||
'read password from environment variable NC_PASS/OC_PASS'
|
||||
)
|
||||
->addOption(
|
||||
'no-password',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Sets the password to blank'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -76,22 +82,32 @@ class ResetPassword extends Base {
|
||||
}
|
||||
}
|
||||
|
||||
$question = new Question('Enter a new password: ');
|
||||
$question->setHidden(true);
|
||||
$password = $helper->ask($input, $output, $question);
|
||||
if ($input->getOption('no-password')) {
|
||||
$question = new ConfirmationQuestion('Are you sure you want to clear the password for ' . $username . '?');
|
||||
|
||||
if ($password === null) {
|
||||
$output->writeln('<error>Password cannot be empty!</error>');
|
||||
return 1;
|
||||
}
|
||||
if (!$helper->ask($input, $output, $question)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$question = new Question('Confirm the new password: ');
|
||||
$question->setHidden(true);
|
||||
$confirm = $helper->ask($input, $output, $question);
|
||||
$password = '';
|
||||
} else {
|
||||
$question = new Question('Enter a new password: ');
|
||||
$question->setHidden(true);
|
||||
$password = $helper->ask($input, $output, $question);
|
||||
|
||||
if ($password !== $confirm) {
|
||||
$output->writeln('<error>Passwords did not match!</error>');
|
||||
return 1;
|
||||
if ($password === null) {
|
||||
$output->writeln('<error>Password cannot be empty!</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$question = new Question('Confirm the new password: ');
|
||||
$question->setHidden(true);
|
||||
$confirm = $helper->ask($input, $output, $question);
|
||||
|
||||
if ($password !== $confirm) {
|
||||
$output->writeln('<error>Passwords did not match!</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$output->writeln('<error>Interactive input or --password-from-env is needed for entering a new password!</error>');
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+4
-3
@@ -766,9 +766,10 @@ class OC {
|
||||
self::checkConfig();
|
||||
self::checkInstalled($systemConfig);
|
||||
|
||||
self::addSecurityHeaders();
|
||||
|
||||
self::performSameSiteCookieProtection($config);
|
||||
if (!self::$CLI) {
|
||||
self::addSecurityHeaders();
|
||||
self::performSameSiteCookieProtection($config);
|
||||
}
|
||||
|
||||
if (!defined('OC_CONSOLE')) {
|
||||
$eventLogger->start('check_server', 'Run a few configuration checks');
|
||||
|
||||
@@ -20,6 +20,7 @@ use OCP\Files\NotFoundException;
|
||||
class Manager implements IMountManager {
|
||||
/** @var array<string, IMountPoint> */
|
||||
private array $mounts = [];
|
||||
private array $mountsByProvider = [];
|
||||
private bool $areMountsSorted = false;
|
||||
/** @var list<string>|null $mountKeys */
|
||||
private ?array $mountKeys = null;
|
||||
@@ -36,7 +37,11 @@ class Manager implements IMountManager {
|
||||
}
|
||||
|
||||
public function addMount(IMountPoint $mount): void {
|
||||
$this->mounts[$mount->getMountPoint()] = $mount;
|
||||
$mountPoint = $mount->getMountPoint();
|
||||
$mountProvider = $mount->getMountProvider();
|
||||
$this->mounts[$mountPoint] = $mount;
|
||||
$this->mountsByProvider[$mountProvider] ??= [];
|
||||
$this->mountsByProvider[$mountProvider][$mountPoint] = $mount;
|
||||
$this->pathCache->clear();
|
||||
$this->inPathCache->clear();
|
||||
$this->areMountsSorted = false;
|
||||
@@ -167,6 +172,7 @@ class Manager implements IMountManager {
|
||||
|
||||
public function clear(): void {
|
||||
$this->mounts = [];
|
||||
$this->mountsByProvider = [];
|
||||
$this->pathCache->clear();
|
||||
$this->inPathCache->clear();
|
||||
}
|
||||
@@ -236,15 +242,18 @@ class Manager implements IMountManager {
|
||||
* @param string[] $mountProviders
|
||||
* @return array<string, IMountPoint>
|
||||
*/
|
||||
public function getMountsByMountProvider(string $path, array $mountProviders) {
|
||||
public function getMountsByMountProvider(string $path, array $mountProviders): array {
|
||||
$this->getSetupManager()->setupForProvider($path, $mountProviders);
|
||||
if (in_array('', $mountProviders)) {
|
||||
if (\in_array('', $mountProviders)) {
|
||||
return $this->mounts;
|
||||
} else {
|
||||
return array_filter($this->mounts, function ($mount) use ($mountProviders) {
|
||||
return in_array($mount->getMountProvider(), $mountProviders);
|
||||
});
|
||||
}
|
||||
|
||||
$mounts = [];
|
||||
foreach ($mountProviders as $mountProvider) {
|
||||
$mounts[] = $this->mountsByProvider[$mountProvider] ?? [];
|
||||
}
|
||||
|
||||
return array_merge(...$mounts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -145,7 +145,7 @@ class Factory implements IFactory {
|
||||
if ($lang === null) {
|
||||
return null;
|
||||
}
|
||||
$lang = preg_replace('/[^a-zA-Z0-9.;,=-]/', '', $lang);
|
||||
$lang = preg_replace('/[^a-zA-Z0-9.;,=_-]/', '', $lang);
|
||||
return str_replace('..', '', $lang);
|
||||
}
|
||||
|
||||
|
||||
+10
-13
@@ -269,20 +269,9 @@ class Log implements ILogger, IDataLogger {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($logCondition['matches'])) {
|
||||
$configLogLevel = $this->config->getValue('loglevel', ILogger::WARN);
|
||||
if (is_numeric($configLogLevel)) {
|
||||
$this->nestingLevel--;
|
||||
return min((int)$configLogLevel, ILogger::FATAL);
|
||||
}
|
||||
$logConditionMatches = $logCondition['matches'] ?? [];
|
||||
|
||||
// Invalid configuration, warn the user and fall back to default level of WARN
|
||||
error_log('Nextcloud configuration: "loglevel" is not a valid integer');
|
||||
$this->nestingLevel--;
|
||||
return ILogger::WARN;
|
||||
}
|
||||
|
||||
foreach ($logCondition['matches'] as $option) {
|
||||
foreach ($logConditionMatches as $option) {
|
||||
if (
|
||||
(!isset($option['shared_secret']) || $this->checkLogSecret($option['shared_secret']))
|
||||
&& (!isset($option['users']) || in_array($userId, $option['users'], true))
|
||||
@@ -300,6 +289,14 @@ class Log implements ILogger, IDataLogger {
|
||||
}
|
||||
}
|
||||
|
||||
$configLogLevel = $this->config->getValue('loglevel', ILogger::WARN);
|
||||
if (is_numeric($configLogLevel)) {
|
||||
$this->nestingLevel--;
|
||||
return min((int)$configLogLevel, ILogger::FATAL);
|
||||
}
|
||||
|
||||
// Invalid configuration, warn the user and fall back to default level of WARN
|
||||
error_log('Nextcloud configuration: "loglevel" is not a valid integer');
|
||||
$this->nestingLevel--;
|
||||
return ILogger::WARN;
|
||||
}
|
||||
|
||||
@@ -199,14 +199,19 @@ class Repair implements IOutput {
|
||||
* @return list<IRepairStep>
|
||||
*/
|
||||
public static function getExpensiveRepairSteps(): array {
|
||||
return [
|
||||
$expensiveSteps = [
|
||||
Server::get(OldGroupMembershipShares::class),
|
||||
Server::get(RemoveBrokenProperties::class),
|
||||
Server::get(RepairMimeTypes::class),
|
||||
Server::get(DeleteSchedulingObjects::class),
|
||||
Server::get(RemoveObjectProperties::class),
|
||||
Server::get(CleanupShareTarget::class),
|
||||
];
|
||||
|
||||
if (class_exists(CleanupShareTarget::class)) {
|
||||
$expensiveSteps[] = Server::get(CleanupShareTarget::class);
|
||||
}
|
||||
|
||||
return $expensiveSteps;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -12,28 +14,27 @@ use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShareHelper;
|
||||
use Override;
|
||||
|
||||
class ShareHelper implements IShareHelper {
|
||||
public function __construct(
|
||||
private IManager $shareManager,
|
||||
private readonly IManager $shareManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return array [ users => [Mapping $uid => $pathForUser], remotes => [Mapping $cloudId => $pathToMountRoot]]
|
||||
*/
|
||||
public function getPathsForAccessList(Node $node) {
|
||||
#[Override]
|
||||
public function getPathsForAccessList(Node $node): array {
|
||||
$result = [
|
||||
'users' => [],
|
||||
'remotes' => [],
|
||||
];
|
||||
|
||||
$accessList = $this->shareManager->getAccessList($node, true, true);
|
||||
if (!empty($accessList['users'])) {
|
||||
if (isset($accessList['users']) && $accessList['users'] !== []) {
|
||||
$result['users'] = $this->getPathsForUsers($node, $accessList['users']);
|
||||
}
|
||||
if (!empty($accessList['remote'])) {
|
||||
|
||||
if (isset($accessList['remote']) && $accessList['remote'] !== []) {
|
||||
$result['remotes'] = $this->getPathsForRemotes($node, $accessList['remote']);
|
||||
}
|
||||
|
||||
@@ -60,20 +61,20 @@ class ShareHelper implements IShareHelper {
|
||||
* 'test3' => '/cat',
|
||||
* ],
|
||||
*
|
||||
* @param Node $node
|
||||
* @param array[] $users
|
||||
* @return array
|
||||
* @param non-empty-array<string, array{node_id: int, node_path: string}> $users
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function getPathsForUsers(Node $node, array $users) {
|
||||
/** @var array[] $byId */
|
||||
protected function getPathsForUsers(Node $node, array $users): array {
|
||||
/** @var array<int, array<string, string>> $byId */
|
||||
$byId = [];
|
||||
/** @var array[] $results */
|
||||
/** @var array<string, string> $results */
|
||||
$results = [];
|
||||
|
||||
foreach ($users as $uid => $info) {
|
||||
if (!isset($byId[$info['node_id']])) {
|
||||
$byId[$info['node_id']] = [];
|
||||
}
|
||||
|
||||
$byId[$info['node_id']][$uid] = $info['node_path'];
|
||||
}
|
||||
|
||||
@@ -82,15 +83,14 @@ class ShareHelper implements IShareHelper {
|
||||
foreach ($byId[$node->getId()] as $uid => $path) {
|
||||
$results[$uid] = $path;
|
||||
}
|
||||
|
||||
unset($byId[$node->getId()]);
|
||||
}
|
||||
} catch (NotFoundException $e) {
|
||||
return $results;
|
||||
} catch (InvalidPathException $e) {
|
||||
} catch (NotFoundException|InvalidPathException) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
if (empty($byId)) {
|
||||
if ($byId === []) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
@@ -98,22 +98,18 @@ class ShareHelper implements IShareHelper {
|
||||
$appendix = '/' . $node->getName();
|
||||
while (!empty($byId)) {
|
||||
try {
|
||||
/** @var Node $item */
|
||||
$item = $item->getParent();
|
||||
|
||||
if (!empty($byId[$item->getId()])) {
|
||||
if ($byId[$item->getId()] !== []) {
|
||||
foreach ($byId[$item->getId()] as $uid => $path) {
|
||||
$results[$uid] = $path . $appendix;
|
||||
}
|
||||
|
||||
unset($byId[$item->getId()]);
|
||||
}
|
||||
|
||||
$appendix = '/' . $item->getName() . $appendix;
|
||||
} catch (NotFoundException $e) {
|
||||
return $results;
|
||||
} catch (InvalidPathException $e) {
|
||||
return $results;
|
||||
} catch (NotPermittedException $e) {
|
||||
} catch (NotFoundException|InvalidPathException|NotPermittedException) {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -141,27 +137,27 @@ class ShareHelper implements IShareHelper {
|
||||
* 'test3' => ['token' => 't3', 'node_path' => '/SixTeen/TwentyThree/FortyTwo'],
|
||||
* ],
|
||||
*
|
||||
* @param Node $node
|
||||
* @param array[] $remotes
|
||||
* @return array
|
||||
* @param non-empty-array<string, array{node_id: int, token: string}> $remotes
|
||||
* @return array<string, array{token: string, node_path: string}>
|
||||
*/
|
||||
protected function getPathsForRemotes(Node $node, array $remotes) {
|
||||
/** @var array[] $byId */
|
||||
protected function getPathsForRemotes(Node $node, array $remotes): array {
|
||||
/** @var array<int, array<string, string>> $byId */
|
||||
$byId = [];
|
||||
/** @var array[] $results */
|
||||
/** @var array<string, array{token: string, node_path: string}> $results */
|
||||
$results = [];
|
||||
|
||||
foreach ($remotes as $cloudId => $info) {
|
||||
if (!isset($byId[$info['node_id']])) {
|
||||
$byId[$info['node_id']] = [];
|
||||
}
|
||||
|
||||
$byId[$info['node_id']][$cloudId] = $info['token'];
|
||||
}
|
||||
|
||||
$item = $node;
|
||||
while (!empty($byId)) {
|
||||
try {
|
||||
if (!empty($byId[$item->getId()])) {
|
||||
if ($byId[$item->getId()] !== []) {
|
||||
$path = $this->getMountedPath($item);
|
||||
foreach ($byId[$item->getId()] as $uid => $token) {
|
||||
$results[$uid] = [
|
||||
@@ -169,16 +165,12 @@ class ShareHelper implements IShareHelper {
|
||||
'token' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
unset($byId[$item->getId()]);
|
||||
}
|
||||
|
||||
/** @var Node $item */
|
||||
$item = $item->getParent();
|
||||
} catch (NotFoundException $e) {
|
||||
return $results;
|
||||
} catch (InvalidPathException $e) {
|
||||
return $results;
|
||||
} catch (NotPermittedException $e) {
|
||||
} catch (NotFoundException|InvalidPathException|NotPermittedException) {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -186,11 +178,7 @@ class ShareHelper implements IShareHelper {
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return string
|
||||
*/
|
||||
protected function getMountedPath(Node $node) {
|
||||
protected function getMountedPath(Node $node): string {
|
||||
$path = $node->getPath();
|
||||
$sections = explode('/', $path, 4);
|
||||
return '/' . $sections[3];
|
||||
|
||||
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OC\Snowflake;
|
||||
|
||||
use OC_Util;
|
||||
use OCP\ITempManager;
|
||||
use Override;
|
||||
|
||||
@@ -27,7 +28,7 @@ class FileSequence implements ISequence {
|
||||
public function __construct(
|
||||
ITempManager $tempManager,
|
||||
) {
|
||||
$this->workDir = $tempManager->getTempBaseDir() . '/' . self::LOCK_FILE_DIRECTORY;
|
||||
$this->workDir = $tempManager->getTempBaseDir() . '/' . self::LOCK_FILE_DIRECTORY . '_' . OC_Util::getInstanceId();
|
||||
$this->ensureWorkdirExists();
|
||||
}
|
||||
|
||||
|
||||
@@ -731,21 +731,10 @@ class Session implements IUserSession, Emitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokenLoginName = $dbToken->getLoginName();
|
||||
if ($this->manager->checkPassword($tokenLoginName, $pwd) === false) {
|
||||
// If the decrypted password is empty or not a valid local password,
|
||||
// but the user exists and is enabled, we DO NOT permanently invalidate the token.
|
||||
if (empty($pwd) || $this->manager->get($tokenLoginName) !== null) {
|
||||
$this->logger->warning('Password check failed for user {user}, but user is active. Token preserved.', [
|
||||
'app' => 'core',
|
||||
'user' => $tokenLoginName,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Legitimate password change or invalid user
|
||||
// Invalidate the token
|
||||
// If the token password is no longer valid mark it as such
|
||||
if ($this->manager->checkPassword($dbToken->getLoginName(), $pwd) === false) {
|
||||
$this->tokenProvider->markPasswordInvalid($dbToken, $token);
|
||||
// User is logged out
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ interface ITypedQueryBuilder extends IQueryBuilder {
|
||||
* @template NewS of string
|
||||
* @param NewS ...$columns The columns to select. They are not allowed to contain table names or aliases, or asterisks. Use {@see self::selectAlias()} for that.
|
||||
* @psalm-this-out self<S|NewS>
|
||||
* @return $this
|
||||
* @since 34.0.0
|
||||
* @note Psalm has a bug that prevents inferring the correct type in chained calls: https://github.com/vimeo/psalm/issues/8803. Convert the chained calls to standalone calls or switch to PHPStan, which suffered the same bug in the past, but fixed it in 2.1.5: https://github.com/phpstan/phpstan/issues/8439
|
||||
*/
|
||||
public function selectColumns(string ...$columns): self;
|
||||
|
||||
@@ -52,7 +54,9 @@ interface ITypedQueryBuilder extends IQueryBuilder {
|
||||
* @template NewS of string
|
||||
* @param NewS ...$columns The columns to select distinct. They are not allowed to contain table names or aliases, or asterisks. Use {@see self::selectAlias()} for that.
|
||||
* @psalm-this-out self<S|NewS>
|
||||
* @return $this
|
||||
* @since 34.0.0
|
||||
* @note Psalm has a bug that prevents inferring the correct type in chained calls: https://github.com/vimeo/psalm/issues/8803. Convert the chained calls to standalone calls or switch to PHPStan, which suffered the same bug in the past, but fixed it in 2.1.5: https://github.com/phpstan/phpstan/issues/8439
|
||||
*/
|
||||
public function selectColumnsDistinct(string ...$columns): self;
|
||||
|
||||
@@ -69,8 +73,244 @@ interface ITypedQueryBuilder extends IQueryBuilder {
|
||||
* @template NewS of string
|
||||
* @param NewS $alias
|
||||
* @psalm-this-out self<S|NewS>
|
||||
* @psalm-suppress LessSpecificImplementedReturnType
|
||||
* @return $this
|
||||
* @note Psalm has a bug that prevents inferring the correct type in chained calls: https://github.com/vimeo/psalm/issues/8803. Convert the chained calls to standalone calls or switch to PHPStan, which suffered the same bug in the past, but fixed it in 2.1.5: https://github.com/phpstan/phpstan/issues/8439
|
||||
*/
|
||||
#[Override]
|
||||
public function selectAlias($select, $alias): self;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setParameter($key, $value, $type = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function setParameters(array $params, array $types = []);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setFirstResult($firstResult);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setMaxResults($maxResults);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function delete($delete = null, $alias = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function update($update = null, $alias = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function insert($insert = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function from($from, $alias = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function join($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function innerJoin($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function leftJoin($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function rightJoin($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function set($key, $value);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function where(...$predicates);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function andWhere(...$where);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function orWhere(...$where);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function groupBy(...$groupBys);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function addGroupBy(...$groupBy);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setValue($column, $value);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function values(array $values);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function having(...$having);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function andHaving(...$having);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function orHaving(...$having);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function orderBy($sort, $order = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function addOrderBy($sort, $order = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function resetQueryParts($queryPartNames = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function resetQueryPart($queryPartName);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function hintShardKey(string $column, mixed $value, bool $overwrite = false): self;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function runAcrossAllShards(): self;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function forUpdate(ConflictResolutionMode $conflictResolutionMode = ConflictResolutionMode::Ordinary): self;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace OCP\Files\Events;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\Node;
|
||||
|
||||
/**
|
||||
* This event is triggered before a archive is created when a user requested
|
||||
@@ -25,16 +26,19 @@ class BeforeZipCreatedEvent extends Event {
|
||||
private bool $successful = true;
|
||||
private ?string $errorMessage = null;
|
||||
private ?Folder $folder = null;
|
||||
private array $nodeFilters = [];
|
||||
|
||||
/**
|
||||
* @param string|Folder $directory Folder instance, or (deprecated) string path relative to user folder
|
||||
* @param list<string> $files
|
||||
* @param list<string> $files Selected files, empty for folder selection
|
||||
* @param iterable<Node> $nodes Recursively collected nodes
|
||||
* @since 25.0.0
|
||||
* @since 31.0.0 support `OCP\Files\Folder` as `$directory` parameter - passing a string is deprecated now
|
||||
*/
|
||||
public function __construct(
|
||||
string|Folder $directory,
|
||||
private array $files,
|
||||
private iterable $nodes,
|
||||
) {
|
||||
parent::__construct();
|
||||
if ($directory instanceof Folder) {
|
||||
@@ -70,6 +74,30 @@ class BeforeZipCreatedEvent extends Event {
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Node>
|
||||
*/
|
||||
public function getNodes(): iterable {
|
||||
foreach ($this->nodes as $node) {
|
||||
$pass = true;
|
||||
foreach ($this->nodeFilters as $filter) {
|
||||
$pass = $pass && $filter($node);
|
||||
}
|
||||
|
||||
if ($pass) {
|
||||
yield $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $filter
|
||||
* @return void
|
||||
*/
|
||||
public function addNodeFilter(callable $filter): void {
|
||||
$this->nodeFilters[] = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 25.0.0
|
||||
*/
|
||||
|
||||
@@ -258,9 +258,9 @@ interface IManager {
|
||||
* @return ($currentAccess is true
|
||||
* ? array{
|
||||
* users?: array<string, array{node_id: int, node_path: string}>,
|
||||
* remote?: array<string, array{node_id: int, node_path: string}>,
|
||||
* remote?: array<string, array{node_id: int, token: string}>,
|
||||
* public?: bool,
|
||||
* mail?: array<string, array{node_id: int, node_path: string}>
|
||||
* mail?: array<string, array{node_id: int, token: string}>
|
||||
* }
|
||||
* : array{users?: list<string>, remote?: bool, public?: bool, mail?: list<string>})
|
||||
* @since 12.0.0
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCP\Share;
|
||||
|
||||
use OCP\AppFramework\Attribute\Consumable;
|
||||
use OCP\Files\Node;
|
||||
|
||||
/**
|
||||
* Interface IShareHelper
|
||||
*
|
||||
* @since 12
|
||||
* @since 12.0.0
|
||||
*/
|
||||
#[Consumable(since: '12.0.0')]
|
||||
interface IShareHelper {
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return array [ users => [Mapping $uid => $pathForUser], remotes => [Mapping $cloudId => $pathToMountRoot]]
|
||||
* @return array{users: array<string, string>, remotes: array<string, array{token: string, node_path: string}>} [ users => [Mapping $uid => $pathForUser], remotes => [Mapping $cloudId => $pathToMountRoot]]
|
||||
* @since 12
|
||||
*/
|
||||
public function getPathsForAccessList(Node $node);
|
||||
public function getPathsForAccessList(Node $node): array;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<file name="build/psalm/ITypedQueryBuilderTest.php"/>
|
||||
<file name="lib/private/DB/QueryBuilder/TypedQueryBuilder.php"/>
|
||||
<file name="lib/public/DB/QueryBuilder/ITypedQueryBuilder.php"/>
|
||||
<file name="lib/private/Share20/ShareHelper.php"/>
|
||||
<file name="lib/public/Share/IShareHelper.php"/>
|
||||
<ignoreFiles>
|
||||
<directory name="apps/**/composer"/>
|
||||
<directory name="apps/**/tests"/>
|
||||
|
||||
@@ -95,6 +95,7 @@ class FactoryTest extends TestCase {
|
||||
return [
|
||||
'null shortcut' => [null, null],
|
||||
'default language' => ['de', 'de'],
|
||||
'regional language' => ['de_DE', 'de_DE'],
|
||||
'malicious language' => ['de/../fr', 'defr'],
|
||||
'request language' => ['kab;q=0.8,ka;q=0.7,de;q=0.6', 'kab;q=0.8,ka;q=0.7,de;q=0.6'],
|
||||
];
|
||||
|
||||
@@ -159,6 +159,34 @@ class LoggerTest extends TestCase implements IWriter {
|
||||
$this->assertEquals($expectedLogs, $this->getLogs());
|
||||
}
|
||||
|
||||
public function testMatchesConditionIncreaseLoglevel(): void {
|
||||
$this->config->expects($this->any())
|
||||
->method('getValue')
|
||||
->willReturnMap([
|
||||
['loglevel', ILogger::WARN, ILogger::INFO],
|
||||
['log.condition', [], ['matches' => [
|
||||
[
|
||||
'message' => 'catched',
|
||||
'loglevel' => 3,
|
||||
]
|
||||
]]],
|
||||
]);
|
||||
$logger = $this->logger;
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')
|
||||
->willReturn('test-userid');
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
$userSession->method('getUser')
|
||||
->willReturn($user);
|
||||
$this->overwriteService(IUserSession::class, $userSession);
|
||||
|
||||
$logger->info('catched message');
|
||||
$logger->info('info level message');
|
||||
|
||||
$this->assertEquals(['1 info level message'], $this->getLogs());
|
||||
}
|
||||
|
||||
public function testLoggingWithDataArray(): void {
|
||||
$this->mockDefaultLogLevel();
|
||||
/** @var IWriter&MockObject */
|
||||
|
||||
Reference in New Issue
Block a user