From 1a034174d209f70cb474182b93d8562719610851 Mon Sep 17 00:00:00 2001 From: Pranam Lashkari Date: Mon, 24 Nov 2025 18:34:02 +0000 Subject: [PATCH] follow me slideshow: use wopi check file info for presentation leader to communicate more securely who is leading the presentation, communicate the leader via check file info, currently we restrict file owner to be the presentation leader Signed-off-by: Pranam Lashkari --- lib/Controller/DocumentController.php | 9 +++++---- lib/Controller/WopiController.php | 15 +++++++++++++++ lib/Db/Wopi.php | 11 +++++++++++ lib/Db/WopiMapper.php | 19 +++++++++++++++++-- lib/TokenManager.php | 4 ++-- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php index 9e4b80195..503f86b6a 100644 --- a/lib/Controller/DocumentController.php +++ b/lib/Controller/DocumentController.php @@ -395,7 +395,8 @@ class DocumentController extends Controller { $file = $this->getFileForUser($fileId); $this->session->set(self::SESSION_FILE_TARGET, [ - 'fileId' => $file->getId() + 'fileId' => $file->getId(), + 'PresentationLeader' => $file->getFileInfo()->getOwner()->getUid() ]); $filePath = $file->getPath(); @@ -425,7 +426,7 @@ class DocumentController extends Controller { } $isGuest = $guestName || !$this->userId; - $wopi = $this->getToken($file, $share, null, $isGuest); + $wopi = $this->getToken($file, $share, null, $isGuest, presentationLeader: $this->session->get(self::SESSION_FILE_TARGET)['PresentationLeader']); $this->tokenManager->setGuestName($wopi, $guestName); @@ -520,7 +521,7 @@ class DocumentController extends Controller { throw new NotFoundException(); } - private function getToken(File $file, ?IShare $share = null, ?int $version = null, bool $isGuest = false): Wopi { + private function getToken(File $file, ?IShare $share = null, ?int $version = null, bool $isGuest = false, ?string $presentationLeader = null): Wopi { // Pass through $version $templateFile = $this->templateManager->getTemplateSource($file->getId()); if ($templateFile) { @@ -540,7 +541,7 @@ class DocumentController extends Controller { } - return $this->tokenManager->generateWopiToken($this->getWopiFileId($file->getId(), $version), $share?->getToken(), $this->userId); + return $this->tokenManager->generateWopiToken($this->getWopiFileId($file->getId(), $version), $share?->getToken(), $this->userId, presentationLeader: $presentationLeader); } private function getWopiFileId(int $fileId, ?int $version = null): string { diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index 656dc139e..9ce76544a 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -64,6 +64,8 @@ use OCP\Share\IShare; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; +use OCP\ICacheFactory; +use OCP\ICache; #[RestrictToWopiServer] class WopiController extends Controller { @@ -96,6 +98,7 @@ class WopiController extends Controller { private SettingsService $settingsService, private CapabilitiesService $capabilitiesService, private Helper $helper, + private ICacheFactory $cacheFactory, ) { parent::__construct($appName, $request); } @@ -149,6 +152,17 @@ class WopiController extends Controller { $userId = !$isPublic ? $wopi->getEditorUid() : $guestUserId; + $cache = $this->cacheFactory->createLocal('richdocuments_wopi'); + $extraJson = $cache->get('wopi_extra_' . $wopi->getToken()); + if ($extraJson !== false && $extraJson !== '') { + $decoded = json_decode($extraJson, true); + if (is_array($decoded) && isset($decoded['PresentationLeader'])) { + $wopi->setPresentationLeader($decoded['PresentationLeader']); + } + $cache->remove('wopi_extra_' . $wopi->getToken()); + } + + $response = [ 'BaseFileName' => $file->getName(), 'Size' => $file->getSize(), @@ -181,6 +195,7 @@ class WopiController extends Controller { 'EnableRemoteAIContent' => $isTaskProcessingEnabled, 'HasContentRange' => true, 'ServerPrivateInfo' => [], + 'PresentationLeader' => $wopi->getPresentationLeader() ]; if ($this->capabilitiesService->hasSettingIframeSupport()) { diff --git a/lib/Db/Wopi.php b/lib/Db/Wopi.php index 82aaf25ef..f28428ac6 100644 --- a/lib/Db/Wopi.php +++ b/lib/Db/Wopi.php @@ -124,6 +124,8 @@ class Wopi extends Entity implements \JsonSerializable { /** @var int */ protected $tokenType = 0; + protected ?string $presentationLeader = null; + public function __construct() { $this->addType('ownerUid', Types::STRING); $this->addType('editorUid', Types::STRING); @@ -139,6 +141,7 @@ class Wopi extends Entity implements \JsonSerializable { $this->addType('hideDownload', Types::BOOLEAN); $this->addType('direct', Types::BOOLEAN); $this->addType('tokenType', Types::INTEGER); + $this->addType('presentationLeader', Types::STRING); } public function hasTemplateId() { @@ -168,6 +171,14 @@ class Wopi extends Entity implements \JsonSerializable { return (bool)$this->direct; } + public function setPresentationLeader(?string $val): void { + $this->presentationLeader = $val; + } + + public function getPresentationLeader(): ?string { + return $this->presentationLeader; + } + #[\ReturnTypeWillChange] public function jsonSerialize() { $properties = get_object_vars($this); diff --git a/lib/Db/WopiMapper.php b/lib/Db/WopiMapper.php index 0ed77392a..05688ffab 100644 --- a/lib/Db/WopiMapper.php +++ b/lib/Db/WopiMapper.php @@ -14,6 +14,8 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; +use OCP\ICacheFactory; +use OCP\ICache; /** @template-extends QBMapper */ class WopiMapper extends QBMapper { @@ -23,6 +25,7 @@ class WopiMapper extends QBMapper { private LoggerInterface $logger, private ITimeFactory $timeFactory, private AppConfig $appConfig, + private ICacheFactory $cacheFactory, ) { parent::__construct($db, 'richdocuments_wopi', Wopi::class); } @@ -38,7 +41,7 @@ class WopiMapper extends QBMapper { * @param int $templateDestination * @return Wopi */ - public function generateFileToken($fileId, $owner, $editor, $version, $updatable, $serverHost, ?string $guestDisplayname = null, $hideDownload = false, $direct = false, $templateId = 0, $share = null) { + public function generateFileToken($fileId, $owner, $editor, $version, $updatable, $serverHost, ?string $guestDisplayname = null, $hideDownload = false, $direct = false, $templateId = 0, $share = null, ?string $presentationLeader = null) { $token = $this->random->generate(32, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); $wopi = Wopi::fromParams([ @@ -57,9 +60,21 @@ class WopiMapper extends QBMapper { 'remoteServer' => '', 'remoteServerToken' => '', 'share' => $share, - 'tokenType' => $guestDisplayname === null ? Wopi::TOKEN_TYPE_USER : Wopi::TOKEN_TYPE_GUEST + 'tokenType' => $guestDisplayname === null ? Wopi::TOKEN_TYPE_USER : Wopi::TOKEN_TYPE_GUEST, + 'presentationLeader' => $presentationLeader ]); + // store transient value in memcache keyed by token with TTL = token expiry - now + if ($presentationLeader !== null) { + /** @var ICache $cache */ + $cache = $this->cacheFactory->createLocal('richdocuments_wopi'); + $ttl = $this->calculateNewTokenExpiry() - $this->timeFactory->getTime(); + if ($ttl <= 0) { + $ttl = 60; + } + $cache->set('wopi_extra_' . $token, json_encode(['PresentationLeader' => $presentationLeader]), (int)$ttl); + } + /** @var Wopi $wopi */ $wopi = $this->insert($wopi); diff --git a/lib/TokenManager.php b/lib/TokenManager.php index b2d34749f..404d5ab1c 100644 --- a/lib/TokenManager.php +++ b/lib/TokenManager.php @@ -45,7 +45,7 @@ class TokenManager { /** * @throws Exception */ - public function generateWopiToken(string $fileId, ?string $shareToken = null, ?string $editoruid = null, bool $direct = false): Wopi { + public function generateWopiToken(string $fileId, ?string $shareToken = null, ?string $editoruid = null, bool $direct = false, ?string $presentationLeader = null): Wopi { [$fileId, , $version] = Helper::parseFileId($fileId); $owneruid = null; $hideDownload = false; @@ -121,7 +121,7 @@ class TokenManager { $serverHost = $this->urlGenerator->getAbsoluteURL('/'); $guestName = $editoruid === null ? $this->prepareGuestName($this->helper->getGuestNameFromCookie()) : null; - return $this->wopiMapper->generateFileToken($fileId, $owneruid, $editoruid, $version, $updatable, $serverHost, $guestName, $hideDownload, $direct, 0, $shareToken); + return $this->wopiMapper->generateFileToken($fileId, $owneruid, $editoruid, $version, $updatable, $serverHost, $guestName, $hideDownload, $direct, 0, $shareToken, $presentationLeader); } /**