diff --git a/apps/dav/lib/Controller/InvitationResponseController.php b/apps/dav/lib/Controller/InvitationResponseController.php index 425f491adef1c..53f04b3f06040 100644 --- a/apps/dav/lib/Controller/InvitationResponseController.php +++ b/apps/dav/lib/Controller/InvitationResponseController.php @@ -17,6 +17,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\IDBConnection; use OCP\IRequest; +use OCP\IURLGenerator; use Sabre\VObject\ITip\Message; use Sabre\VObject\Reader; @@ -31,6 +32,7 @@ class InvitationResponseController extends Controller { * @param IDBConnection $db * @param ITimeFactory $timeFactory * @param InvitationResponseServer $responseServer + * @param IURLGenerator $urlGenerator */ public function __construct( string $appName, @@ -38,6 +40,7 @@ public function __construct( private IDBConnection $db, private ITimeFactory $timeFactory, private InvitationResponseServer $responseServer, + private IURLGenerator $urlGenerator, ) { parent::__construct($appName, $request); // Don't run `$server->exec()`, because we just need access to the @@ -57,14 +60,13 @@ public function accept(string $token):TemplateResponse { return new TemplateResponse($this->appName, 'schedule-response-error', [], 'guest'); } - $iTipMessage = $this->buildITipResponse($row, 'ACCEPTED'); - $this->responseServer->handleITipMessage($iTipMessage); - if ($iTipMessage->getScheduleStatus() === '1.2') { - return new TemplateResponse($this->appName, 'schedule-response-success', [], 'guest'); - } - - return new TemplateResponse($this->appName, 'schedule-response-error', [ - 'organizer' => $row['organizer'], + // Show confirmation page with ACCEPTED preselected. + // The actual action is only performed via POST (processMoreOptionsResult), + // which prevents email link scanners from triggering accept/decline. + return new TemplateResponse($this->appName, 'schedule-response-options', [ + 'token' => $token, + 'preselect' => 'ACCEPTED', + 'formAction' => $this->urlGenerator->linkToRoute('dav.invitation_response.processMoreOptionsResult', ['token' => $token]), ], 'guest'); } @@ -80,15 +82,10 @@ public function decline(string $token):TemplateResponse { return new TemplateResponse($this->appName, 'schedule-response-error', [], 'guest'); } - $iTipMessage = $this->buildITipResponse($row, 'DECLINED'); - $this->responseServer->handleITipMessage($iTipMessage); - - if ($iTipMessage->getScheduleStatus() === '1.2') { - return new TemplateResponse($this->appName, 'schedule-response-success', [], 'guest'); - } - - return new TemplateResponse($this->appName, 'schedule-response-error', [ - 'organizer' => $row['organizer'], + return new TemplateResponse($this->appName, 'schedule-response-options', [ + 'token' => $token, + 'preselect' => 'DECLINED', + 'formAction' => $this->urlGenerator->linkToRoute('dav.invitation_response.processMoreOptionsResult', ['token' => $token]), ], 'guest'); } @@ -100,7 +97,8 @@ public function decline(string $token):TemplateResponse { #[NoCSRFRequired] public function options(string $token):TemplateResponse { return new TemplateResponse($this->appName, 'schedule-response-options', [ - 'token' => $token + 'token' => $token, + 'formAction' => $this->urlGenerator->linkToRoute('dav.invitation_response.processMoreOptionsResult', ['token' => $token]), ], 'guest'); } diff --git a/apps/dav/templates/schedule-response-options.php b/apps/dav/templates/schedule-response-options.php index 30020cc853576..9e9a4ed46354f 100644 --- a/apps/dav/templates/schedule-response-options.php +++ b/apps/dav/templates/schedule-response-options.php @@ -4,24 +4,32 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ \OCP\Util::addStyle('dav', 'schedule-response'); +$preselect = $_['preselect'] ?? 'ACCEPTED'; +$formAction = $_['formAction']; ?>
-
+

t('Are you accepting the invitation?')); ?>

- + /> - + /> - + /> diff --git a/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php index 3b535ceed22fc..229b3f5ee1ff2 100644 --- a/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php +++ b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php @@ -18,6 +18,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IRequest; +use OCP\IURLGenerator; use PHPUnit\Framework\MockObject\MockObject; use Sabre\VObject\ITip\Message; use Test\TestCase; @@ -27,6 +28,7 @@ class InvitationResponseControllerTest extends TestCase { private IRequest&MockObject $request; private ITimeFactory&MockObject $timeFactory; private InvitationResponseServer&MockObject $responseServer; + private IURLGenerator&MockObject $urlGenerator; private InvitationResponseController $controller; protected function setUp(): void { @@ -36,13 +38,17 @@ protected function setUp(): void { $this->request = $this->createMock(IRequest::class); $this->timeFactory = $this->createMock(ITimeFactory::class); $this->responseServer = $this->createMock(InvitationResponseServer::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->urlGenerator->method('linkToRoute') + ->willReturn('/apps/dav/invitation/moreOptions/TOKEN123'); $this->controller = new InvitationResponseController( 'appName', $this->request, $this->dbConnection, $this->timeFactory, - $this->responseServer + $this->responseServer, + $this->urlGenerator ); } @@ -53,8 +59,7 @@ public static function attendeeProvider(): array { ]; } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'attendeeProvider')] - public function testAccept(bool $isExternalAttendee): void { + public function testAccept(): void { $this->buildQueryExpects('TOKEN123', [ 'id' => 0, 'uid' => 'this-is-the-events-uid', @@ -66,57 +71,17 @@ public function testAccept(bool $isExternalAttendee): void { 'expiration' => 420000, ], 1337); - $expected = <<responseServer->expects($this->once()) - ->method('handleITipMessage') - ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { - $called = true; - $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); - $this->assertEquals('VEVENT', $iTipMessage->component); - $this->assertEquals('REPLY', $iTipMessage->method); - $this->assertEquals(null, $iTipMessage->sequence); - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); - if ($isExternalAttendee) { - $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); - } else { - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); - } - - $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; - - $this->assertEquals($expected, $iTipMessage->message->serialize()); - }); - $this->responseServer->expects($this->once()) - ->method('isExternalAttendee') - ->willReturn($isExternalAttendee); + $this->responseServer->expects($this->never()) + ->method('handleITipMessage'); $response = $this->controller->accept('TOKEN123'); $this->assertInstanceOf(TemplateResponse::class, $response); - $this->assertEquals('schedule-response-success', $response->getTemplateName()); - $this->assertEquals([], $response->getParams()); - $this->assertTrue($called); + $this->assertEquals('schedule-response-options', $response->getTemplateName()); + $this->assertEquals('ACCEPTED', $response->getParams()['preselect']); + $this->assertEquals('TOKEN123', $response->getParams()['token']); } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'attendeeProvider')] - public function testAcceptSequence(bool $isExternalAttendee): void { + public function testAcceptShowsConfirmationPageRegardlessOfSequence(): void { $this->buildQueryExpects('TOKEN123', [ 'id' => 0, 'uid' => 'this-is-the-events-uid', @@ -128,57 +93,16 @@ public function testAcceptSequence(bool $isExternalAttendee): void { 'expiration' => 420000, ], 1337); - $expected = <<responseServer->expects($this->once()) - ->method('handleITipMessage') - ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { - $called = true; - $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); - $this->assertEquals('VEVENT', $iTipMessage->component); - $this->assertEquals('REPLY', $iTipMessage->method); - $this->assertEquals(1337, $iTipMessage->sequence); - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); - if ($isExternalAttendee) { - $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); - } else { - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); - } - - $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; - - $this->assertEquals($expected, $iTipMessage->message->serialize()); - }); - $this->responseServer->expects($this->once()) - ->method('isExternalAttendee') - ->willReturn($isExternalAttendee); + $this->responseServer->expects($this->never()) + ->method('handleITipMessage'); $response = $this->controller->accept('TOKEN123'); $this->assertInstanceOf(TemplateResponse::class, $response); - $this->assertEquals('schedule-response-success', $response->getTemplateName()); - $this->assertEquals([], $response->getParams()); - $this->assertTrue($called); + $this->assertEquals('schedule-response-options', $response->getTemplateName()); + $this->assertEquals('ACCEPTED', $response->getParams()['preselect']); } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'attendeeProvider')] - public function testAcceptRecurrenceId(bool $isExternalAttendee): void { + public function testAcceptShowsConfirmationPageRegardlessOfRecurrenceId(): void { $this->buildQueryExpects('TOKEN123', [ 'id' => 0, 'uid' => 'this-is-the-events-uid', @@ -190,54 +114,13 @@ public function testAcceptRecurrenceId(bool $isExternalAttendee): void { 'expiration' => 420000, ], 1337); - $expected = <<responseServer->expects($this->once()) - ->method('handleITipMessage') - ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { - $called = true; - $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); - $this->assertEquals('VEVENT', $iTipMessage->component); - $this->assertEquals('REPLY', $iTipMessage->method); - $this->assertEquals(0, $iTipMessage->sequence); - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); - if ($isExternalAttendee) { - $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); - } else { - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); - } - - $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; - - $this->assertEquals($expected, $iTipMessage->message->serialize()); - }); - $this->responseServer->expects($this->once()) - ->method('isExternalAttendee') - ->willReturn($isExternalAttendee); + $this->responseServer->expects($this->never()) + ->method('handleITipMessage'); $response = $this->controller->accept('TOKEN123'); $this->assertInstanceOf(TemplateResponse::class, $response); - $this->assertEquals('schedule-response-success', $response->getTemplateName()); - $this->assertEquals([], $response->getParams()); - $this->assertTrue($called); + $this->assertEquals('schedule-response-options', $response->getTemplateName()); + $this->assertEquals('ACCEPTED', $response->getParams()['preselect']); } public function testAcceptTokenNotFound(): void { @@ -267,8 +150,7 @@ public function testAcceptExpiredToken(): void { $this->assertEquals([], $response->getParams()); } - #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'attendeeProvider')] - public function testDecline(bool $isExternalAttendee): void { + public function testDecline(): void { $this->buildQueryExpects('TOKEN123', [ 'id' => 0, 'uid' => 'this-is-the-events-uid', @@ -280,60 +162,22 @@ public function testDecline(bool $isExternalAttendee): void { 'expiration' => 420000, ], 1337); - $expected = <<responseServer->expects($this->once()) - ->method('handleITipMessage') - ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { - $called = true; - $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); - $this->assertEquals('VEVENT', $iTipMessage->component); - $this->assertEquals('REPLY', $iTipMessage->method); - $this->assertEquals(null, $iTipMessage->sequence); - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); - if ($isExternalAttendee) { - $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); - } else { - $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); - } - - $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; - - $this->assertEquals($expected, $iTipMessage->message->serialize()); - }); - $this->responseServer->expects($this->once()) - ->method('isExternalAttendee') - ->willReturn($isExternalAttendee); + $this->responseServer->expects($this->never()) + ->method('handleITipMessage'); $response = $this->controller->decline('TOKEN123'); $this->assertInstanceOf(TemplateResponse::class, $response); - $this->assertEquals('schedule-response-success', $response->getTemplateName()); - $this->assertEquals([], $response->getParams()); - $this->assertTrue($called); + $this->assertEquals('schedule-response-options', $response->getTemplateName()); + $this->assertEquals('DECLINED', $response->getParams()['preselect']); + $this->assertEquals('TOKEN123', $response->getParams()['token']); } public function testOptions(): void { $response = $this->controller->options('TOKEN123'); $this->assertInstanceOf(TemplateResponse::class, $response); $this->assertEquals('schedule-response-options', $response->getTemplateName()); - $this->assertEquals(['token' => 'TOKEN123'], $response->getParams()); + $this->assertEquals('TOKEN123', $response->getParams()['token']); + $this->assertArrayHasKey('formAction', $response->getParams()); } #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'attendeeProvider')]