Compare commits

..

5 Commits

Author SHA1 Message Date
provokateurin df380d0291 fixup! fix(Share20\Manager): Explicitly setup the filesystem for the user 2026-02-24 10:17:21 +01:00
provokateurin d12c339549 chore: Remove unused \OC\Cache\File
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-24 10:16:31 +01:00
provokateurin 19beae15f3 refactor(Server): Deprecate \OCP\ICache service and replace it with a local cache
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-24 10:14:21 +01:00
provokateurin 41642aae45 perf(base): Stop setting up the FS for every basic auth request
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-24 10:14:21 +01:00
provokateurin 5642b6b336 fix(Share20\Manager): Explicitly setup the filesystem for the user
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-24 10:14:21 +01:00
119 changed files with 373 additions and 2715 deletions
-4
View File
@@ -23,7 +23,3 @@
- [ ] [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
@@ -77,7 +77,6 @@ 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,7 +92,6 @@ 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',
-7
View File
@@ -191,7 +191,6 @@ 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",
@@ -204,8 +203,6 @@ 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",
@@ -230,10 +227,6 @@ 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 поламаний.",
-7
View File
@@ -189,7 +189,6 @@
"{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",
@@ -202,8 +201,6 @@
"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",
@@ -228,10 +225,6 @@
"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 поламаний.",
+4 -2
View File
@@ -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,7 +60,9 @@ 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,10 +104,9 @@ class CalendarFederationProvider implements ICloudFederationProvider {
);
}
// convert access to permissions
// TODO: implement read-write sharing
$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",
'',
@@ -123,27 +122,20 @@ class CalendarFederationProvider implements ICloudFederationProvider {
$sharedWithPrincipal = 'principals/users/' . $share->getShareWith();
// Delete existing incoming federated share first
$calendar = $this->federatedCalendarMapper->findByUri($sharedWithPrincipal, $calendarUri);
$this->federatedCalendarMapper->deleteByUri($sharedWithPrincipal, $calendarUri);
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);
}
$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);
$this->jobList->add(FederatedCalendarSyncJob::class, [
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
@@ -10,289 +10,29 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Federation;
use OCA\DAV\CalDAV\CalDavBackend;
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;
use OCA\DAV\CalDAV\Calendar;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend;
class FederatedCalendar extends Calendar {
public function __construct(
private readonly FederatedCalendarMapper $federatedCalendarMapper,
private readonly FederatedCalendarSyncService $federatedCalendarService,
private readonly CalDavBackend $caldavBackend,
Backend\BackendInterface $caldavBackend,
$calendarInfo,
IL10N $l10n,
IConfig $config,
LoggerInterface $logger,
private readonly FederatedCalendarMapper $federatedCalendarMapper,
) {
$this->principalUri = $calendarInfo['principaluri'];
$this->calendarUri = $calendarInfo['uri'];
$this->federationInfo = $federatedCalendarMapper->findByUri($this->principalUri, $this->calendarUri);
parent::__construct($caldavBackend, $calendarInfo, $l10n, $config, $logger);
}
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 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 {
public function delete() {
$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');
protected function getCalendarType(): int {
return CalDavBackend::CALENDAR_TYPE_FEDERATED;
}
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(),
'{' . \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(),
// TODO: implement read-write sharing
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => 1
];
}
}
@@ -9,23 +9,34 @@ 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 FederatedCalendarMapper $federatedCalendarMapper,
private readonly FederatedCalendarSyncService $federatedCalendarService,
private readonly CalDavBackend $caldavBackend,
private readonly IConfig $config,
private readonly LoggerInterface $logger,
private readonly FederatedCalendarMapper $federatedCalendarMapper,
IL10NFactory $l10nFactory,
) {
$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,7 +51,8 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
}
public function getPermissions(): int {
return $this->calendarInfo['{http://owncloud.org/ns}permissions'] ?? Constants::PERMISSION_READ;
// TODO: implement read-write sharing
return Constants::PERMISSION_READ;
}
public function isDeleted(): bool {
@@ -63,8 +64,7 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
}
public function isWritable(): bool {
$permissions = $this->getPermissions();
return ($permissions & Constants::PERMISSION_UPDATE) !== 0;
return false;
}
public function isEnabled(): bool {
@@ -1,107 +0,0 @@
<?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,52 +9,20 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Federation;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Service\ASyncService;
use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Http;
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
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 extends ASyncService {
use TTransactional;
class FederatedCalendarSyncService {
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 CalDavBackend $backend,
private readonly IDBConnection $dbConnection,
private readonly CalDavSyncService $syncService,
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(),
];
}
/**
@@ -63,77 +31,29 @@ class FederatedCalendarSyncService extends ASyncService {
* @throws ClientExceptionInterface If syncing the calendar fails.
*/
public function syncOne(FederatedCalendarEntity $calendar): int {
$credentials = $this->getCredentials($calendar);
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
$remoteUrl = $calendar->getRemoteUrl();
$syncToken = $calendar->getSyncTokenForSabre();
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;
}
// 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);
// 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
);
}
}
$syncResponse = $this->syncService->syncRemoteCalendar(
$remoteUrl,
$calDavUser,
$calendar->getToken(),
$syncToken,
$calendar,
);
$newSyncToken = $response['token'];
$newSyncToken = $syncResponse->getSyncToken();
// 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 {$credentials['remoteUrl']}: New sync token has unexpected format: $newSyncToken", [
$this->logger->error("Failed to sync federated calendar at $remoteUrl: New sync token has unexpected format: $newSyncToken", [
'calendar' => $calendar->toCalendarInfo(),
'newSyncToken' => $newSyncToken,
]);
@@ -147,58 +67,10 @@ class FederatedCalendarSyncService extends ASyncService {
$newSyncToken,
);
} else {
$this->logger->debug("Sync Token for {$credentials['remoteUrl']} unchanged from previous sync");
$this->logger->debug("Sync Token for $remoteUrl unchanged from previous sync");
$this->federatedCalendarMapper->updateSyncTime($calendar->getId());
}
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']);
return $syncResponse->getDownloadedEvents();
}
}
+1 -9
View File
@@ -12,7 +12,6 @@ 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;
@@ -174,15 +173,8 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
return;
}
/** @var Calendar&ICalendar $calendarNode */
/** @var Calendar $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,6 +8,7 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CardDAV\Notification;
use InvalidArgumentException;
use OCA\DAV\AppInfo\Application;
use OCP\IL10N;
use OCP\L10N\IFactory;
@@ -41,7 +42,7 @@ class Notifier implements INotifier {
*/
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== Application::APP_ID) {
throw new UnknownNotificationException();
throw new InvalidArgumentException();
}
$l = $this->l10nFactory->get(Application::APP_ID, $languageCode);
+2 -3
View File
@@ -482,10 +482,9 @@ class File extends Node implements IFile {
// comparing current file size with the one in DB
// if different, fix DB and refresh cache.
$fsSize = $this->fileView->filesize($this->getPath());
if ($this->getSize() !== $fsSize) {
if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
$logger = Server::get(LoggerInterface::class);
$logger->warning('fixing cached size of file id=' . $this->getId() . ', cached size was ' . $this->getSize() . ', but the filesystem reported a size of ' . $fsSize);
$logger->warning('fixing cached size of file id=' . $this->getId());
$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
$this->refreshInfo();
-66
View File
@@ -191,70 +191,4 @@ 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,12 +92,11 @@ class CalendarFederationProviderTest extends TestCase {
->willReturn(true);
$this->federatedCalendarMapper->expects(self::once())
->method('findByUri')
->method('deleteByUri')
->with(
'principals/users/sharee1',
'ae4b8ab904076fff2b955ea21b1a0d92',
)
->willReturn(null);
);
$this->federatedCalendarMapper->expects(self::once())
->method('insert')
@@ -124,68 +123,6 @@ 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')
@@ -333,65 +270,6 @@ class CalendarFederationProviderTest extends TestCase {
$this->calendarFederationProvider->shareReceived($share);
}
public function testShareReceivedWithReadWriteAccess(): 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' => 2, // Backend::ACCESS_READ_WRITE
'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::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')
@@ -402,7 +280,7 @@ class CalendarFederationProviderTest extends TestCase {
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
'displayName' => 'Calendar 1',
'color' => '#ff0000',
'access' => 999, // Invalid access value
'access' => 2, // Backend::ACCESS_READ_WRITE
'components' => 'VEVENT,VTODO',
]);
$share->method('getShareWith')
@@ -9,17 +9,13 @@ 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;
@@ -30,30 +26,21 @@ class FederatedCalendarSyncServiceTest extends TestCase {
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
private LoggerInterface&MockObject $logger;
private CalDavBackend&MockObject $backend;
private IDBConnection&MockObject $dbConnection;
private CalDavSyncService&MockObject $calDavSyncService;
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->backend = $this->createMock(CalDavBackend::class);
$this->dbConnection = $this->createMock(IDBConnection::class);
$this->calDavSyncService = $this->createMock(CalDavSyncService::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->backend,
$this->dbConnection,
$this->calDavSyncService,
$this->cloudIdManager,
);
}
@@ -74,24 +61,16 @@ class FederatedCalendarSyncServiceTest extends TestCase {
->with('user1')
->willReturn($cloudId);
// 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->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));
$this->federatedCalendarMapper->expects(self::once())
->method('updateSyncTokenAndTime')
@@ -99,7 +78,7 @@ class FederatedCalendarSyncServiceTest extends TestCase {
$this->federatedCalendarMapper->expects(self::never())
->method('updateSyncTime');
$this->assertEquals(0, $this->federatedCalendarSyncService->syncOne($calendar));
$this->assertEquals(10, $this->federatedCalendarSyncService->syncOne($calendar));
}
public function testSyncOneUnchanged(): void {
@@ -118,24 +97,16 @@ class FederatedCalendarSyncServiceTest extends TestCase {
->with('user1')
->willReturn($cloudId);
// 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->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));
$this->federatedCalendarMapper->expects(self::never())
->method('updateSyncTokenAndTime');
@@ -172,24 +143,16 @@ class FederatedCalendarSyncServiceTest extends TestCase {
->with('user1')
->willReturn($cloudId);
// 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->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));
$this->federatedCalendarMapper->expects(self::never())
->method('updateSyncTokenAndTime');
@@ -1,404 +0,0 @@
<?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');
}
}
+1 -1
View File
@@ -545,7 +545,7 @@ class Crypt {
$options,
$iv);
if ($plainContent !== false) {
if ($plainContent) {
return $plainContent;
} else {
throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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
View File
@@ -11,18 +11,7 @@ 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.",
-11
View File
@@ -9,18 +9,7 @@
"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.",
-10
View File
@@ -79,7 +79,6 @@ 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",
@@ -110,7 +109,6 @@ 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",
@@ -122,7 +120,6 @@ 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",
@@ -234,9 +231,6 @@ 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",
@@ -251,7 +245,6 @@ 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",
@@ -319,9 +312,7 @@ 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şı",
@@ -346,7 +337,6 @@ 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",
-10
View File
@@ -77,7 +77,6 @@
"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",
@@ -108,7 +107,6 @@
"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,7 +118,6 @@
"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",
@@ -232,9 +229,6 @@
"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",
@@ -249,7 +243,6 @@
"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",
@@ -317,9 +310,7 @@
"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şı",
@@ -344,7 +335,6 @@
"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",
+1 -1
View File
@@ -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." : "Шукати результати серед ваших файлів.",
+1 -1
View File
@@ -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(10)
expect(action.order).toBe(80)
expect(action.enabled).toBeDefined()
})
})
+3 -3
View File
@@ -5,7 +5,7 @@
import type { IFileAction } from '@nextcloud/files'
import FolderEyeSvg from '@mdi/svg/svg/folder-eye-outline.svg?raw'
import FolderMoveSvg from '@mdi/svg/svg/folder-move-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: () => FolderEyeSvg,
iconSvgInline: () => FolderMoveSvg,
enabled({ nodes, view }) {
// Not enabled for public shares
@@ -63,5 +63,5 @@ export const action: IFileAction = {
return null
},
order: 10,
order: 80,
}
+1 -1
View File
@@ -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();
@@ -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'])
]
+1 -1
View File
@@ -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 …" : "Име или е-пошта …",
+1 -1
View File
@@ -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 …" : "Име или е-пошта …",
+1 -1
View File
@@ -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" : "Встановити пароль",
+1 -1
View File
@@ -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" : "Встановити пароль",
@@ -236,7 +236,7 @@ class ShareAPIController extends OCSController {
$expiration = $share->getExpirationDate();
if ($expiration !== null) {
$expiration->setTimezone($this->dateTimeZone->getTimeZone());
$result['expiration'] = $expiration->format('Y-m-d H:i:s');
$result['expiration'] = $expiration->format('Y-m-d 00:00:00');
}
if ($share->getShareType() === IShare::TYPE_USER) {
+4 -4
View File
@@ -1052,7 +1052,7 @@ class ApiTest extends TestCase {
$share1 = $this->shareManager->getShareById($share1->getFullId());
// date should be changed
$dateWithinRange->setTime(23, 59, 59);
$dateWithinRange->setTime(0, 0, 0);
$dateWithinRange->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$this->assertEquals($dateWithinRange, $share1->getExpirationDate());
@@ -1263,7 +1263,7 @@ class ApiTest extends TestCase {
public static function datesProvider() {
$date = new \DateTime();
$date->setTime(23, 59, 59);
$date->setTime(0, 0);
$date->add(new \DateInterval('P5D'));
$date->setTimezone(new \DateTimeZone(date_default_timezone_get()));
@@ -1325,14 +1325,14 @@ class ApiTest extends TestCase {
$data = $result->getData();
$this->assertTrue(is_string($data['token']));
$this->assertEquals($date->format('Y-m-d 23:59:59'), $data['expiration']);
$this->assertEquals($date->format('Y-m-d 00:00:00'), $data['expiration']);
// check for correct link
$url = Server::get(IURLGenerator::class)->getAbsoluteURL('/index.php/s/' . $data['token']);
$this->assertEquals($url, $data['url']);
$share = $this->shareManager->getShareById('ocinternal:' . $data['id']);
$date->setTime(23, 59, 59);
$date->setTime(0, 0, 0);
$this->assertEquals($date, $share->getExpirationDate());
$this->shareManager->deleteShare($share);
@@ -773,7 +773,7 @@ class ShareAPIControllerTest extends TestCase {
$data['Folder shared with group'] = [$share, $expected, true];
// File shared by link with Expire
$expire = \DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-02 23:59:59');
$expire = \DateTime::createFromFormat('Y-m-d h:i:s', '2000-01-02 01:02:03');
$share = [
'101',
IShare::TYPE_LINK,
@@ -807,7 +807,7 @@ class ShareAPIControllerTest extends TestCase {
'file_target' => 'target',
'file_parent' => 3,
'token' => 'token',
'expiration' => '2000-01-02 23:59:59',
'expiration' => '2000-01-02 00:00:00',
'permissions' => 4,
'attributes' => null,
'stime' => 5,
@@ -4504,7 +4504,7 @@ class ShareAPIControllerTest extends TestCase {
'permissions' => 1,
'stime' => 946684862,
'parent' => null,
'expiration' => '2001-02-03 04:05:06',
'expiration' => '2001-02-03 00:00:00',
'token' => null,
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
@@ -4554,7 +4554,7 @@ class ShareAPIControllerTest extends TestCase {
'permissions' => 1,
'stime' => 946684862,
'parent' => null,
'expiration' => '2001-02-03 04:05:06',
'expiration' => '2001-02-03 00:00:00',
'token' => null,
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
@@ -99,11 +99,12 @@ class SharesReminderJobTest extends \Test\TestCase {
$someMail = 'test@test.com';
$noExpirationDate = null;
$today = new \DateTime();
// Expiration dates are set to end of day (23:59:59) by the Share Manager
$today->setTime(23, 59, 59);
$nearFuture = clone $today;
// For expiration dates, the time is always automatically set to zero by ShareAPIController
$today->setTime(0, 0);
$nearFuture = new \DateTime();
$nearFuture->setTimestamp($today->getTimestamp() + 86400 * 1);
$farFuture = new \DateTime();
$farFuture->setTimestamp($today->getTimestamp() + 86400 * 1);
$farFuture->setTimestamp($today->getTimestamp() + 86400 * 2);
$permissionRead = Constants::PERMISSION_READ;
$permissionCreate = $permissionRead | Constants::PERMISSION_CREATE;
$permissionUpdate = $permissionRead | Constants::PERMISSION_UPDATE;
+1
View File
@@ -193,6 +193,7 @@ abstract class TestCase extends \Test\TestCase {
Server::get(IUserSession::class)->setUser(null);
Filesystem::tearDown();
Server::get(IUserSession::class)->login($user, $password);
Filesystem::initMountPoints($user);
\OC::$server->getUserFolder($user);
\OC_Util::setupFS($user);
-16
View File
@@ -1,16 +0,0 @@
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);");
-14
View File
@@ -1,14 +0,0 @@
{ "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);"
}
-16
View File
@@ -1,16 +0,0 @@
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);");
-14
View File
@@ -1,14 +0,0 @@
{ "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);"
}
-16
View File
@@ -1,16 +0,0 @@
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);");
-14
View File
@@ -1,14 +0,0 @@
{ "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);"
}
-15
View File
@@ -1,15 +0,0 @@
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;");
-13
View File
@@ -1,13 +0,0 @@
{ "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;"
}
-17
View File
@@ -1,17 +0,0 @@
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);");
-15
View File
@@ -1,15 +0,0 @@
{ "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);"
}
-14
View File
@@ -1,14 +0,0 @@
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));");
-12
View File
@@ -1,12 +0,0 @@
{ "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));"
}
+12
View File
@@ -0,0 +1,12 @@
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;");
+10
View File
@@ -0,0 +1,10 @@
{ "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;"
}
-4
View File
@@ -1,15 +1,11 @@
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ş",
-4
View File
@@ -1,13 +1,9 @@
{ "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ş",
-3
View File
@@ -593,11 +593,8 @@ 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 (15 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 (15 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}",
-3
View File
@@ -591,11 +591,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 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 (15 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}",
-3
View File
@@ -593,11 +593,8 @@ 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 (15 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 (15 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}",
-3
View File
@@ -591,11 +591,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 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 (15 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}",
-3
View File
@@ -593,11 +593,8 @@ 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 (15 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}",
-3
View File
@@ -591,11 +591,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 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}",
-3
View File
@@ -593,11 +593,8 @@ 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 (15 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 (15 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}",
-3
View File
@@ -591,11 +591,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 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 (15 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}",
-27
View File
@@ -330,10 +330,6 @@ 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.",
@@ -386,8 +382,6 @@ 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.",
@@ -442,16 +436,9 @@ 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",
@@ -593,11 +580,8 @@ 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 (15 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 15 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}",
@@ -914,17 +898,6 @@ 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",
-27
View File
@@ -328,10 +328,6 @@
"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.",
@@ -384,8 +380,6 @@
"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.",
@@ -440,16 +434,9 @@
"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",
@@ -591,11 +578,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 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 15 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}",
@@ -912,17 +896,6 @@
"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",
+16 -19
View File
@@ -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,11 +590,8 @@ 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 (15 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}",
@@ -981,7 +978,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} облікових записів …"],
+16 -19
View File
@@ -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,11 +588,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 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}",
@@ -979,7 +976,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} облікових записів …"],
-3
View File
@@ -593,11 +593,8 @@ 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 (15 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}",
-3
View File
@@ -591,11 +591,8 @@
"cron.php is registered at a webcron service to call cron.php every 5 minutes over HTTP. Use case: Very small instance (15 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}",
+1 -1
View File
@@ -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);");
+1 -1
View File
@@ -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);"
}
-1
View File
@@ -109,7 +109,6 @@ 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!",
-1
View File
@@ -107,7 +107,6 @@
"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!",
+21 -21
View File
@@ -6730,9 +6730,9 @@
}
},
"node_modules/asn1.js/node_modules/bn.js": {
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/assert": {
@@ -7084,9 +7084,9 @@
"license": "MIT"
},
"node_modules/bn.js": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
"integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
"license": "MIT"
},
"node_modules/body-parser": {
@@ -8114,9 +8114,9 @@
}
},
"node_modules/create-ecdh/node_modules/bn.js": {
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/create-hash": {
@@ -8690,9 +8690,9 @@
}
},
"node_modules/diffie-hellman/node_modules/bn.js": {
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/dijkstrajs": {
@@ -8889,9 +8889,9 @@
}
},
"node_modules/elliptic/node_modules/bn.js": {
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/emoji-mart-vue-fast": {
@@ -13028,9 +13028,9 @@
}
},
"node_modules/miller-rabin/node_modules/bn.js": {
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/mime": {
@@ -14750,9 +14750,9 @@
}
},
"node_modules/public-encrypt/node_modules/bn.js": {
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/punycode": {
@@ -310,7 +310,7 @@ trait Sharing {
$data = simplexml_load_string($this->response->getBody())->data[0];
if ((string)$field == 'expiration') {
if (!empty($contentExpected)) {
$contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 23:59:59';
$contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00';
}
}
if (count($data->element) > 0) {
@@ -611,7 +611,7 @@ trait Sharing {
}
if ($field === 'expiration' && !empty($contentExpected)) {
$contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 23:59:59';
$contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00';
}
if ($contentExpected === 'A_NUMBER') {
+3 -12
View File
@@ -3265,12 +3265,6 @@
<code><![CDATA[$this->providers]]></code>
</UndefinedInterfaceMethod>
</file>
<file src="lib/private/Cache/File.php">
<LessSpecificImplementedReturnType>
<code><![CDATA[bool|mixed]]></code>
<code><![CDATA[bool|mixed]]></code>
</LessSpecificImplementedReturnType>
</file>
<file src="lib/private/Calendar/Manager.php">
<LessSpecificReturnStatement>
<code><![CDATA[array_merge(
@@ -3395,6 +3389,9 @@
<NullableReturnStatement>
<code><![CDATA[$alias]]></code>
</NullableReturnStatement>
<ParamNameMismatch>
<code><![CDATA[$selects]]></code>
</ParamNameMismatch>
</file>
<file src="lib/private/DB/QueryBuilder/QuoteHelper.php">
<InvalidNullableReturnType>
@@ -3404,12 +3401,6 @@
<code><![CDATA[$string]]></code>
</NullableReturnStatement>
</file>
<file src="lib/private/DB/QueryBuilder/TypedQueryBuilder.php">
<InternalMethod>
<code><![CDATA[select]]></code>
<code><![CDATA[selectDistinct]]></code>
</InternalMethod>
</file>
<file src="lib/private/DateTimeFormatter.php">
<FalsableReturnStatement>
<code><![CDATA[$l->l($type, $timestamp, [
-69
View File
@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
use OCP\IDBConnection;
use OCP\Server;
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
$qb = Server::get(IDBConnection::class)->getTypedQueryBuilder();
$qb->selectColumns('a', 'b');
$qb->selectColumns('c');
$qb->selectColumnsDistinct('d', 'e');
$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();
/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>|false */
$rows = $result->fetch(\PDO::FETCH_ASSOC);
/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>|false */
$rows = $result->fetchAssociative();
/** @psalm-check-type-exact $rows = list<array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>> */
$rows = $result->fetchAll(\PDO::FETCH_ASSOC);
/** @psalm-check-type-exact $rows = list<array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>> */
$rows = $result->fetchAllAssociative();
-3
View File
@@ -19,9 +19,6 @@ return (require __DIR__ . '/rector-shared.php')
$nextcloudDir . '/apps/settings/lib/Service/AuthorizedGroupService.php',
$nextcloudDir . '/lib/private/Files/Storage/Storage.php',
$nextcloudDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php',
$nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php',
$nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
$nextcloudDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
])
->withPreparedSets(
deadCode: true,
+1 -1
View File
@@ -82,7 +82,7 @@ class StatusCommand extends Command implements CompletionAwareInterface {
$numExecutedUnavailableMigrations = count($executedUnavailableMigrations);
$numNewMigrations = count(array_diff(array_keys($availableMigrations), $executedMigrations));
$pending = $ms->describeMigrationStep();
$pending = $ms->describeMigrationStep('lastest');
$infos = [
'App' => $ms->getApp(),
+13 -29
View File
@@ -43,12 +43,6 @@ 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'
)
;
}
@@ -82,32 +76,22 @@ class ResetPassword extends Base {
}
}
if ($input->getOption('no-password')) {
$question = new ConfirmationQuestion('Are you sure you want to clear the password for ' . $username . '?');
$question = new Question('Enter a new password: ');
$question->setHidden(true);
$password = $helper->ask($input, $output, $question);
if (!$helper->ask($input, $output, $question)) {
return 1;
}
if ($password === null) {
$output->writeln('<error>Password cannot be empty!</error>');
return 1;
}
$password = '';
} else {
$question = new Question('Enter a new password: ');
$question->setHidden(true);
$password = $helper->ask($input, $output, $question);
$question = new Question('Confirm the new password: ');
$question->setHidden(true);
$confirm = $helper->ask($input, $output, $question);
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;
}
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>');
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -21
View File
@@ -766,10 +766,9 @@ class OC {
self::checkConfig();
self::checkInstalled($systemConfig);
if (!self::$CLI) {
self::addSecurityHeaders();
self::performSameSiteCookieProtection($config);
}
self::addSecurityHeaders();
self::performSameSiteCookieProtection($config);
if (!defined('OC_CONSOLE')) {
$eventLogger->start('check_server', 'Run a few configuration checks');
@@ -978,23 +977,6 @@ class OC {
$throttler = Server::get(IThrottler::class);
$throttler->resetDelay($request->getRemoteAddress(), 'login', ['user' => $uid]);
}
try {
$cache = new \OC\Cache\File();
$cache->gc();
} catch (\OC\ServerNotAvailableException $e) {
// not a GC exception, pass it on
throw $e;
} catch (\OC\ForbiddenException $e) {
// filesystem blocked for this request, ignore
} catch (\Exception $e) {
// a GC exception should not prevent users from using OC,
// so log the exception
Server::get(LoggerInterface::class)->warning('Exception when running cache gc.', [
'app' => 'core',
'exception' => $e,
]);
}
});
}
}
@@ -327,7 +327,6 @@ return array(
'OCP\\DB\\QueryBuilder\\IParameter' => $baseDir . '/lib/public/DB/QueryBuilder/IParameter.php',
'OCP\\DB\\QueryBuilder\\IQueryBuilder' => $baseDir . '/lib/public/DB/QueryBuilder/IQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\IQueryFunction' => $baseDir . '/lib/public/DB/QueryBuilder/IQueryFunction.php',
'OCP\\DB\\QueryBuilder\\ITypedQueryBuilder' => $baseDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\Sharded\\IShardMapper' => $baseDir . '/lib/public/DB/QueryBuilder/Sharded/IShardMapper.php',
'OCP\\DB\\Types' => $baseDir . '/lib/public/DB/Types.php',
'OCP\\Dashboard\\IAPIWidget' => $baseDir . '/lib/public/Dashboard/IAPIWidget.php',
@@ -1248,7 +1247,6 @@ return array(
'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php',
'OC\\Calendar\\AvailabilityResult' => $baseDir . '/lib/private/Calendar/AvailabilityResult.php',
'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php',
@@ -1658,7 +1656,6 @@ return array(
'OC\\DB\\QueryBuilder\\Sharded\\ShardDefinition' => $baseDir . '/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardQueryRunner' => $baseDir . '/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardedQueryBuilder' => $baseDir . '/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php',
'OC\\DB\\QueryBuilder\\TypedQueryBuilder' => $baseDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
'OC\\DB\\ResultAdapter' => $baseDir . '/lib/private/DB/ResultAdapter.php',
'OC\\DB\\SQLiteMigrator' => $baseDir . '/lib/private/DB/SQLiteMigrator.php',
'OC\\DB\\SQLiteSessionInit' => $baseDir . '/lib/private/DB/SQLiteSessionInit.php',
@@ -368,7 +368,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\DB\\QueryBuilder\\IParameter' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IParameter.php',
'OCP\\DB\\QueryBuilder\\IQueryBuilder' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\IQueryFunction' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IQueryFunction.php',
'OCP\\DB\\QueryBuilder\\ITypedQueryBuilder' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\Sharded\\IShardMapper' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/Sharded/IShardMapper.php',
'OCP\\DB\\Types' => __DIR__ . '/../../..' . '/lib/public/DB/Types.php',
'OCP\\Dashboard\\IAPIWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IAPIWidget.php',
@@ -1289,7 +1288,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php',
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php',
'OC\\Calendar\\AvailabilityResult' => __DIR__ . '/../../..' . '/lib/private/Calendar/AvailabilityResult.php',
'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php',
@@ -1699,7 +1697,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\DB\\QueryBuilder\\Sharded\\ShardDefinition' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardQueryRunner' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardedQueryBuilder' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php',
'OC\\DB\\QueryBuilder\\TypedQueryBuilder' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
'OC\\DB\\ResultAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ResultAdapter.php',
'OC\\DB\\SQLiteMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteMigrator.php',
'OC\\DB\\SQLiteSessionInit' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteSessionInit.php',
-194
View File
@@ -1,194 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Cache;
use OC\Files\Filesystem;
use OC\Files\View;
use OC\ForbiddenException;
use OC\User\NoUserException;
use OCP\Files\LockNotAcquiredException;
use OCP\ICache;
use OCP\IUserSession;
use OCP\Lock\LockedException;
use OCP\Security\ISecureRandom;
use OCP\Server;
use Psr\Log\LoggerInterface;
class File implements ICache {
/** @var View */
protected $storage;
/**
* Returns the cache storage for the logged in user
*
* @return View cache storage
* @throws ForbiddenException
* @throws NoUserException
*/
protected function getStorage() {
if ($this->storage !== null) {
return $this->storage;
}
$session = Server::get(IUserSession::class);
if ($session->isLoggedIn()) {
$rootView = new View();
$userId = $session->getUser()->getUID();
Filesystem::initMountPoints($userId);
if (!$rootView->file_exists('/' . $userId . '/cache')) {
$rootView->mkdir('/' . $userId . '/cache');
}
$this->storage = new View('/' . $userId . '/cache');
return $this->storage;
} else {
Server::get(LoggerInterface::class)->error('Can\'t get cache storage, user not logged in', ['app' => 'core']);
throw new ForbiddenException('Can\t get cache storage, user not logged in');
}
}
/**
* @param string $key
* @return mixed|null
* @throws ForbiddenException
*/
public function get($key) {
$result = null;
if ($this->hasKey($key)) {
$storage = $this->getStorage();
$result = $storage->file_get_contents($key);
}
return $result;
}
/**
* Returns the size of the stored/cached data
*
* @param string $key
* @return int
*/
public function size($key) {
$result = 0;
if ($this->hasKey($key)) {
$storage = $this->getStorage();
$result = $storage->filesize($key);
}
return $result;
}
/**
* @param string $key
* @param mixed $value
* @param int $ttl
* @return bool|mixed
* @throws ForbiddenException
*/
public function set($key, $value, $ttl = 0) {
$storage = $this->getStorage();
$result = false;
// unique id to avoid chunk collision, just in case
$uniqueId = Server::get(ISecureRandom::class)->generate(
16,
ISecureRandom::CHAR_ALPHANUMERIC
);
// use part file to prevent hasKey() to find the key
// while it is being written
$keyPart = $key . '.' . $uniqueId . '.part';
if ($storage && $storage->file_put_contents($keyPart, $value)) {
if ($ttl === 0) {
$ttl = 86400; // 60*60*24
}
$result = $storage->touch($keyPart, time() + $ttl);
$result &= $storage->rename($keyPart, $key);
}
return $result;
}
/**
* @param string $key
* @return bool
* @throws ForbiddenException
*/
public function hasKey($key) {
$storage = $this->getStorage();
if ($storage && $storage->is_file($key) && $storage->isReadable($key)) {
return true;
}
return false;
}
/**
* @param string $key
* @return bool|mixed
* @throws ForbiddenException
*/
public function remove($key) {
$storage = $this->getStorage();
if (!$storage) {
return false;
}
return $storage->unlink($key);
}
/**
* @param string $prefix
* @return bool
* @throws ForbiddenException
*/
public function clear($prefix = '') {
$storage = $this->getStorage();
if ($storage && $storage->is_dir('/')) {
$dh = $storage->opendir('/');
if (is_resource($dh)) {
while (($file = readdir($dh)) !== false) {
if ($file !== '.' && $file !== '..' && ($prefix === '' || str_starts_with($file, $prefix))) {
$storage->unlink('/' . $file);
}
}
}
}
return true;
}
/**
* Runs GC
* @throws ForbiddenException
*/
public function gc() {
$storage = $this->getStorage();
if ($storage) {
$ttl = \OC::$server->getConfig()->getSystemValueInt('cache_chunk_gc_ttl', 60 * 60 * 24);
$now = time() - $ttl;
$dh = $storage->opendir('/');
if (!is_resource($dh)) {
return null;
}
while (($file = readdir($dh)) !== false) {
if ($file !== '.' && $file !== '..') {
try {
$mtime = $storage->filemtime('/' . $file);
if ($mtime < $now) {
$storage->unlink('/' . $file);
}
} catch (LockedException $e) {
// ignore locked chunks
Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']);
} catch (\OCP\Files\ForbiddenException $e) {
Server::get(LoggerInterface::class)->debug('Could not cleanup forbidden chunk "' . $file . '"', ['app' => 'core']);
} catch (LockNotAcquiredException $e) {
Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']);
}
}
}
}
}
public static function isAvailable(): bool {
return true;
}
}
-2
View File
@@ -14,8 +14,6 @@ use PDO;
/**
* Wrap an array or rows into a result interface
*
* @template-implements IResult<string>
*/
class ArrayResult implements IResult {
protected int $count;
-9
View File
@@ -34,7 +34,6 @@ use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
use OC\DB\QueryBuilder\Sharded\ShardDefinition;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\ITypedQueryBuilder;
use OCP\DB\QueryBuilder\Sharded\IShardMapper;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
@@ -248,14 +247,6 @@ class Connection extends PrimaryReadReplicaConnection {
* Returns a QueryBuilder for the connection.
*/
public function getQueryBuilder(): IQueryBuilder {
return $this->getInnerQueryBuilder();
}
public function getTypedQueryBuilder(): ITypedQueryBuilder {
return $this->getInnerQueryBuilder();
}
private function getInnerQueryBuilder(): IQueryBuilder&ITypedQueryBuilder {
$this->queriesBuilt++;
$builder = new QueryBuilder(
-5
View File
@@ -17,7 +17,6 @@ use OC\DB\QueryBuilder\Sharded\ShardDefinition;
use OCP\DB\IPreparedStatement;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\ITypedQueryBuilder;
use OCP\IDBConnection;
/**
@@ -33,10 +32,6 @@ class ConnectionAdapter implements IDBConnection {
return $this->inner->getQueryBuilder();
}
public function getTypedQueryBuilder(): ITypedQueryBuilder {
return $this->inner->getTypedQueryBuilder();
}
public function prepare($sql, $limit = null, $offset = null): IPreparedStatement {
try {
return new PreparedStatement(
@@ -16,7 +16,7 @@ use OCP\IDBConnection;
/**
* Base class for creating classes that extend the builtin query builder
*/
abstract class ExtendedQueryBuilder extends TypedQueryBuilder {
abstract class ExtendedQueryBuilder implements IQueryBuilder {
public function __construct(
protected IQueryBuilder $builder,
) {
@@ -100,7 +100,7 @@ abstract class ExtendedQueryBuilder extends TypedQueryBuilder {
return $this;
}
public function selectAlias($select, $alias): self {
public function selectAlias($select, $alias) {
$this->builder->selectAlias($select, $alias);
return $this;
}
@@ -92,7 +92,7 @@ class PartitionedQueryBuilder extends ShardedQueryBuilder {
return $this;
}
public function selectAlias($select, $alias): self {
public function selectAlias($select, $alias) {
$this->selects[] = ['select' => $select, 'alias' => $alias];
return $this;
}
+10 -11
View File
@@ -30,9 +30,8 @@ use OCP\DB\QueryBuilder\IQueryFunction;
use OCP\IDBConnection;
use Override;
use Psr\Log\LoggerInterface;
use RuntimeException;
class QueryBuilder extends TypedQueryBuilder {
class QueryBuilder implements IQueryBuilder {
private \Doctrine\DBAL\Query\QueryBuilder $queryBuilder;
private QuoteHelper $helper;
private bool $automaticTablePrefix = true;
@@ -243,7 +242,7 @@ class QueryBuilder extends TypedQueryBuilder {
public function executeQuery(?IDBConnection $connection = null): IResult {
if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
throw new RuntimeException('Invalid query type, expected SELECT query');
throw new \RuntimeException('Invalid query type, expected SELECT query');
}
$this->prepareForExecute();
@@ -260,7 +259,7 @@ class QueryBuilder extends TypedQueryBuilder {
public function executeStatement(?IDBConnection $connection = null): int {
if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
throw new RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
}
$this->prepareForExecute();
@@ -477,7 +476,7 @@ class QueryBuilder extends TypedQueryBuilder {
*
* @return $this This QueryBuilder instance.
*/
public function selectAlias($select, $alias): self {
public function selectAlias($select, $alias) {
$this->queryBuilder->addSelect(
$this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias)
);
@@ -525,18 +524,18 @@ class QueryBuilder extends TypedQueryBuilder {
* ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
* </code>
*
* @param mixed ...$select The selection expression.
* @param mixed ...$selects The selection expression.
*
* @return $this This QueryBuilder instance.
*/
public function addSelect(...$select) {
if (count($select) === 1 && is_array($select[0])) {
$select = $select[0];
public function addSelect(...$selects) {
if (count($selects) === 1 && is_array($selects[0])) {
$selects = $selects[0];
}
$this->addOutputColumns($select);
$this->addOutputColumns($selects);
$this->queryBuilder->addSelect(
$this->helper->quoteColumnNames($select)
$this->helper->quoteColumnNames($selects)
);
return $this;
@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\DB\QueryBuilder;
use OCP\DB\QueryBuilder\ITypedQueryBuilder;
use RuntimeException;
/**
* @psalm-suppress InvalidTemplateParam
* @template-implements ITypedQueryBuilder<string>
*/
abstract class TypedQueryBuilder implements ITypedQueryBuilder {
private function validateColumn(string $column): void {
if (str_contains($column, '.') || trim($column) === '*') {
throw new RuntimeException('Only column names are allowed, got: ' . $column);
}
}
public function selectColumns(string ...$columns): static {
foreach ($columns as $column) {
$this->validateColumn($column);
}
return $this->select(...$columns);
}
public function selectColumnsDistinct(string ...$columns): static {
foreach ($columns as $column) {
$this->validateColumn($column);
}
return $this->selectDistinct($columns);
}
}
-2
View File
@@ -15,8 +15,6 @@ use PDO;
/**
* Adapts DBAL 2.6 API for DBAL 3.x for backwards compatibility of a leaked type
*
* @template-implements IResult<string>
*/
class ResultAdapter implements IResult {
public function __construct(
@@ -92,12 +92,7 @@ class CacheJail extends CacheWrapper {
protected function formatCacheEntry($entry) {
if (isset($entry['path'])) {
$jailedPath = $this->getJailedPath($entry['path']);
if ($jailedPath !== null) {
$entry['path'] = $jailedPath;
} else {
return false;
}
$entry['path'] = $this->getJailedPath($entry['path']);
}
return $entry;
}
+7 -16
View File
@@ -20,7 +20,6 @@ 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;
@@ -37,11 +36,7 @@ class Manager implements IMountManager {
}
public function addMount(IMountPoint $mount): void {
$mountPoint = $mount->getMountPoint();
$mountProvider = $mount->getMountProvider();
$this->mounts[$mountPoint] = $mount;
$this->mountsByProvider[$mountProvider] ??= [];
$this->mountsByProvider[$mountProvider][$mountPoint] = $mount;
$this->mounts[$mount->getMountPoint()] = $mount;
$this->pathCache->clear();
$this->inPathCache->clear();
$this->areMountsSorted = false;
@@ -172,7 +167,6 @@ class Manager implements IMountManager {
public function clear(): void {
$this->mounts = [];
$this->mountsByProvider = [];
$this->pathCache->clear();
$this->inPathCache->clear();
}
@@ -242,18 +236,15 @@ class Manager implements IMountManager {
* @param string[] $mountProviders
* @return array<string, IMountPoint>
*/
public function getMountsByMountProvider(string $path, array $mountProviders): array {
public function getMountsByMountProvider(string $path, array $mountProviders) {
$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);
}
/**

Some files were not shown because too many files have changed in this diff Show More