From: Coleman Watts Date: Thu, 7 Oct 2021 04:24:07 +0000 (-0400) Subject: Afform - Fix chainSelect to work with anonymous users X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=db3935802b37b888d7a0d1369d1e0ca266c32581;p=civicrm-core.git Afform - Fix chainSelect to work with anonymous users --- diff --git a/ext/afform/core/Civi/Afform/AfformMetadataInjector.php b/ext/afform/core/Civi/Afform/AfformMetadataInjector.php index a8e596e720..ca20b3ac78 100644 --- a/ext/afform/core/Civi/Afform/AfformMetadataInjector.php +++ b/ext/afform/core/Civi/Afform/AfformMetadataInjector.php @@ -163,7 +163,7 @@ class AfformMetadataInjector { $params = [ 'action' => $action, 'where' => [['name', 'IN', $namesToMatch]], - 'select' => ['name', 'label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'entity', 'fk_entity'], + 'select' => ['name', 'label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'fk_entity'], 'loadOptions' => ['id', 'label'], // If the admin included this field on the form, then it's OK to get metadata about the field regardless of user permissions. 'checkPermissions' => FALSE, @@ -172,11 +172,16 @@ class AfformMetadataInjector { $params['values'] = ['contact_type' => $entityName]; $entityName = 'Contact'; } - $fields = civicrm_api4($entityName, 'getFields', $params); - $field = $originalField = $fields->first(); + foreach (civicrm_api4($entityName, 'getFields', $params) as $field) { + // In the highly unlikely event of 2 fields returned, prefer the exact match + if ($field['name'] === $fieldName) { + break; + } + } // If this is an implicit join, get new field from fk entity if ($field['name'] !== $fieldName && $field['fk_entity']) { $params['where'] = [['name', '=', substr($fieldName, 1 + strrpos($fieldName, '.'))]]; + $originalField = $field; $field = civicrm_api4($field['fk_entity'], 'getFields', $params)->first(); if ($field) { $field['label'] = $originalField['label'] . ' ' . $field['label']; diff --git a/ext/afform/core/Civi/Afform/FormDataModel.php b/ext/afform/core/Civi/Afform/FormDataModel.php index 82b78fb189..a0d93dad53 100644 --- a/ext/afform/core/Civi/Afform/FormDataModel.php +++ b/ext/afform/core/Civi/Afform/FormDataModel.php @@ -26,6 +26,11 @@ class FormDataModel { */ protected $blocks = []; + /** + * @var array + */ + protected $searchDisplays = []; + /** * @var array * Ex: $secureApi4s['spouse'] = function($entity, $action, $params){...}; @@ -124,14 +129,19 @@ class FormDataModel { * @param array $nodes * @param string $entity * @param string $join + * @param string $searchDisplay */ - protected function parseFields($nodes, $entity = NULL, $join = NULL) { + protected function parseFields($nodes, $entity = NULL, $join = NULL, $searchDisplay = NULL) { foreach ($nodes as $node) { if (!is_array($node) || !isset($node['#tag'])) { continue; } - elseif (!empty($node['af-fieldset']) && !empty($node['#children'])) { - $this->parseFields($node['#children'], $node['af-fieldset'], $join); + elseif (isset($node['af-fieldset']) && !empty($node['#children'])) { + $searchDisplay = $node['af-fieldset'] ? NULL : $this->findSearchDisplay($node); + $this->parseFields($node['#children'], $node['af-fieldset'], $join, $searchDisplay); + } + elseif ($searchDisplay && $node['#tag'] === 'af-field') { + $this->searchDisplays[$searchDisplay]['fields'][$node['name']] = AHQ::getProps($node); } elseif ($entity && $node['#tag'] === 'af-field') { if ($join) { @@ -146,7 +156,7 @@ class FormDataModel { $this->parseFields($node['#children'] ?? [], $entity, $node['af-join']); } elseif (!empty($node['#children'])) { - $this->parseFields($node['#children'], $entity, $join); + $this->parseFields($node['#children'], $entity, $join, $searchDisplay); } // Recurse into embedded blocks if (isset($this->blocks[$node['#tag']])) { @@ -154,12 +164,26 @@ class FormDataModel { $this->blocks[$node['#tag']] = Afform::get()->setCheckPermissions(FALSE)->setSelect(['name', 'layout'])->addWhere('name', '=', $this->blocks[$node['#tag']]['name'])->execute()->first(); } if (!empty($this->blocks[$node['#tag']]['layout'])) { - $this->parseFields($this->blocks[$node['#tag']]['layout'], $entity, $join); + $this->parseFields($this->blocks[$node['#tag']]['layout'], $entity, $join, $searchDisplay); } } } } + /** + * Finds a search display within a fieldset + * + * @param array $node + */ + public function findSearchDisplay($node) { + foreach (\Civi\Search\Display::getDisplayTypes(['name']) as $displayType) { + foreach (AHQ::getTags($node, $displayType['name']) as $display) { + $this->searchDisplays[$display['display-name']]['searchName'] = $display['search-name']; + return $display['display-name']; + } + } + } + /** * @return array[] * Ex: $entities['spouse']['type'] = 'Contact'; @@ -175,4 +199,11 @@ class FormDataModel { return $this->entities[$entityName] ?? NULL; } + /** + * @return array + */ + public function getSearchDisplay($displayName) { + return $this->searchDisplays[$displayName] ?? NULL; + } + } diff --git a/ext/afform/core/Civi/Api4/Action/Afform/GetOptions.php b/ext/afform/core/Civi/Api4/Action/Afform/GetOptions.php new file mode 100644 index 0000000000..a972694072 --- /dev/null +++ b/ext/afform/core/Civi/Api4/Action/Afform/GetOptions.php @@ -0,0 +1,98 @@ +_formDataModel->getEntity($this->modelName); + $searchDisplay = $this->_formDataModel->getSearchDisplay($this->modelName); + $fieldName = $this->fieldName; + + // For data-entry forms + if ($formEntity) { + $entity = $this->joinEntity ?: $formEntity['type']; + if ($this->joinEntity && !isset($formEntity['joins'][$this->joinEntity]['fields'][$this->fieldName])) { + throw new \API_Exception('Cannot get options for field not present on form'); + } + elseif (!$this->joinEntity && !isset($formEntity['fields'][$this->fieldName])) { + throw new \API_Exception('Cannot get options for field not present on form'); + } + } + // For search forms, get entity from savedSearch api params + elseif ($searchDisplay) { + if (!isset($searchDisplay['fields'][$this->fieldName])) { + throw new \API_Exception('Cannot get options for field not present on form'); + } + $savedSearch = SavedSearch::get(FALSE) + ->addWhere('name', '=', $searchDisplay['searchName']) + ->addSelect('api_entity', 'api_params') + ->execute()->single(); + // If field is not prefixed with a join, it's from the main entity + $entity = $savedSearch['api__entity']; + // Check to see if field belongs to a join + foreach ($savedSearch['api_params']['join'] ?? [] as $join) { + [$joinEntity, $joinAlias] = array_pad(explode(' AS ', $join[0]), 2, ''); + if (strpos($fieldName, $joinAlias . '.') === 0) { + $entity = $joinEntity; + $fieldName = substr($fieldName, strlen($joinAlias) + 1); + } + } + } + + return civicrm_api4($entity, 'getFields', [ + 'checkPermissions' => FALSE, + 'where' => [['name', '=', $fieldName]], + 'select' => ['options'], + 'loadOptions' => ['id', 'label'], + 'values' => FormattingUtil::filterByPrefix($this->values, $this->fieldName, $fieldName), + ], 0)['options'] ?: []; + } + + protected function loadEntities() { + // Do nothing; this action doesn't need entity data + } + +} diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php index 5bd35b0d46..4e5f0c977f 100644 --- a/ext/afform/core/Civi/Api4/Afform.php +++ b/ext/afform/core/Civi/Api4/Afform.php @@ -93,6 +93,15 @@ class Afform extends Generic\AbstractEntity { ->setCheckPermissions($checkPermissions); } + /** + * @param bool $checkPermissions + * @return Action\Afform\GetOptions + */ + public static function getOptions($checkPermissions = TRUE) { + return (new Action\Afform\GetOptions('Afform', __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + /** * @param bool $checkPermissions * @return Generic\BasicBatchAction @@ -243,6 +252,7 @@ class Afform extends Generic\AbstractEntity { "default" => ["administer CiviCRM"], // These all check form-level permissions 'get' => [], + 'getOptions' => [], 'prefill' => [], 'submit' => [], 'submitFile' => [], diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php index df16ec6a80..2ecf7c854d 100644 --- a/ext/afform/core/afform.php +++ b/ext/afform/core/afform.php @@ -492,7 +492,9 @@ function _afform_angular_module_name($fileBaseName, $format = 'camel') { */ function afform_civicrm_alterApiRoutePermissions(&$permissions, $entity, $action) { if ($entity == 'Afform') { - if ($action == 'prefill' || $action == 'submit' || $action == 'submitFile') { + // These actions should be accessible to anonymous users; permissions are checked internally + $allowedActions = ['prefill', 'submit', 'submitFile', 'getOptions']; + if (in_array($action, $allowedActions, TRUE)) { $permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION; } } diff --git a/ext/afform/core/ang/af/afField.component.js b/ext/afform/core/ang/af/afField.component.js index 5737c1a4a8..aa83eac8fb 100644 --- a/ext/afform/core/ang/af/afField.component.js +++ b/ext/afform/core/ang/af/afField.component.js @@ -90,19 +90,19 @@ $scope.dataProvider.getFieldData()[ctrl.fieldName] = ''; } } - if (val) { + if (val && (typeof val === 'number' || val.length)) { $('input[crm-ui-select]', $element).addClass('loading').prop('disabled', true); var params = { - where: [['name', '=', ctrl.defn.name]], - select: ['options'], - loadOptions: ['id', 'label'], - values: {} + name: ctrl.afFieldset.getFormName(), + modelName: ctrl.afFieldset.getName(), + fieldName: ctrl.fieldName, + joinEntity: ctrl.afJoin ? ctrl.afJoin.entity : null, + values: $scope.dataProvider.getFieldData() }; - params.values[ctrl.defn.input_attrs.control_field] = val; - crmApi4(ctrl.defn.entity, 'getFields', params, 0) + crmApi4('Afform', 'getOptions', params) .then(function(data) { - $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', false); - chainSelectOptions = data.options; + $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', !data.length); + chainSelectOptions = data; validateValue(); }); } else { diff --git a/ext/afform/core/ang/af/afFieldset.directive.js b/ext/afform/core/ang/af/afFieldset.directive.js index 4525f02054..384fad9b90 100644 --- a/ext/afform/core/ang/af/afFieldset.directive.js +++ b/ext/afform/core/ang/af/afFieldset.directive.js @@ -35,7 +35,7 @@ return data[0].fields; }; this.getFormName = function() { - return ctrl.afFormCtrl ? ctrl.afFormCtrl.getMeta().name : $scope.meta.name; + return ctrl.afFormCtrl ? ctrl.afFormCtrl.getFormMeta().name : $scope.meta.name; }; } }; diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php index 7e0b2f6f46..35d1c2a159 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php @@ -15,6 +15,9 @@ class api_v4_AfformContactUsageTest extends api_v4_AfformUsageTestCase {
+
+ +
EOHTML; @@ -87,6 +90,33 @@ EOHTML; $this->assertEquals('Lasty', $contact['last_name']); } + public function testChainSelect(): void { + $this->useValues([ + 'layout' => self::$layouts['aboutMe'], + 'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION, + ]); + + // Get states for USA + $result = Civi\Api4\Afform::getOptions() + ->setName($this->formName) + ->setModelName('me') + ->setFieldName('state_province_id') + ->setJoinEntity('Address') + ->setValues(['country_id' => CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', 'United States', 'id', 'name')]) + ->execute(); + $this->assertEquals('Alabama', $result[0]['label']); + + // Get states for UK + $result = Civi\Api4\Afform::getOptions() + ->setName($this->formName) + ->setModelName('me') + ->setFieldName('state_province_id') + ->setJoinEntity('Address') + ->setValues(['country_id' => CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', 'United Kingdom', 'id', 'name')]) + ->execute(); + $this->assertEquals('Aberdeen City', $result[0]['label']); + } + public function testCheckEntityReferenceFieldsReplacement(): void { $this->useValues([ 'layout' => self::$layouts['registerSite'],