5252use OCP \IUser ;
5353use OCP \IUserManager ;
5454use OCP \IUserSession ;
55-
5655use Psr \Log \LoggerInterface ;
5756
5857/**
@@ -190,6 +189,8 @@ public function newForm(?int $fromId = null): DataResponse {
190189 unset($ formData ['state ' ]);
191190 unset($ formData ['fileId ' ]);
192191 unset($ formData ['fileFormat ' ]);
192+ unset($ formData ['locked_by ' ]);
193+ unset($ formData ['locked_until ' ]);
193194 $ formData ['hash ' ] = $ this ->formsService ->generateFormHash ();
194195 // TRANSLATORS Appendix to the form Title of a duplicated/copied form.
195196 $ formData ['title ' ] .= ' - ' . $ this ->l10n ->t ('Copy ' );
@@ -267,16 +268,85 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
267268 'keyValuePairs ' => $ keyValuePairs
268269 ]);
269270
270- $ form = $ this ->getFormIfAllowed ($ formId );
271+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
272+ $ currentUserId = $ this ->currentUser ->getUID ();
271273
272274 // Don't allow empty array
273275 if (sizeof ($ keyValuePairs ) === 0 ) {
274276 $ this ->logger ->info ('Empty keyValuePairs, will not update. ' );
275277 throw new OCSForbiddenException ('Empty keyValuePairs, will not update. ' );
276278 }
277279
280+ // Process complete form locking (lockedUntil: 0)
281+ if (
282+ sizeof ($ keyValuePairs ) === 2
283+ && array_key_exists ('lockedBy ' , $ keyValuePairs ) && $ keyValuePairs ['lockedBy ' ] === $ form ->getOwnerId ()
284+ && array_key_exists ('lockedUntil ' , $ keyValuePairs ) && $ keyValuePairs ['lockedUntil ' ] === 0
285+ ) {
286+ // Only allow form locking for form owner
287+ if ($ currentUserId !== $ form ->getOwnerId () || $ currentUserId !== $ form ->getLockedBy ()) {
288+ $ this ->logger ->debug ('Only the form owner can lock the form permanently ' );
289+ throw new OCSForbiddenException ('Only the form owner can lock the form permanently ' );
290+ }
291+
292+ // Only allow if the form is not currently locked by another user
293+ if (
294+ $ form ->getLockedBy () !== null
295+ && $ form ->getLockedBy () !== $ currentUserId
296+ && $ form ->getLockedUntil () >= time ()
297+ ) {
298+ $ this ->logger ->debug ('Form is currently locked by another user. ' );
299+ throw new OCSForbiddenException ('Form is currently locked by another user. ' );
300+ }
301+
302+ // Abort if form is already completely locked
303+ if ($ form ->getLockedUntil () === 0 ) {
304+ $ this ->logger ->debug ('Form is already locked completely. ' );
305+ throw new OCSBadRequestException ('Form is already locked completely. ' );
306+ }
307+
308+ $ form ->setLockedBy ($ keyValuePairs ['lockedBy ' ]);
309+ $ form ->setLockedUntil ($ keyValuePairs ['lockedUntil ' ]);
310+
311+ // Update changed Columns in Db.
312+ $ this ->formMapper ->update ($ form );
313+
314+ return new DataResponse ($ form ->getId ());
315+ }
316+
317+ // Process form unlocking
318+ if (
319+ sizeof ($ keyValuePairs ) === 2
320+ && array_key_exists ('lockedBy ' , $ keyValuePairs ) && is_null ($ keyValuePairs ['lockedBy ' ])
321+ && array_key_exists ('lockedUntil ' , $ keyValuePairs ) && is_null ($ keyValuePairs ['lockedUntil ' ])
322+ ) {
323+ // Only allow form unlocking if for form owner or lock user
324+ if ($ currentUserId !== $ form ->getOwnerId () && $ currentUserId !== $ form ->getLockedBy () && $ form ->getLockedUntil () !== 0 ) {
325+ $ this ->logger ->debug ('Only the form owner or the user who obtained the lock can unlock the form ' );
326+ throw new OCSForbiddenException ('Only the form owner or the user who obtained the lock can unlock the form ' );
327+ }
328+
329+ // remove form lock
330+ $ form ->setLockedBy (null );
331+ $ form ->setLockedUntil (null );
332+
333+ // Update changed Columns in Db.
334+ $ this ->formMapper ->update ($ form );
335+
336+ return new DataResponse ($ form ->getId ());
337+ }
338+
339+ // Lock form temporary
340+ $ this ->obtainFormLock ($ form );
341+
278342 // Process owner transfer
279343 if (sizeof ($ keyValuePairs ) === 1 && key_exists ('ownerId ' , $ keyValuePairs )) {
344+ // Only allow owner transfer if current user is the form owner
345+ if ($ currentUserId !== $ form ->getOwnerId ()) {
346+ $ this ->logger ->debug ('Only the form owner can transfer ownership ' );
347+ throw new OCSForbiddenException ('Only the form owner can transfer ownership ' );
348+ }
349+
280350 $ this ->logger ->debug ('Updating owner: formId: {formId}, userId: {uid} ' , [
281351 'formId ' => $ formId ,
282352 'uid ' => $ keyValuePairs ['ownerId ' ]
@@ -288,23 +358,33 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
288358 throw new OCSBadRequestException ('Could not find new form owner ' );
289359 }
290360
291- // update form owner
361+ // update form owner and remove form lock
292362 $ form ->setOwnerId ($ keyValuePairs ['ownerId ' ]);
363+ $ form ->setLockedBy (null );
364+ $ form ->setLockedUntil (null );
293365
294366 // Update changed Columns in Db.
295367 $ this ->formMapper ->update ($ form );
296368
297369 return new DataResponse ($ form ->getOwnerId ());
298370 }
299371
300- // Don't allow to change params id, hash, ownerId, created, lastUpdated, fileId
301- if (
302- key_exists ('id ' , $ keyValuePairs ) || key_exists ('hash ' , $ keyValuePairs ) ||
303- key_exists ('ownerId ' , $ keyValuePairs ) || key_exists ('created ' , $ keyValuePairs ) ||
304- isset ($ keyValuePairs ['fileId ' ]) || key_exists ('lastUpdated ' , $ keyValuePairs )
305- ) {
306- $ this ->logger ->info ('Not allowed to update id, hash, ownerId, created, fileId or lastUpdated ' );
307- throw new OCSForbiddenException ('Not allowed to update id, hash, ownerId, created, fileId or lastUpdated ' );
372+ // Don't allow to change the following attributes
373+ $ forbiddenKeys = [
374+ 'id ' , 'hash ' , 'ownerId ' , 'created ' , 'lastUpdated ' , 'lockedBy ' , 'lockedUntil '
375+ ];
376+
377+ foreach ($ forbiddenKeys as $ key ) {
378+ if (array_key_exists ($ key , $ keyValuePairs )) {
379+ $ this ->logger ->info ("Not allowed to update {$ key }" );
380+ throw new OCSForbiddenException ("Not allowed to update {$ key }" );
381+ }
382+ }
383+
384+ // Don't allow to change fileId
385+ if (isset ($ keyValuePairs ['fileId ' ])) {
386+ $ this ->logger ->info ('Not allowed to update fileId ' );
387+ throw new OCSForbiddenException ('Not allowed to update fileId ' );
308388 }
309389
310390 // Do not allow changing showToAllUsers if disabled
@@ -477,7 +557,9 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
477557 #[BruteForceProtection(action: 'form ' )]
478558 #[ApiRoute(verb: 'POST ' , url: '/api/v3/forms/{formId}/questions ' )]
479559 public function newQuestion (int $ formId , ?string $ type = null , string $ text = '' , ?int $ fromId = null ): DataResponse {
480- $ form = $ this ->getFormIfAllowed ($ formId );
560+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
561+ $ this ->obtainFormLock ($ form );
562+
481563 if ($ this ->formsService ->isFormArchived ($ form )) {
482564 $ this ->logger ->debug ('This form is archived and can not be modified ' );
483565 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -608,7 +690,9 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
608690
609691 // Make sure we query the form first to check the user has permissions
610692 // So the user does not get information about "questions" if they do not even have permissions to the form
611- $ form = $ this ->getFormIfAllowed ($ formId );
693+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
694+ $ this ->obtainFormLock ($ form );
695+
612696 if ($ this ->formsService ->isFormArchived ($ form )) {
613697 $ this ->logger ->debug ('This form is archived and can not be modified ' );
614698 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -682,7 +766,9 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse {
682766 ]);
683767
684768
685- $ form = $ this ->getFormIfAllowed ($ formId );
769+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
770+ $ this ->obtainFormLock ($ form );
771+
686772 if ($ this ->formsService ->isFormArchived ($ form )) {
687773 $ this ->logger ->debug ('This form is archived and can not be modified ' );
688774 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -748,7 +834,9 @@ public function reorderQuestions(int $formId, array $newOrder): DataResponse {
748834 'newOrder ' => $ newOrder
749835 ]);
750836
751- $ form = $ this ->getFormIfAllowed ($ formId );
837+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
838+ $ this ->obtainFormLock ($ form );
839+
752840 if ($ this ->formsService ->isFormArchived ($ form )) {
753841 $ this ->logger ->debug ('This form is archived and can not be modified ' );
754842 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -847,7 +935,9 @@ public function newOption(int $formId, int $questionId, array $optionTexts): Dat
847935 'text ' => $ optionTexts ,
848936 ]);
849937
850- $ form = $ this ->getFormIfAllowed ($ formId );
938+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
939+ $ this ->obtainFormLock ($ form );
940+
851941 if ($ this ->formsService ->isFormArchived ($ form )) {
852942 $ this ->logger ->debug ('This form is archived and can not be modified ' );
853943 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -929,7 +1019,9 @@ public function updateOption(int $formId, int $questionId, int $optionId, array
9291019 'keyValuePairs ' => $ keyValuePairs
9301020 ]);
9311021
932- $ form = $ this ->getFormIfAllowed ($ formId );
1022+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
1023+ $ this ->obtainFormLock ($ form );
1024+
9331025 if ($ this ->formsService ->isFormArchived ($ form )) {
9341026 $ this ->logger ->debug ('This form is archived and can not be modified ' );
9351027 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -996,7 +1088,9 @@ public function deleteOption(int $formId, int $questionId, int $optionId): DataR
9961088 'optionId ' => $ optionId
9971089 ]);
9981090
999- $ form = $ this ->getFormIfAllowed ($ formId );
1091+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
1092+ $ this ->obtainFormLock ($ form );
1093+
10001094 if ($ this ->formsService ->isFormArchived ($ form )) {
10011095 $ this ->logger ->debug ('This form is archived and can not be modified ' );
10021096 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -1052,7 +1146,9 @@ public function deleteOption(int $formId, int $questionId, int $optionId): DataR
10521146 #[BruteForceProtection(action: 'form ' )]
10531147 #[ApiRoute(verb: 'PATCH ' , url: '/api/v3/forms/{formId}/questions/{questionId}/options ' )]
10541148 public function reorderOptions (int $ formId , int $ questionId , array $ newOrder ) {
1055- $ form = $ this ->getFormIfAllowed ($ formId );
1149+ $ form = $ this ->getFormIfAllowed ($ formId , Constants::PERMISSION_EDIT );
1150+ $ this ->obtainFormLock ($ form );
1151+
10561152 if ($ this ->formsService ->isFormArchived ($ form )) {
10571153 $ this ->logger ->debug ('This form is archived and can not be modified ' );
10581154 throw new OCSForbiddenException ('This form is archived and can not be modified ' );
@@ -1797,6 +1893,12 @@ private function getFormIfAllowed(int $formId, string $permissions = 'all'): For
17971893 throw new NoSuchFormException ('This form is not owned by the current user and user has no `results_delete` permission ' , Http::STATUS_FORBIDDEN );
17981894 }
17991895 break ;
1896+ case Constants::PERMISSION_EDIT :
1897+ if (!$ this ->formsService ->canEditForm ($ form )) {
1898+ $ this ->logger ->debug ('This form is not owned by the current user and user has no `edit` permission ' );
1899+ throw new NoSuchFormException ('This form is not owned by the current user and user has no `edit` permission ' , Http::STATUS_FORBIDDEN );
1900+ }
1901+ break ;
18001902 default :
18011903 // By default we request full permissions
18021904 if ($ form ->getOwnerId () !== $ this ->currentUser ->getUID ()) {
@@ -1807,4 +1909,23 @@ private function getFormIfAllowed(int $formId, string $permissions = 'all'): For
18071909 }
18081910 return $ form ;
18091911 }
1912+
1913+ /**
1914+ * Locks the given form for the current user for a duration of 15 minutes.
1915+ *
1916+ * @param Form $form The form instance to lock.
1917+ */
1918+ private function obtainFormLock (Form $ form ): void {
1919+ // Only lock if not locked or locked by current user, or lock has expired
1920+ if (
1921+ $ form ->getLockedBy () !== null
1922+ && $ form ->getLockedBy () !== $ this ->currentUser ->getUID ()
1923+ && ($ form ->getLockedUntil () >= time () || $ form ->getLockedUntil () === 0 )
1924+ ) {
1925+ throw new OCSForbiddenException ('Form is currently locked by another user. ' );
1926+ }
1927+
1928+ $ form ->setLockedBy ($ this ->currentUser ->getUID ());
1929+ $ form ->setLockedUntil (time () + 15 * 60 );
1930+ }
18101931}
0 commit comments