Skip to content

Commit

Permalink
Add new Public Link Sharing
Browse files Browse the repository at this point in the history
Signed-off-by: Jonas Rittershofer <[email protected]>
  • Loading branch information
jotoeri committed Jan 17, 2022
1 parent 7d4a1de commit 816541a
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 34 deletions.
9 changes: 8 additions & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@

return [
'routes' => [
// Public Share Link
[
'name' => 'page#public_link_view',
'url' => '/s/{hash}',
'verb' => 'GET'

],
// Internal views
[
'name' => 'page#views',
Expand All @@ -33,7 +40,7 @@
],
// Share-Link & public submit
[
'name' => 'page#goto_form',
'name' => 'page#internal_link_view',
'url' => '/{hash}',
'verb' => 'GET'
],
Expand Down
2 changes: 1 addition & 1 deletion lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
54 changes: 46 additions & 8 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,7 +59,10 @@ class PageController extends Controller {

/** @var FormMapper */
private $formMapper;


/** @var ShareMapper */
private $shareMapper;

/** @var FormsService */
private $formsService;

Expand Down Expand Up @@ -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,
Expand All @@ -118,6 +123,7 @@ public function __construct(string $appName,
$this->appName = $appName;

$this->formMapper = $formMapper;
$this->shareMapper = $shareMapper;
$this->formsService = $formsService;

$this->accountManager = $accountManager;
Expand Down Expand Up @@ -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');

Expand All @@ -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);
Expand Down
24 changes: 20 additions & 4 deletions lib/Controller/ShareApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Expand All @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
23 changes: 23 additions & 0 deletions lib/Db/ShareMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 27 additions & 5 deletions lib/Service/FormsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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
*
Expand All @@ -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;
}

Expand Down
12 changes: 10 additions & 2 deletions src/components/SidebarTabs/SettingsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@
<script>
import { CheckboxRadioSwitch, DatetimePicker } from '@nextcloud/vue'
import moment from '@nextcloud/moment'
import ShareTypes from '../../mixins/ShareTypes'
export default {
components: {
CheckboxRadioSwitch,
DatetimePicker,
},
mixins: [ShareTypes],
props: {
form: {
type: Object,
Expand All @@ -92,14 +95,19 @@ export default {
* Submit Multiple is disabled, if it cannot be controlled.
*/
disableSubmitMultiple() {
return !(this.form.access.requireLogin || this.form.access.restrictSelected) || this.form.isAnonymous
return this.hasPublicLink || this.form.access.legacyLink || this.form.isAnonymous
},
disableSubmitMultipleExplanation() {
if (this.disableSubmitMultiple) {
return t('forms', 'WORDING - Blocked by requireLogin, restrictSelected, isAnonymous')
return t('forms', 'WORDING - Blocked by public link or isAnonymous')
}
return ''
},
hasPublicLink() {
return this.form.shares.slice()
.filter(share => share.shareType === this.SHARE_TYPES.SHARE_TYPE_LINK)
.length !== 0
},
// Inverting submitOnce for UI here. Adapt downto Db for V3, if this imposes for longterm.
submitMultiple() {
Expand Down
Loading

0 comments on commit 816541a

Please sign in to comment.