From 0221d71fb771265d974692c0dc97543b39d4b879 Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 6 Feb 2026 00:41:24 +0100 Subject: [PATCH 1/2] fix(share): Set expiration time to end of day (23:59:59) Shares now expire at the end of the selected day instead of the beginning, allowing access throughout the entire expiration day. Also return actual stored time in API response instead of hardcoded 00:00:00 to prevent timezone-related display issues in the UI. Signed-off-by: nfebe --- .../lib/Controller/ShareAPIController.php | 2 +- lib/private/Share20/Manager.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index d442665248d9c..91d4bfdbcbf46 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -235,7 +235,7 @@ protected function formatShare(IShare $share, ?Node $recipientNode = null): arra $expiration = $share->getExpirationDate(); if ($expiration !== null) { $expiration->setTimezone($this->dateTimeZone->getTimeZone()); - $result['expiration'] = $expiration->format('Y-m-d 00:00:00'); + $result['expiration'] = $expiration->format('Y-m-d H:i:s'); } $currentUserPermissions = $recipientNode?->getPermissions() ?? Constants::PERMISSION_ALL; diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 970f17c0f4079..cc8482ac71ad2 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -303,7 +303,7 @@ protected function validateExpirationDateInternal(IShare $share) { if (!$share->getNoExpirationDate() || $isEnforced) { if ($expirationDate !== null) { $expirationDate->setTimezone($this->dateTimeZone->getTimeZone()); - $expirationDate->setTime(0, 0, 0); + $expirationDate->setTime(23, 59, 59); $date = new \DateTime('now', $this->dateTimeZone->getTimeZone()); $date->setTime(0, 0, 0); @@ -322,7 +322,7 @@ protected function validateExpirationDateInternal(IShare $share) { if ($fullId === null && $expirationDate === null && $defaultExpireDate) { $expirationDate = new \DateTime('now', $this->dateTimeZone->getTimeZone()); - $expirationDate->setTime(0, 0, 0); + $expirationDate->setTime(23, 59, 59); $days = (int)$this->config->getAppValue('core', $configProp, (string)$defaultExpireDays); if ($days > $defaultExpireDays) { $days = $defaultExpireDays; @@ -337,7 +337,7 @@ protected function validateExpirationDateInternal(IShare $share) { } $date = new \DateTime('now', $this->dateTimeZone->getTimeZone()); - $date->setTime(0, 0, 0); + $date->setTime(23, 59, 59); $date->add(new \DateInterval('P' . $defaultExpireDays . 'D')); if ($date < $expirationDate) { throw new GenericShareException($this->l->n('Cannot set expiration date more than %n day in the future', 'Cannot set expiration date more than %n days in the future', $defaultExpireDays), code: 404); @@ -381,7 +381,7 @@ protected function validateExpirationDateLink(IShare $share) { if (!($share->getNoExpirationDate() && !$isEnforced)) { if ($expirationDate !== null) { $expirationDate->setTimezone($this->dateTimeZone->getTimeZone()); - $expirationDate->setTime(0, 0, 0); + $expirationDate->setTime(23, 59, 59); $date = new \DateTime('now', $this->dateTimeZone->getTimeZone()); $date->setTime(0, 0, 0); @@ -400,7 +400,7 @@ protected function validateExpirationDateLink(IShare $share) { if ($fullId === null && $expirationDate === null && $this->shareApiLinkDefaultExpireDate()) { $expirationDate = new \DateTime('now', $this->dateTimeZone->getTimeZone()); - $expirationDate->setTime(0, 0, 0); + $expirationDate->setTime(23, 59, 59); $days = (int)$this->config->getAppValue('core', 'link_defaultExpDays', (string)$this->shareApiLinkDefaultExpireDays()); if ($days > $this->shareApiLinkDefaultExpireDays()) { @@ -416,7 +416,7 @@ protected function validateExpirationDateLink(IShare $share) { } $date = new \DateTime('now', $this->dateTimeZone->getTimeZone()); - $date->setTime(0, 0, 0); + $date->setTime(23, 59, 59); $date->add(new \DateInterval('P' . $this->shareApiLinkDefaultExpireDays() . 'D')); if ($date < $expirationDate) { throw new GenericShareException( From bbc47827e9f943f6433935bf817d8e40ccdb8d1d Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 6 Feb 2026 00:46:22 +0100 Subject: [PATCH 2/2] test(share): Update expiration date tests for end-of-day time Update expected values in ManagerTest to reflect the new behavior where share expiration dates are set to 23:59:59 instead of 00:00:00. Signed-off-by: nfebe Signed-off-by: Carl Schwan --- apps/files_sharing/tests/ApiTest.php | 8 +-- .../Controller/ShareAPIControllerTest.php | 12 ++-- .../tests/SharesReminderJobTest.php | 9 ++- autotest.sh | 2 +- .../features/bootstrap/Sharing.php | 4 +- tests/lib/Share20/ManagerTest.php | 57 ++++++++++++------- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/apps/files_sharing/tests/ApiTest.php b/apps/files_sharing/tests/ApiTest.php index 960f29224bb9e..ec98cf4154eca 100644 --- a/apps/files_sharing/tests/ApiTest.php +++ b/apps/files_sharing/tests/ApiTest.php @@ -1091,7 +1091,7 @@ public function testUpdateShareExpireDate(): void { $share1 = $this->shareManager->getShareById($share1->getFullId()); // date should be changed - $dateWithinRange->setTime(0, 0, 0); + $dateWithinRange->setTime(23, 59, 59); $dateWithinRange->setTimezone(new \DateTimeZone(date_default_timezone_get())); $this->assertEquals($dateWithinRange, $share1->getExpirationDate()); @@ -1306,7 +1306,7 @@ public function testShareStorageMountPoint(): void { public static function datesProvider() { $date = new \DateTime(); - $date->setTime(0, 0); + $date->setTime(23, 59, 59); $date->add(new \DateInterval('P5D')); $date->setTimezone(new \DateTimeZone(date_default_timezone_get())); @@ -1371,14 +1371,14 @@ public function testCreatePublicLinkExpireDateValid(): void { $data = $result->getData(); $this->assertTrue(is_string($data['token'])); - $this->assertEquals($date->format('Y-m-d 00:00:00'), $data['expiration']); + $this->assertEquals($date->format('Y-m-d 23:59:59'), $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(0, 0, 0); + $date->setTime(23, 59, 59); $this->assertEquals($date, $share->getExpirationDate()); $this->shareManager->deleteShare($share); diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index f1a6806a11887..35c7198580ab0 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -774,9 +774,9 @@ public function dataGetShare() { $data[] = [$share, $expected]; // File shared by link with Expire - $expire = \DateTime::createFromFormat('Y-m-d h:i:s', '2000-01-02 01:02:03'); - $share = $this->createShare( - 101, + $expire = \DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-02 23:59:59'); + $share = [ + '101', IShare::TYPE_LINK, null, 'initiatorId', @@ -808,7 +808,7 @@ public function dataGetShare() { 'file_target' => 'target', 'file_parent' => 3, 'token' => 'token', - 'expiration' => '2000-01-02 00:00:00', + 'expiration' => '2000-01-02 23:59:59', 'permissions' => 4, 'attributes' => null, 'stime' => 5, @@ -4481,7 +4481,7 @@ public function dataFormatShare() { 'permissions' => 1, 'stime' => 946684862, 'parent' => null, - 'expiration' => '2001-02-03 00:00:00', + 'expiration' => '2001-02-03 04:05:06', 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', @@ -4535,7 +4535,7 @@ public function dataFormatShare() { 'permissions' => 1, 'stime' => 946684862, 'parent' => null, - 'expiration' => '2001-02-03 00:00:00', + 'expiration' => '2001-02-03 04:05:06', 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', diff --git a/apps/files_sharing/tests/SharesReminderJobTest.php b/apps/files_sharing/tests/SharesReminderJobTest.php index ce468e279ecc1..49bd921b21b90 100644 --- a/apps/files_sharing/tests/SharesReminderJobTest.php +++ b/apps/files_sharing/tests/SharesReminderJobTest.php @@ -99,12 +99,11 @@ public static function dataSharesReminder() { $someMail = 'test@test.com'; $noExpirationDate = null; $today = new \DateTime(); - // 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); + // Expiration dates are set to end of day (23:59:59) by the Share Manager + $today->setTime(23, 59, 59); + $nearFuture = clone $today; $farFuture = new \DateTime(); - $farFuture->setTimestamp($today->getTimestamp() + 86400 * 2); + $farFuture->setTimestamp($today->getTimestamp() + 86400 * 1); $permissionRead = Constants::PERMISSION_READ; $permissionCreate = $permissionRead | Constants::PERMISSION_CREATE; $permissionUpdate = $permissionRead | Constants::PERMISSION_UPDATE; diff --git a/autotest.sh b/autotest.sh index 32a844a670de8..a30931414c619 100755 --- a/autotest.sh +++ b/autotest.sh @@ -309,7 +309,7 @@ function execute_tests { if [ ! -z "$USEDOCKER" ] ; then echo "Fire up the postgres docker" DOCKER_CONTAINER_ID=$(docker run -e POSTGRES_DB="$DATABASENAME" -e POSTGRES_USER="$DATABASEUSER" -e POSTGRES_PASSWORD=owncloud -d postgres) - DATABASEHOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "$DOCKER_CONTAINER_ID") + DATABASEHOST=$(docker inspect --format="{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" "$DOCKER_CONTAINER_ID") echo "Waiting for Postgres initialisation ..." diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index 14e736b90f271..c2c2fcda939b9 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -318,7 +318,7 @@ public function isFieldInResponse($field, $contentExpected) { $data = simplexml_load_string($this->response->getBody())->data[0]; if ((string)$field == 'expiration') { if (!empty($contentExpected)) { - $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00'; + $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 23:59:59'; } } if (count($data->element) > 0) { @@ -625,7 +625,7 @@ private function assertFieldIsInReturnedShare(string $field, string $contentExpe } if ($field === 'expiration' && !empty($contentExpected)) { - $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00'; + $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 23:59:59'; } if ($contentExpected === 'A_NUMBER') { diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index f0000c89fffdf..7e5d4f4f20c66 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -1293,7 +1293,7 @@ public function testValidateExpirationDateInternalEnforceButNotSetNewShare($shar } $expected = new \DateTime('now', $this->timezone); - $expected->setTime(0, 0, 0); + $expected->setTime(23, 59, 59); $expected->add(new \DateInterval('P3D')); self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); @@ -1326,7 +1326,7 @@ public function testValidateExpirationDateInternalEnforceRelaxedDefaultButNotSet } $expected = new \DateTime('now', $this->timezone); - $expected->setTime(0, 0, 0); + $expected->setTime(23, 59, 59); $expected->add(new \DateInterval('P1D')); self::invokePrivate($this->manager, 'validateExpirationDateInternal', [$share]); @@ -1373,7 +1373,7 @@ public function testValidateExpirationDateInternalEnforceValid($shareType): void $future->setTime(1, 2, 3); $expected = clone $future; - $expected->setTime(0, 0, 0); + $expected->setTime(23, 59, 59); $share = $this->manager->newShare(); $share->setShareType($shareType); @@ -1413,7 +1413,7 @@ public function testValidateExpirationDateInternalNoDefault($shareType): void { $date->setTime(1, 2, 3); $expected = clone $date; - $expected->setTime(0, 0, 0); + $expected->setTime(23, 59, 59); $share = $this->manager->newShare(); $share->setShareType($shareType); @@ -1453,7 +1453,7 @@ public function testValidateExpirationDateInternalNoDateDefault($shareType): voi $share->setShareType($shareType); $expected = new \DateTime('now', $this->timezone); - $expected->setTime(0, 0); + $expected->setTime(23, 59, 59); $expected->add(new \DateInterval('P3D')); $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); @@ -1491,7 +1491,7 @@ public function testValidateExpirationDateInternalDefault($shareType): void { $future->setTime(1, 2, 3); $expected = clone $future; - $expected->setTime(0, 0); + $expected->setTime(23, 59, 59); $share = $this->manager->newShare(); $share->setShareType($shareType); @@ -1528,7 +1528,7 @@ public function testValidateExpirationDateInternalDefault($shareType): void { public function testValidateExpirationDateInternalHookModification($shareType): void { $nextWeek = new \DateTime('now', $this->timezone); $nextWeek->add(new \DateInterval('P7D')); - $nextWeek->setTime(0, 0, 0); + $nextWeek->setTime(23, 59, 59); $save = clone $nextWeek; @@ -1555,7 +1555,7 @@ public function testValidateExpirationDateInternalHookException($shareType): voi $nextWeek = new \DateTime(); $nextWeek->add(new \DateInterval('P7D')); - $nextWeek->setTime(0, 0, 0); + $nextWeek->setTime(23, 59, 59); $share = $this->manager->newShare(); $share->setShareType($shareType); @@ -1656,7 +1656,7 @@ public function testValidateExpirationDateEnforceButNotSetNewShare(): void { ]); $expected = new \DateTime('now', $this->timezone); - $expected->setTime(0, 0, 0); + $expected->setTime(23, 59, 59); $expected->add(new \DateInterval('P3D')); self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); @@ -1681,7 +1681,7 @@ public function testValidateExpirationDateEnforceRelaxedDefaultButNotSetNewShare ]); $expected = new \DateTime('now', $this->timezone); - $expected->setTime(0, 0, 0); + $expected->setTime(23, 59, 59); $expected->add(new \DateInterval('P1D')); self::invokePrivate($this->manager, 'validateExpirationDateLink', [$share]); @@ -1720,7 +1720,7 @@ public function testValidateExpirationDateEnforceValid(): void { $future->setTime(1, 2, 3); $expected = clone $future; - $expected->setTime(0, 0, 0); + $expected->setTime(23, 59, 59); $share = $this->manager->newShare(); $share->setExpirationDate($future); @@ -1753,7 +1753,7 @@ public function testValidateExpirationDateNoDefault(): void { $date->setTime(1, 2, 3); $expected = clone $date; - $expected->setTime(0, 0); + $expected->setTime(23, 59, 59); $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); $share = $this->manager->newShare(); @@ -1790,7 +1790,7 @@ public function testValidateExpirationDateNoDateDefault(): void { $expected = new \DateTime('now', $this->timezone); $expected->add(new \DateInterval('P3D')); - $expected->setTime(0, 0); + $expected->setTime(23, 59, 59); $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); $this->config->method('getAppValue') @@ -1822,7 +1822,7 @@ public function testValidateExpirationDateDefault(): void { $future->setTime(1, 2, 3); $expected = clone $future; - $expected->setTime(0, 0); + $expected->setTime(23, 59, 59); $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); $share = $this->manager->newShare(); @@ -1858,7 +1858,7 @@ public function testValidateExpirationNegativeOffsetTimezone(): void { $expected = clone $future; $expected->setTimezone($this->timezone); - $expected->setTime(0, 0); + $expected->setTime(23, 59, 59); $expected->setTimezone(new \DateTimeZone(date_default_timezone_get())); $share = $this->manager->newShare(); @@ -1892,7 +1892,7 @@ public function testValidateExpirationDateHookModification(): void { $nextWeek->add(new \DateInterval('P7D')); $save = clone $nextWeek; - $save->setTime(0, 0); + $save->setTime(23, 59, 59); $save->sub(new \DateInterval('P2D')); $save->setTimezone(new \DateTimeZone(date_default_timezone_get())); @@ -1916,7 +1916,7 @@ public function testValidateExpirationDateHookException(): void { $nextWeek = new \DateTime(); $nextWeek->add(new \DateInterval('P7D')); - $nextWeek->setTime(0, 0, 0); + $nextWeek->setTime(23, 59, 59); $share = $this->manager->newShare(); $share->setExpirationDate($nextWeek); @@ -2580,7 +2580,7 @@ public function testCanShare($expected, $sharingEnabled, $disabledForUser): void public function testCreateShareUser(): void { /** @var Manager&MockObject $manager */ $manager = $this->createManagerMock() - ->onlyMethods(['canShare', 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks']) + ->onlyMethods(['canShare', 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) ->getMock(); $shareOwner = $this->createMock(IUser::class); @@ -2616,6 +2616,10 @@ public function testCreateShareUser(): void { $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); + $manager->expects($this->once()) + ->method('validateExpirationDateInternal') + ->with($share) + ->willReturnArgument(0); $this->defaultProvider ->expects($this->once()) @@ -2635,7 +2639,7 @@ public function testCreateShareUser(): void { public function testCreateShareGroup(): void { $manager = $this->createManagerMock() - ->onlyMethods(['canShare', 'generalCreateChecks', 'groupCreateChecks', 'pathCreateChecks']) + ->onlyMethods(['canShare', 'generalCreateChecks', 'groupCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) ->getMock(); $shareOwner = $this->createMock(IUser::class); @@ -2671,6 +2675,10 @@ public function testCreateShareGroup(): void { $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); + $manager->expects($this->once()) + ->method('validateExpirationDateInternal') + ->with($share) + ->willReturnArgument(0); $this->defaultProvider ->expects($this->once()) @@ -2904,6 +2912,7 @@ public function testCreateShareHookError(): void { 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks', + 'validateExpirationDateInternal', ]) ->getMock(); @@ -2940,6 +2949,10 @@ public function testCreateShareHookError(): void { $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); + $manager->expects($this->once()) + ->method('validateExpirationDateInternal') + ->with($share) + ->willReturnArgument(0); $share->expects($this->once()) ->method('setShareOwner') @@ -2964,7 +2977,7 @@ public function testCreateShareHookError(): void { public function testCreateShareOfIncomingFederatedShare(): void { $manager = $this->createManagerMock() - ->onlyMethods(['canShare', 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks']) + ->onlyMethods(['canShare', 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) ->getMock(); $shareOwner = $this->createMock(IUser::class); @@ -3019,6 +3032,10 @@ public function testCreateShareOfIncomingFederatedShare(): void { $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); + $manager->expects($this->once()) + ->method('validateExpirationDateInternal') + ->with($share) + ->willReturnArgument(0); $this->defaultProvider ->expects($this->once())