Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/API_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,30 @@ Update a single or all properties of an option-object
"data": 5
```

### Update a Public Share Token

- Endpoint: `/api/v3/forms/{formId}/shares/{shareId}/token`
- Method: `PATCH`
- Url-Parameters:
| Parameter | Type | Description |
|-----------|---------|-------------|
| _formId_ | Integer | ID of the form containing the share |
| _shareId_ | Integer | ID of the public link share to update |
- Parameters:
| Parameter | Type | Description |
|-----------|---------|-------------|
| _token_ | String | New token for the public share link |
- Restrictions:
- Only available when the admin setting _allowCustomPublicShareTokens_ is enabled.
- Only link shares can be updated.
- Token must be unique among link shares and only contain alphanumeric characters.
- Token length must be between 8 and 256 characters.
- Response: **Status-Code OK**, as well as the id of the updated share.

```
"data": 5
```

Comment thread
Chartman123 marked this conversation as resolved.
Outdated
## Submission Endpoints

### Get Form Submissions
Expand Down
5 changes: 5 additions & 0 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,22 @@ class Constants {
*/
public const CONFIG_KEY_ALLOWPERMITALL = 'allowPermitAll';
public const CONFIG_KEY_ALLOWPUBLICLINK = 'allowPublicLink';
public const CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN = 'allowCustomPublicShareTokens';
public const CONFIG_KEY_ALLOWSHOWTOALL = 'allowShowToAll';
public const CONFIG_KEY_CREATIONALLOWEDGROUPS = 'creationAllowedGroups';
public const CONFIG_KEY_RESTRICTCREATION = 'restrictCreation';
public const CONFIG_KEYS = [
self::CONFIG_KEY_ALLOWPERMITALL,
self::CONFIG_KEY_ALLOWPUBLICLINK,
self::CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN,
self::CONFIG_KEY_ALLOWSHOWTOALL,
self::CONFIG_KEY_CREATIONALLOWEDGROUPS,
self::CONFIG_KEY_RESTRICTCREATION
];

public const PUBLIC_SHARE_TOKEN_MIN_LENGTH = 8;
public const PUBLIC_SHARE_TOKEN_MAX_LENGTH = 256;

/**
* Maximum String lengths, the database is set to store.
*/
Expand Down
3 changes: 2 additions & 1 deletion lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class PageController extends Controller {
private const TEMPLATE_MAIN = 'main';
private const PUBLIC_SHARE_HASH_REQUIREMENT = '[a-zA-Z0-9]{8,256}';
Comment thread
Chartman123 marked this conversation as resolved.
Outdated

public function __construct(
string $appName,
Expand Down Expand Up @@ -145,7 +146,7 @@ public function internalLinkView(string $hash): Response {
#[NoAdminRequired()]
#[NoCSRFRequired()]
#[PublicPage()]
#[FrontpageRoute(verb: 'GET', url: '/s/{hash}', requirements: ['hash' => '[a-zA-Z0-9]{24,}'])]
#[FrontpageRoute(verb: 'GET', url: '/s/{hash}', requirements: ['hash' => self::PUBLIC_SHARE_HASH_REQUIREMENT])]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would probably be good if we can decide here wether custom share tokens are allowed on that instance. But IIRC it's not working with dynamically defined requirements.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meaning it would be good, but can't be done in this instance? Or should I try?

public function publicLinkView(string $hash): Response {
try {
$share = $this->shareMapper->findPublicShareByHash($hash);
Expand Down
97 changes: 97 additions & 0 deletions lib/Controller/ShareApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,85 @@ public function updateShare(int $formId, int $shareId, array $keyValuePairs): Da
return new DataResponse($formShare->getId());
}

/**
* Update token/hash of a public link share
*
* @param int $formId of the form
* @param int $shareId of the share to update
* @param string $token The new share token
* @return DataResponse<Http::STATUS_OK, int, array{}>
* @throws OCSBadRequestException Share doesn't belong to given Form
* @throws OCSBadRequestException Invalid share token
* @throws OCSBadRequestException Share hash exists, please retry
* @throws OCSForbiddenException Custom public share tokens are not allowed
* @throws OCSForbiddenException Not allowed to update token on non-link share
* @throws OCSForbiddenException This form is not owned by the current user
* @throws OCSNotFoundException Could not find share
*
* 200: the id of the updated share
*/
#[CORS()]
#[NoAdminRequired()]
#[ApiRoute(verb: 'PATCH', url: '/api/v3/forms/{formId}/shares/{shareId}/token')]
public function updateShareToken(int $formId, int $shareId, string $token): DataResponse {
Comment thread
Chartman123 marked this conversation as resolved.
Outdated
$this->logger->debug('Updating share token: {shareId} of form {formId}', [
'formId' => $formId,
'shareId' => $shareId,
]);

if (!$this->configService->getAllowCustomPublicToken()) {
$this->logger->debug('Custom public share tokens are not allowed.');
throw new OCSForbiddenException('Custom public share tokens are not allowed.');
}

$form = $this->formsService->getFormIfAllowed($formId);
if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException('This form is archived and can not be modified');
}

try {
$formShare = $this->shareMapper->findById($shareId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find share', ['exception' => $e]);
throw new OCSNotFoundException('Could not find share');
}

if ($formId !== $formShare->getFormId()) {
$this->logger->debug('This share doesn\'t belong to the given Form');
throw new OCSBadRequestException('Share doesn\'t belong to given Form');
}

if ($formShare->getShareType() !== IShare::TYPE_LINK) {
$this->logger->debug('Not allowed to update token on non-link share');
throw new OCSForbiddenException('Not allowed to update token on non-link share');
}

if ($token === $formShare->getShareWith()) {
return new DataResponse($formShare->getId());
}

$this->validatePublicShareToken($token);

try {
$existingShare = $this->shareMapper->findPublicShareByHash($token);
if ($existingShare->getId() !== $formShare->getId()) {
$this->logger->debug('Share hash already exists.');
throw new OCSBadRequestException('Share hash exists, please retry.');
}
} catch (DoesNotExistException $e) {
// Just continue, this is what we expect to happen (share hash not existing yet).
}

$this->formsService->obtainFormLock($form);

$formShare->setShareWith($token);
$formShare = $this->shareMapper->update($formShare);
$this->formMapper->update($form);

return new DataResponse($formShare->getId());
}

/**
* Delete a share
*
Expand Down Expand Up @@ -421,4 +500,22 @@ private function validatePermissions(array $permissions, int $shareType): bool {
}
return true;
}

/**
* @throws OCSBadRequestException If token does not satisfy basic safety checks
*/
private function validatePublicShareToken(string $token): void {
if ($token !== trim($token)) {
throw new OCSBadRequestException('Invalid share token');
}

$tokenLength = strlen($token);
if ($tokenLength < Constants::PUBLIC_SHARE_TOKEN_MIN_LENGTH || $tokenLength > Constants::PUBLIC_SHARE_TOKEN_MAX_LENGTH) {
throw new OCSBadRequestException('Invalid share token');
}

if (preg_match('/^[a-zA-Z0-9]+$/', $token) !== 1) {
throw new OCSBadRequestException('Invalid share token');
}
}
}
6 changes: 5 additions & 1 deletion lib/Service/ConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ public function getAllowPermitAll(): bool {
public function getAllowPublicLink(): bool {
return json_decode($this->config->getAppValue($this->appName, Constants::CONFIG_KEY_ALLOWPUBLICLINK, 'true'));
}
public function getAllowShowToAll() : bool {
public function getAllowCustomPublicToken(): bool {
return json_decode($this->config->getAppValue($this->appName, Constants::CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN, 'false'));
}
public function getAllowShowToAll(): bool {
return json_decode($this->config->getAppValue($this->appName, Constants::CONFIG_KEY_ALLOWSHOWTOALL, 'true'));
}
private function getUnformattedCreationAllowedGroups(): array {
Expand All @@ -57,6 +60,7 @@ public function getAppConfig(): array {
return [
Constants::CONFIG_KEY_ALLOWPERMITALL => $this->getAllowPermitAll(),
Constants::CONFIG_KEY_ALLOWPUBLICLINK => $this->getAllowPublicLink(),
Constants::CONFIG_KEY_ALLOWCUSTOMPUBLICTOKEN => $this->getAllowCustomPublicToken(),
Constants::CONFIG_KEY_ALLOWSHOWTOALL => $this->getAllowShowToAll(),
Constants::CONFIG_KEY_CREATIONALLOWEDGROUPS => $this->getCreationAllowedGroups(),
Constants::CONFIG_KEY_RESTRICTCREATION => $this->getRestrictCreation(),
Expand Down
Loading
Loading