diff --git a/appinfo/routes.php b/appinfo/routes.php index 0c5bbb03b..ec9ccab69 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -25,6 +25,13 @@ return [ 'routes' => [ + // Public Share Link + [ + 'name' => 'page#public_link_view', + 'url' => '/s/{hash}', + 'verb' => 'GET' + + ], // Internal views [ 'name' => 'page#views', @@ -33,7 +40,7 @@ ], // Share-Link & public submit [ - 'name' => 'page#goto_form', + 'name' => 'page#internal_link_view', 'url' => '/{hash}', 'verb' => 'GET' ], diff --git a/lib/Constants.php b/lib/Constants.php index a16e6fefe..cfc52735b 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -45,5 +45,5 @@ class Constants { /** * !! Keep in sync with src/mixins/ShareTypes.js !! */ - public const SHARE_TYPES_USED = [IShare::TYPE_USER, IShare::TYPE_GROUP]; + public const SHARE_TYPES_USED = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]; } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index f3ef502fb..008da85f3 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -28,6 +28,7 @@ use OCA\Forms\Db\Form; use OCA\Forms\Db\FormMapper; +use OCA\Forms\Db\ShareMapper; use OCA\Forms\Service\FormsService; use OCP\Accounts\IAccountManager; @@ -58,7 +59,10 @@ class PageController extends Controller { /** @var FormMapper */ private $formMapper; - + + /** @var ShareMapper */ + private $shareMapper; + /** @var FormsService */ private $formsService; @@ -104,6 +108,7 @@ class PageController extends Controller { public function __construct(string $appName, IRequest $request, FormMapper $formMapper, + ShareMapper $shareMapper, FormsService $formsService, IAccountManager $accountManager, IGroupManager $groupManager, @@ -118,6 +123,7 @@ public function __construct(string $appName, $this->appName = $appName; $this->formMapper = $formMapper; + $this->shareMapper = $shareMapper; $this->formsService = $formsService; $this->accountManager = $accountManager; @@ -162,7 +168,7 @@ public function views(): TemplateResponse { * @param string $hash * @return RedirectResponse|TemplateResponse Redirect for logged-in users, public template otherwise. */ - public function gotoForm(string $hash): Response { + public function internalLinkView(string $hash): Response { // Inject style on all templates Util::addStyle($this->appName, 'forms'); @@ -172,18 +178,50 @@ public function gotoForm(string $hash): Response { return $this->provideTemplate(self::TEMPLATE_NOTFOUND); } - // If there is no logged in user, and we don't need legacyLink-Support, then on this route, login is necessary. - if (!$this->userSession->isLoggedIn() && !$form->getAccess()['legacyLink']) { - // Redirect to login + // If there is no logged in user, and we need legacyLink-Support, show public page. + if (!$this->userSession->isLoggedIn() && $form->getAccess()['legacyLink']) { + // Has form expired + if ($this->formsService->hasFormExpired($form->getId())) { + return $this->provideTemplate(self::TEMPLATE_EXPIRED, $form); + } + + // Main Template to fill the form + Util::addScript($this->appName, 'forms-submit'); + $this->insertHeaderOnIos(); + $this->initialStateService->provideInitialState($this->appName, 'form', $this->formsService->getPublicForm($form->getId())); + $this->initialStateService->provideInitialState($this->appName, 'isLoggedIn', $this->userSession->isLoggedIn()); + $this->initialStateService->provideInitialState($this->appName, 'maxStringLengths', $this->maxStringLengths); + return $this->provideTemplate(self::TEMPLATE_MAIN, $form); + } + + + + + // Redirect to login // TODO Change to internal submit-view again, once this is possible for forms that are not shownToAll // $internalLink = $this->urlGenerator->linkToRoute('forms.page.views', ['hash' => $hash, 'view' => 'submit']); $internalLink = $this->urlGenerator->linkToRoute('forms.page.goto_form', ['hash' => $hash]); return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', ['redirect_url' => $internalLink])); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @param string $hash Public sharing hash. + * @return TemplateResponse Public template. + */ + public function publicLinkView(string $hash): Response { + // Inject style on all templates + Util::addStyle($this->appName, 'forms'); + + try { + $share = $this->shareMapper->findPublicShareByHash($hash); + $form = $this->formMapper->findById($share->getFormId()); + } catch (DoesNotExistException $e) { + return $this->provideTemplate(self::TEMPLATE_NOTFOUND); } - /** - * Show the public link template (independent of logged in or not) - */ // Has form expired if ($this->formsService->hasFormExpired($form->getId())) { return $this->provideTemplate(self::TEMPLATE_EXPIRED, $form); diff --git a/lib/Controller/ShareApiController.php b/lib/Controller/ShareApiController.php index a92da2dfb..06f0eff60 100644 --- a/lib/Controller/ShareApiController.php +++ b/lib/Controller/ShareApiController.php @@ -42,6 +42,8 @@ use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\Share\IShare; class ShareApiController extends OCSController { protected $appName; @@ -61,19 +63,24 @@ class ShareApiController extends OCSController { /** @var IUser */ private $currentUser; + /** @var ISecureRandom */ + private $secureRandom; + public function __construct(string $appName, FormMapper $formMapper, ShareMapper $shareMapper, FormsService $formsService, ILogger $logger, IRequest $request, - IUserSession $userSession) { + IUserSession $userSession, + ISecureRandom $secureRandom) { parent::__construct($appName, $request); $this->appName = $appName; $this->formMapper = $formMapper; $this->shareMapper = $shareMapper; $this->formsService = $formsService; $this->logger = $logger; + $this->secureRandom = $secureRandom; $this->currentUser = $userSession->getUser(); } @@ -85,12 +92,12 @@ public function __construct(string $appName, * * @param int $formId The form to share * @param int $shareType Nextcloud-ShareType - * @param string $shareWith ID of user/group/... to share with + * @param string $shareWith ID of user/group/... to share with. For Empty shareWith and shareType Link, this will be set as RandomID. * @return DataResponse * @throws OCSBadRequestException * @throws OCSForbiddenException */ - public function newShare(int $formId, int $shareType, string $shareWith): DataResponse { + public function newShare(int $formId, int $shareType, string $shareWith = ''): DataResponse { $this->logger->debug('Adding new share: formId: {formId}, shareType: {shareType}, shareWith: {shareWith}', [ 'formId' => $formId, 'shareType' => $shareType, @@ -119,7 +126,16 @@ public function newShare(int $formId, int $shareType, string $shareWith): DataRe $share->setFormId($formId); $share->setShareType($shareType); - $share->setShareWith($shareWith); + + if ($shareType === IShare::TYPE_LINK && $shareWith === '') { + // TODO Check if hash already exists. (Unfortunately not possible with unique index on db.) + $share->setShareWith($this->secureRandom->generate( + 24, + ISecureRandom::CHAR_HUMAN_READABLE + )); + } else { + $share->setShareWith($shareWith); + } $share = $this->shareMapper->insert($share); diff --git a/lib/Db/ShareMapper.php b/lib/Db/ShareMapper.php index f2a0bd4cd..6476d8506 100644 --- a/lib/Db/ShareMapper.php +++ b/lib/Db/ShareMapper.php @@ -31,6 +31,7 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\Share\IShare; class ShareMapper extends QBMapper { /** @@ -79,6 +80,28 @@ public function findByForm(int $formId): array { return $this->findEntities($qb); } + /** + * Find Public Share by Hash + * @param string $hash + * @return Share + * @throws MultipleObjectsReturnedException if more than one result + * @throws DoesNotExistException if not found + */ + public function findPublicShareByHash(string $hash): Share { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $qb->expr()->eq('share_With', $qb->createNamedParameter($hash, IQueryBuilder::PARAM_STR)) + ); + + return $this->findEntity($qb); + } + /** * Delete a share * @param int $id of the share. diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php index 9f3f9c927..1af9c566d 100644 --- a/lib/Service/FormsService.php +++ b/lib/Service/FormsService.php @@ -205,11 +205,9 @@ public function getPublicForm(int $id): array { */ public function canSubmit($formId) { $form = $this->formMapper->findById($formId); - $access = $form->getAccess(); - // TODO - // We cannot control how many time users can submit in public mode / legacyLink - if (isset($access['legacyLink'])) { + // We cannot control how many time users can submit if public link / legacyLink available + if ($this->hasPublicLink($formId)) { return true; } @@ -231,6 +229,30 @@ public function canSubmit($formId) { return true; } + /** + * Searching Shares for public link + * + * @param integer $formId + * @return boolean + */ + public function hasPublicLink($formId): bool { + $form = $this->formMapper->findById($formId); + $access = $form->getAccess(); + + if (isset($access['legacyLink'])) { + return true; + } + + $shareEntities = $this->shareMapper->findByForm($form->getId()); + foreach ($shareEntities as $shareEntity) { + if ($shareEntity->getShareType() === IShare::TYPE_LINK) { + return true; + } + } + + return false; + } + /** * Check if user has access to this form * @@ -243,7 +265,7 @@ public function hasUserAccess(int $formId): bool { $ownerId = $form->getOwnerId(); // TODO check public access again - if (isset($access['legacyLink'])) { + if ($this->hasPublicLink($formId)) { return true; } diff --git a/src/components/SidebarTabs/SettingsSidebarTab.vue b/src/components/SidebarTabs/SettingsSidebarTab.vue index 64e900a62..1233319c2 100644 --- a/src/components/SidebarTabs/SettingsSidebarTab.vue +++ b/src/components/SidebarTabs/SettingsSidebarTab.vue @@ -64,6 +64,7 @@