From 8f4118b456d4676472524b5ee8a3228bf8d3dfa1 Mon Sep 17 00:00:00 2001 From: colemanw Date: Wed, 30 Aug 2023 22:25:37 -0400 Subject: [PATCH] Afform - Enforce submission open/closed status --- CRM/Api4/Page/AJAX.php | 4 +- .../Api4/Action/Afform/AbstractProcessor.php | 12 +++- ext/afform/core/ang/af/afForm.component.js | 21 +++++-- .../phpunit/api/v4/AfformContactUsageTest.php | 63 +++++++++++++++++++ 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/CRM/Api4/Page/AJAX.php b/CRM/Api4/Page/AJAX.php index 650641b07e..dee72c6f70 100644 --- a/CRM/Api4/Page/AJAX.php +++ b/CRM/Api4/Page/AJAX.php @@ -134,8 +134,9 @@ class CRM_Api4_Page_AJAX extends CRM_Core_Page { $statusMap = [ \Civi\API\Exception\UnauthorizedException::class => 403, ]; + $status = $statusMap[get_class($e)] ?? 500; // Send error code (but don't overwrite success code if there are multiple calls and one was successful) - $this->httpResponseCode = $this->httpResponseCode ?: ($statusMap[get_class($e)] ?? 500); + $this->httpResponseCode = $this->httpResponseCode ?: $status; if (CRM_Core_Permission::check('view debug output')) { $response['error_code'] = $e->getCode(); $response['error_message'] = $e->getMessage(); @@ -165,6 +166,7 @@ class CRM_Api4_Page_AJAX extends CRM_Core_Page { 'exception' => $e, ]); } + $response['status'] = $status; } return $response; } diff --git a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php index df07e413f0..fc4ad6a32f 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php @@ -5,8 +5,10 @@ namespace Civi\Api4\Action\Afform; use Civi\Afform\Event\AfformEntitySortEvent; use Civi\Afform\Event\AfformPrefillEvent; use Civi\Afform\FormDataModel; +use Civi\API\Exception\UnauthorizedException; use Civi\Api4\Generic\Result; use Civi\Api4\Utils\CoreUtil; +use CRM_Afform_ExtensionUtil as E; /** * Shared functionality for form submission pre & post processing. @@ -60,8 +62,14 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction { * @throws \CRM_Core_Exception */ public function _run(Result $result) { - // This will throw an exception if the form doesn't exist or user lacks permission - $this->_afform = (array) civicrm_api4('Afform', 'get', ['where' => [['name', '=', $this->name]]], 0); + $this->_afform = civicrm_api4('Afform', 'get', [ + 'where' => [['name', '=', $this->name], ['submit_currently_open', '=', TRUE]], + ])->first(); + if (!$this->_afform) { + // Either the form doesn't exist, user lacks permission, + // or submit_currently_open = false. + throw new UnauthorizedException(E::ts('You do not have permission to submit this form')); + } $this->_formDataModel = new FormDataModel($this->_afform['layout']); $this->loadEntities(); $result->exchangeArray($this->processForm()); diff --git a/ext/afform/core/ang/af/afForm.component.js b/ext/afform/core/ang/af/afForm.component.js index 3208ff8b13..ed288e65ec 100644 --- a/ext/afform/core/ang/af/afForm.component.js +++ b/ext/afform/core/ang/af/afForm.component.js @@ -1,4 +1,5 @@ (function(angular, $, _) { + "use strict"; // Example usage: angular.module('af').component('afForm', { bindings: { @@ -41,9 +42,10 @@ return $scope.$parent.meta; }; // With no arguments this will prefill the entire form based on url args + // and also check if the form is open for submissions. // With selectedEntity, selectedIndex & selectedId provided this will prefill a single entity this.loadData = function(selectedEntity, selectedIndex, selectedId, selectedField) { - let toLoad = false; + let toLoad = true; const params = {name: ctrl.getFormMeta().name, args: {}}; // Load single entity if (selectedEntity) { @@ -56,9 +58,6 @@ else { args = _.assign({}, $scope.$parent.routeParams || {}, $scope.$parent.options || {}); _.each(schema, function (entity, entityName) { - if (args[entityName] || entity.actions.update) { - toLoad = true; - } if (args[entityName] && typeof args[entityName] === 'string') { args[entityName] = args[entityName].split(','); } @@ -75,6 +74,13 @@ angular.merge(data[item.name][index], values, {fields: _.cloneDeep(schema[item.name].data || {})}); }); }); + }, (error) => { + if (error.status === 403) { + // Permission denied + disableForm(); + } else { + // Unknown server error. What to do? + } }); } // Clear existing contact selection @@ -153,6 +159,13 @@ return valid; } + function disableForm() { + CRM.alert(ts('This form is not currently open for submissions.'), ts('Sorry'), 'error'); + $('af-form[ng-form="' + ctrl.getFormMeta().name + '"]') + .addClass('disabled') + .find('button[ng-click="afform.submit()"]').prop('disabled', true); + } + this.submit = function() { // validate required fields on the form if (!ctrl.ngForm.$valid || !validateFileFields()) { diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php index 2792c1e593..2457fb510b 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php @@ -1,4 +1,6 @@ useValues([ + 'layout' => self::$layouts['aboutMe'], + 'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION, + 'create_submission' => TRUE, + 'submit_limit' => 3, + ]); + + $cid = $this->createLoggedInUser(); + CRM_Core_Config::singleton()->userPermissionTemp = new CRM_Core_Permission_Temp(); + + $submitValues = [ + ['fields' => ['first_name' => 'Firsty', 'last_name' => 'Lasty']], + ]; + + // Submit twice + Civi\Api4\Afform::submit() + ->setName($this->formName) + ->setValues(['me' => $submitValues]) + ->execute(); + Civi\Api4\Afform::submit() + ->setName($this->formName) + ->setValues(['me' => $submitValues]) + ->execute(); + + // Autofilling form works because limit hasn't been reached + Civi\Api4\Afform::prefill()->setName($this->formName)->execute(); + + // Last time + Civi\Api4\Afform::submit() + ->setName($this->formName) + ->setValues(['me' => $submitValues]) + ->execute(); + + // Stats should report that we've reached the submission limit + $stats = \Civi\Api4\Afform::get(FALSE) + ->addSelect('submit_enabled', 'submission_count', 'submit_currently_open') + ->addWhere('name', '=', $this->formName) + ->execute()->single(); + $this->assertTrue($stats['submit_enabled']); + $this->assertFalse($stats['submit_currently_open']); + $this->assertEquals(3, $stats['submission_count']); + + // Prefilling and submitting are no longer allowed. + try { + Civi\Api4\Afform::prefill()->setName($this->formName)->execute(); + $this->fail(); + } + catch (\Civi\API\Exception\UnauthorizedException $e) { + } + try { + Civi\Api4\Afform::submit() + ->setName($this->formName) + ->setValues(['me' => $submitValues]) + ->execute(); + $this->fail(); + } + catch (\Civi\API\Exception\UnauthorizedException $e) { + } + } + } -- 2.25.1