Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
10 changes: 9 additions & 1 deletion docs/API_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Returns condensed objects of all Forms beeing owned by the authenticated user.
"hash": "yWeMwcwCwoqRs8T2",
"title": "Form 2",
"expires": 0,
"opens": 0,
"permissions": [
"edit",
"results",
Expand All @@ -103,6 +104,7 @@ Returns condensed objects of all Forms beeing owned by the authenticated user.
"hash": "em4djk8B9BpXnkYG",
"title": "Form 1",
"expires": 0,
"opens": 0,
"permissions": [
"edit",
"results",
Expand Down Expand Up @@ -151,6 +153,7 @@ Returns the full-depth object of the requested form (without submissions).
|-----------|---------|-------------|
| _formId_ | Integer | ID of the form to request |
- Response: A full object of the form, including access, questions and options in full depth.
Before the opening date (`opens`) of the form, `questions` is empty for users who can neither edit the form nor see its results.

```
"data": {
Expand All @@ -166,6 +169,7 @@ Returns the full-depth object of the requested form (without submissions).
"showToAllUsers": false
},
"expires": 0,
"opens": 0,
"fileFormat": "csv",
"fileId": 157,
"filePath": "foo/bar",
Expand Down Expand Up @@ -281,6 +285,8 @@ Update a single or multiple properties of a form-object. Concerns **only** the F
- To transfer the ownership of a form to another user, you must only send a single _keyValuePair_ containing the key `ownerId` and the user id of the new owner.
- To link a file for submissions, the _keyValuePairs_ need to contain the keys `path` and `fileFormat`
- To unlink a file for submissions, the _keyValuePairs_ need to contain the keys `fileId` and `fileFormat` need to contain the value `null`
- `opens` and `expires` must be non-negative unix timestamps, `0` means not set
- If both `opens` and `expires` are set (not `0`), `opens` must be before `expires`
Comment thread
susnux marked this conversation as resolved.
Resolved
- Response: **Status-Code OK**, as well as the id of the updated form.

```
Expand Down Expand Up @@ -314,6 +320,7 @@ Returns the questions and options of the given form (without submissions).
|-----------|---------|-------------|
| _formId_ | Integer | ID of the form |
- Response: An array of all questions of the form including options.
- Before the opening date (`opens`) of the form, users who can neither edit the form nor see its results get a **Forbidden** response.

```
"data": [
Expand Down Expand Up @@ -398,6 +405,7 @@ Returns the requested question and options of the given form (without submission
|-----------|---------|-------------|
| _formId_ | Integer | ID of the form |
| _questionId_ | Integer | ID of the question |
- Before the opening date (`opens`) of the form, users who can neither edit the form nor see its results get a **Forbidden** response.
- Response: An object of the requested question including options.

```
Expand Down Expand Up @@ -1024,7 +1032,7 @@ This returns in case the Request is not properly set. This can e.g. include:
This returns in case the authenticated user is not allowed to access this resource/endpoint. This can e.g. include:

- The user has no write access to the form (only form owner is allowed to edit)
- The user is not allowed to submit to the form (access-settings, form expired, already submitted)
- The user is not allowed to submit to the form (access-settings, form not yet open or expired, already submitted)

```
"ocs": {
Expand Down
2 changes: 2 additions & 0 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This document describes the Object-Structure, that is used within the Forms App
| created | unix timestamp | | When the form has been created |
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
| opens | unix-timestamp | | From when the form accepts responses. Timestamp `0` indicates _immediately_ |
| isAnonymous | Boolean | | If Answers will be stored anonymously |
| state | Integer | [Form state](#form-state) | The state of the form |
| lockedBy | String | | The user ID for who has exclusive edit access at the moment |
Expand Down Expand Up @@ -57,6 +58,7 @@ This document describes the Object-Structure, that is used within the Forms App
"created": 1611240961,
"access": {},
"expires": 0,
"opens": 0,
"isAnonymous": false,
"submitMultiple": true,
"allowEditSubmissions": false,
Expand Down
1 change: 1 addition & 0 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ class Constants {
*/
public const EMPTY_EXPIRED = 'expired';
public const EMPTY_NOTFOUND = 'notfound';
public const EMPTY_NOTYETOPEN = 'notyetopen';

/**
* Constants related to extra settings for questions
Expand Down
56 changes: 56 additions & 0 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ public function newForm(?int $fromId = null): DataResponse {
$form->setAllowEditSubmissions(false);
$form->setShowExpiration(false);
$form->setExpires(0);
$form->setOpens(0);
$form->setIsAnonymous(false);

$this->formMapper->insert($form);
Expand Down Expand Up @@ -212,6 +213,7 @@ public function newForm(?int $fromId = null): DataResponse {
$formData['allowEditSubmissions'] = false;
$formData['showExpiration'] = false;
$formData['expires'] = 0;
$formData['opens'] = 0;
$formData['isAnonymous'] = false;

$form = Form::fromParams($formData);
Expand Down Expand Up @@ -282,6 +284,7 @@ public function getForm(int $formId): DataResponse {
* @param int $formId FormId of form to update
* @param array<string, mixed> $keyValuePairs Array of key=>value pairs to update.
* @return DataResponse<Http::STATUS_OK, int|string, array{}>
* @throws OCSBadRequestException Invalid opening or expiration date, or opening date not before expiration date
* @throws OCSBadRequestException Could not find new form owner
* @throws OCSForbiddenException Empty keyValuePairs provided
* @throws OCSForbiddenException Not allowed to update id, hash, created, fileId or lastUpdated. OwnerId only allowed if no other key provided.
Expand Down Expand Up @@ -336,6 +339,8 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
// Do not allow changing showToAllUsers or permitAllUsers if disabled
$this->checkAccessUpdate($keyValuePairs);

$this->checkDatesUpdate($form, $keyValuePairs);

$previousAccess = $form->getAccess();
$previousState = $form->getState();
$previousSettings = $this->getResponseSettings($form);
Expand Down Expand Up @@ -429,6 +434,7 @@ public function deleteForm(int $formId): DataResponse {
*
* @param int $formId the form id
* @return DataResponse<Http::STATUS_OK, list<FormsQuestion>, array{}>
* @throws OCSForbiddenException Form is not yet open
* @throws OCSForbiddenException User has no permissions to get this form
* @throws OCSNotFoundException Could not find form
*
Expand All @@ -450,6 +456,11 @@ public function getQuestions(int $formId): DataResponse {
throw new OCSForbiddenException('User has no permissions to get this form');
}

if (!$this->formsService->canSeeQuestions($form)) {
$this->logger->debug('Form is not yet open');
throw new OCSForbiddenException('This form is not yet open');
}

$questionData = $this->formsService->getQuestions($formId);
$questionData = array_map(static function (array $question) {
if (empty($question['extraSettings'])) {
Expand All @@ -468,6 +479,7 @@ public function getQuestions(int $formId): DataResponse {
* @param int $questionId QuestionId
* @return DataResponse<Http::STATUS_OK, FormsQuestion, array{}>
* @throws OCSBadRequestException Question doesn\'t belong to given Form
* @throws OCSForbiddenException Form is not yet open
* @throws OCSForbiddenException User has no permissions to get this form
* @throws OCSNotFoundException Could not find form
*
Expand All @@ -489,6 +501,11 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
throw new OCSForbiddenException('User has no permissions to get this form');
}

if (!$this->formsService->canSeeQuestions($form)) {
$this->logger->debug('Form is not yet open');
throw new OCSForbiddenException('This form is not yet open');
}

$question = $this->formsService->getQuestion($questionId);
if ($question === null) {
throw new OCSNotFoundException('Question doesn\'t exist');
Expand Down Expand Up @@ -1430,6 +1447,7 @@ public function deleteAllSubmissions(int $formId): DataResponse {
* @throws OCSBadRequestException At least one submitted answer is not valid
* @throws OCSForbiddenException Already submitted
* @throws OCSForbiddenException Not allowed to access this form
* @throws OCSForbiddenException This form is not yet taking answers
* @throws OCSForbiddenException This form is no longer taking answers
* @throws OCSForbiddenException This form is not owned by the current user and user has no `results_delete` permission
* @throws OCSNotFoundException Could not find form
Expand Down Expand Up @@ -1522,6 +1540,7 @@ public function newSubmission(int $formId, array $answers, string $shareHash = '
* @param array<string, list<string>> $answers [question_id => arrayOfString]
* @return DataResponse<Http::STATUS_OK, int, array{}>
* @throws OCSBadRequestException Can only update submission if allowEditSubmissions is set and the answers are valid
* @throws OCSForbiddenException This form is not yet taking answers
* @throws OCSForbiddenException Can only update your own submission
*
* 200: the id of the updated submission
Expand Down Expand Up @@ -1707,6 +1726,7 @@ public function exportSubmissionsToCloud(int $formId, string $path, string $file
* @throws OCSBadRequestException Failed to upload the file
* @throws OCSBadRequestException File size exceeds the maximum allowed size
* @throws OCSBadRequestException File type is not allowed
* @throws OCSForbiddenException This form is not yet taking answers
* @throws OCSForbiddenException Already submitted
* @throws OCSNotFoundException Could not find question
*
Expand Down Expand Up @@ -1950,6 +1970,41 @@ private function checkForbiddenKeys(array $keyValuePairs): void {
}
}

/**
* Throws if the opening or expiration date is not a timestamp or the form would not open before it expires.
* Numeric strings are converted to integers.
*
* @param array<string, mixed> $keyValuePairs
*/
private function checkDatesUpdate(Form $form, array &$keyValuePairs): void {
if (!array_key_exists('opens', $keyValuePairs) && !array_key_exists('expires', $keyValuePairs)) {
return;
}

foreach (['opens' => 'opening', 'expires' => 'expiration'] as $key => $name) {
if (!array_key_exists($key, $keyValuePairs)) {
continue;
}

$value = $keyValuePairs[$key];
if (is_string($value) && ctype_digit($value)) {
$value = (int)$value;
}
if (!is_int($value) || $value < 0) {
$this->logger->info("Invalid {$name} date");
throw new OCSBadRequestException("Invalid {$name} date");
}
$keyValuePairs[$key] = $value;
}

$opens = $keyValuePairs['opens'] ?? (int)$form->getOpens();
$expires = $keyValuePairs['expires'] ?? (int)$form->getExpires();
if (!FormsService::areFormDatesValid($opens, $expires)) {
$this->logger->info('Opening date must be before expiration date');
throw new OCSBadRequestException('Opening date must be before expiration date');
}
}

/**
* Throws if fileId is present in update
*/
Expand Down Expand Up @@ -2103,6 +2158,7 @@ private function getResponseSettings(Form $form): array {
'submitMultiple' => $form->getSubmitMultiple(),
'allowEditSubmissions' => $form->getAllowEditSubmissions(),
'expires' => $form->getExpires(),
'opens' => $form->getOpens(),
'maxSubmissions' => $form->getMaxSubmissions(),
];
}
Expand Down
5 changes: 5 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ protected function createPublicSubmitView(Form $form, string $hash): TemplateRes
return $this->provideEmptyContent(Constants::EMPTY_EXPIRED, $form);
}

if (!$this->formsService->hasFormOpened($form)) {
Comment thread
susnux marked this conversation as resolved.
Resolved
$this->initialState->provideInitialState('opens', $form->getOpens());
return $this->provideEmptyContent(Constants::EMPTY_NOTYETOPEN, $form);
}

$this->insertHeaderOnIos();

// Inject style on all templates
Expand Down
6 changes: 6 additions & 0 deletions lib/Db/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
* @method void setCreated(int $value)
* @method int getExpires()
* @method void setExpires(int $value)
* @method int getOpens()
* @method void setOpens(int $value)
* @method int getIsAnonymous()
* @method void setIsAnonymous(bool $value)
* @method int getSubmitMultiple()
Expand Down Expand Up @@ -74,6 +76,7 @@ class Form extends Entity {
protected $accessEnum;
protected $created;
protected $expires;
protected $opens;
protected $isAnonymous;
protected $submitMultiple;
protected $allowEditSubmissions;
Expand All @@ -96,6 +99,7 @@ class Form extends Entity {
public function __construct() {
$this->addType('created', 'integer');
$this->addType('expires', 'integer');
$this->addType('opens', 'integer');
$this->addType('isAnonymous', 'boolean');
$this->addType('submitMultiple', 'boolean');
$this->addType('allowEditSubmissions', 'boolean');
Expand Down Expand Up @@ -172,6 +176,7 @@ public function setAccess(array $access): void {
* created: int,
* access: FormsAccess,
* expires: int,
* opens: int,
* isAnonymous: bool,
* submitMultiple: bool,
* allowEditSubmissions: bool,
Expand Down Expand Up @@ -201,6 +206,7 @@ public function read() {
'created' => $this->getCreated(),
'access' => $this->getAccess(),
'expires' => (int)$this->getExpires(),
'opens' => (int)$this->getOpens(),
'isAnonymous' => (bool)$this->getIsAnonymous(),
'submitMultiple' => (bool)$this->getSubmitMultiple(),
'allowEditSubmissions' => (bool)$this->getAllowEditSubmissions(),
Expand Down
6 changes: 6 additions & 0 deletions lib/FormsMigrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
$form->setOwnerId($user->getUID());
$form->setAccess($formData['access']);
$form->setExpires($formData['expires']);
$opens = (int)($formData['opens'] ?? 0);
if ($opens !== 0 && !FormsService::areFormDatesValid($opens, (int)$formData['expires'])) {
$output->writeln('Ignoring invalid opening date of form "' . $formData['title'] . '"');
$opens = 0;
}
$form->setOpens($opens);
$form->setIsAnonymous($formData['isAnonymous']);
$form->setSubmitMultiple($formData['submitMultiple']);
$form->setAllowEditSubmissions($formData['allowEditSubmissions']);
Expand Down
41 changes: 41 additions & 0 deletions lib/Migration/Version050500Date20261030000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Forms\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

#[AddColumn(table: 'forms_v2_forms', name: 'opens', type: ColumnType::BIGINT, description: 'Timestamp from which a form accepts responses')]
class Version050500Date20261030000000 extends SimpleMigrationStep {
#[Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('forms_v2_forms');

if ($table->hasColumn('opens')) {
return null;
}

$table->addColumn('opens', Types::BIGINT, [
'notnull' => true,
'default' => 0,
'comment' => 'Timestamp from which the form accepts responses, 0 means immediately',
]);

return $schema;
}
}
2 changes: 2 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
* hash: string,
* title: string,
* expires: int,
* opens: int,
* permissions: list<FormsPermission>,
* partial: true,
* state: int,
Expand All @@ -123,6 +124,7 @@
* created: int,
* access: FormsAccess,
* expires: int,
* opens: int,
* fileFormat: ?string,
* fileId: ?int,
* filePath?: ?string,
Expand Down
Loading
Loading