@@ -190,6 +190,8 @@ public function newForm(?int $fromId = null): DataResponse {
190190 unset($ formData ['state ' ]);
191191 unset($ formData ['fileId ' ]);
192192 unset($ formData ['fileFormat ' ]);
193+ unset($ formData ['locked_by ' ]);
194+ unset($ formData ['locked_until ' ]);
193195 $ formData ['hash ' ] = $ this ->formsService ->generateFormHash ();
194196 // TRANSLATORS Appendix to the form Title of a duplicated/copied form.
195197 $ formData ['title ' ] .= ' - ' . $ this ->l10n ->t ('Copy ' );
@@ -267,7 +269,8 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
267269 'keyValuePairs ' => $ keyValuePairs
268270 ]);
269271
270- $ form = $ this ->getFormIfAllowed ($ formId );
272+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
273+ $ this ->obtainFormLock ($ form );
271274
272275 // Don't allow empty array
273276 if (sizeof ($ keyValuePairs ) === 0 ) {
@@ -277,6 +280,12 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
277280
278281 // Process owner transfer
279282 if (sizeof ($ keyValuePairs ) === 1 && key_exists ('ownerId ' , $ keyValuePairs )) {
283+ // Only allow owner transfer if current user is the form owner
284+ if ($ this ->currentUser ->getUID () !== $ form ->getOwnerId ()) {
285+ $ this ->logger ->debug ('Only the form owner can transfer ownership ' );
286+ throw new OCSForbiddenException ('Only the form owner can transfer ownership ' );
287+ }
288+
280289 $ this ->logger ->debug ('Updating owner: formId: {formId}, userId: {uid} ' , [
281290 'formId ' => $ formId ,
282291 'uid ' => $ keyValuePairs ['ownerId ' ]
@@ -477,7 +486,9 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
477486 #[BruteForceProtection(action: 'form ' )]
478487 #[ApiRoute(verb: 'POST ' , url: '/api/v3/forms/{formId}/questions ' )]
479488 public function newQuestion (int $ formId , ?string $ type = null , string $ text = '' , ?int $ fromId = null ): DataResponse {
480- $ form = $ this ->getFormIfAllowed ($ formId );
489+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
490+ $ this ->obtainFormLock ($ form );
491+
481492 if ($ this ->formsService ->isFormArchived ($ form )) {
482493 $ this ->logger ->debug ('This form is archived and can not be modified ' );
483494 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -608,7 +619,9 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
608619
609620 // Make sure we query the form first to check the user has permissions
610621 // So the user does not get information about "questions" if they do not even have permissions to the form
611- $ form = $ this ->getFormIfAllowed ($ formId );
622+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
623+ $ this ->obtainFormLock ($ form );
624+
612625 if ($ this ->formsService ->isFormArchived ($ form )) {
613626 $ this ->logger ->debug ('This form is archived and can not be modified ' );
614627 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -682,7 +695,9 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse {
682695 ]);
683696
684697
685- $ form = $ this ->getFormIfAllowed ($ formId );
698+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
699+ $ this ->obtainFormLock ($ form );
700+
686701 if ($ this ->formsService ->isFormArchived ($ form )) {
687702 $ this ->logger ->debug ('This form is archived and can not be modified ' );
688703 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -748,7 +763,9 @@ public function reorderQuestions(int $formId, array $newOrder): DataResponse {
748763 'newOrder ' => $ newOrder
749764 ]);
750765
751- $ form = $ this ->getFormIfAllowed ($ formId );
766+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
767+ $ this ->obtainFormLock ($ form );
768+
752769 if ($ this ->formsService ->isFormArchived ($ form )) {
753770 $ this ->logger ->debug ('This form is archived and can not be modified ' );
754771 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -847,7 +864,9 @@ public function newOption(int $formId, int $questionId, array $optionTexts): Dat
847864 'text ' => $ optionTexts ,
848865 ]);
849866
850- $ form = $ this ->getFormIfAllowed ($ formId );
867+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
868+ $ this ->obtainFormLock ($ form );
869+
851870 if ($ this ->formsService ->isFormArchived ($ form )) {
852871 $ this ->logger ->debug ('This form is archived and can not be modified ' );
853872 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -929,7 +948,9 @@ public function updateOption(int $formId, int $questionId, int $optionId, array
929948 'keyValuePairs ' => $ keyValuePairs
930949 ]);
931950
932- $ form = $ this ->getFormIfAllowed ($ formId );
951+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
952+ $ this ->obtainFormLock ($ form );
953+
933954 if ($ this ->formsService ->isFormArchived ($ form )) {
934955 $ this ->logger ->debug ('This form is archived and can not be modified ' );
935956 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -996,7 +1017,9 @@ public function deleteOption(int $formId, int $questionId, int $optionId): DataR
9961017 'optionId ' => $ optionId
9971018 ]);
9981019
999- $ form = $ this ->getFormIfAllowed ($ formId );
1020+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
1021+ $ this ->obtainFormLock ($ form );
1022+
10001023 if ($ this ->formsService ->isFormArchived ($ form )) {
10011024 $ this ->logger ->debug ('This form is archived and can not be modified ' );
10021025 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -1052,7 +1075,9 @@ public function deleteOption(int $formId, int $questionId, int $optionId): DataR
10521075 #[BruteForceProtection(action: 'form ' )]
10531076 #[ApiRoute(verb: 'PATCH ' , url: '/api/v3/forms/{formId}/questions/{questionId}/options ' )]
10541077 public function reorderOptions (int $ formId , int $ questionId , array $ newOrder ) {
1055- $ form = $ this ->getFormIfAllowed ($ formId );
1078+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
1079+ $ this ->obtainFormLock ($ form );
1080+
10561081 if ($ this ->formsService ->isFormArchived ($ form )) {
10571082 $ this ->logger ->debug ('This form is archived and can not be modified ' );
10581083 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -1790,6 +1815,12 @@ private function getFormIfAllowed(int $formId, string $permissions = 'all'): For
17901815 throw new NoSuchFormException ('This form is not owned by the current user and user has no `results_delete` permission ' , Http::STATUS_FORBIDDEN );
17911816 }
17921817 break ;
1818+ case Constants::PERMISSION_EDIT :
1819+ if (!$ this ->formsService ->canEditForm ($ form )) {
1820+ $ this ->logger ->debug ('This form is not owned by the current user and user has no `edit` permission ' );
1821+ throw new NoSuchFormException ('This form is not owned by the current user and user has no `edit` permission ' , Http::STATUS_FORBIDDEN );
1822+ }
1823+ break ;
17931824 default :
17941825 // By default we request full permissions
17951826 if ($ form ->getOwnerId () !== $ this ->currentUser ->getUID ()) {
@@ -1800,4 +1831,23 @@ private function getFormIfAllowed(int $formId, string $permissions = 'all'): For
18001831 }
18011832 return $ form ;
18021833 }
1834+
1835+ /**
1836+ * Locks the given form for the current user for a duration of 15 minutes.
1837+ *
1838+ * @param Form $form The form instance to lock.
1839+ */
1840+ private function obtainFormLock (Form $ form ): void {
1841+ // Only lock if not locked or locked by current user, or lock has expired
1842+ if (
1843+ $ form ->getLockedBy () === null ||
1844+ $ form ->getLockedBy () === $ this ->currentUser ->getUID () ||
1845+ $ form ->getLockedUntil () < time ()
1846+ ) {
1847+ $ form ->setLockedBy ($ this ->currentUser ->getUID ());
1848+ $ form ->setLockedUntil (time () + 15 * 60 );
1849+ } else {
1850+ throw new OCSForbiddenException ('Form is currently locked by another user. ' );
1851+ }
1852+ }
18031853}
0 commit comments