From cc6a25326a345f72ebd2ff2710583e11be7a6fd5 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 5 Aug 2022 10:53:39 -0400 Subject: [PATCH] APIv4 autocomplete - Support filters --- Civi/Api4/Generic/AutocompleteAction.php | 63 ++++++++++++++++++- .../Traits/SavedSearchInspectorTrait.php | 9 ++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Civi/Api4/Generic/AutocompleteAction.php b/Civi/Api4/Generic/AutocompleteAction.php index 1fc9110ad8..700310ad6a 100644 --- a/Civi/Api4/Generic/AutocompleteAction.php +++ b/Civi/Api4/Generic/AutocompleteAction.php @@ -55,6 +55,22 @@ class AutocompleteAction extends AbstractAction { */ protected $fieldName; + /** + * Filters requested by untrusted client, permissions will be checked before applying (even if this request has checkPermissions = FALSE). + * + * Format: [fieldName => value][] + * @var array + */ + protected $clientFilters = []; + + /** + * Filters set programmatically by `civi.api.prepare` listener. Automatically trusted. + * + * Format: [fieldName => value][] + * @var array + */ + private $trustedFilters = []; + /** * Fetch results. * @@ -102,10 +118,12 @@ class AutocompleteAction extends AbstractAction { if (empty($this->_apiParams['having'])) { $this->_apiParams['select'] = $select; } + // A HAVING clause depends on the SELECT clause so don't overwrite it. else { - $this->_apiParams['select'] = array_merge($this->_apiParams['select'], $select); + $this->_apiParams['select'] = array_unique(array_merge($this->_apiParams['select'], $select)); } $this->_apiParams['checkPermissions'] = $this->getCheckPermissions(); + $this->applyFilters(); $apiResult = civicrm_api4($entityName, 'get', $this->_apiParams); $rawResults = array_slice((array) $apiResult, 0, $resultsPerPage); foreach ($rawResults as $row) { @@ -124,4 +142,47 @@ class AutocompleteAction extends AbstractAction { $result->setCountMatched($apiResult->countFetched()); } + /** + * Method for `civi.api.prepare` listener to add a trusted filter. + * + * @param string $fieldName + * @param mixed $value + */ + public function addFilter(string $fieldName, $value) { + $this->trustedFilters[$fieldName] = $value; + } + + /** + * Applies trusted filters. Checks access before applying client filters. + */ + private function applyFilters() { + foreach ($this->trustedFilters as $field => $val) { + if ($this->hasValue($val)) { + $this->applyFilter($field, $val); + } + } + foreach ($this->clientFilters as $field => $val) { + if ($this->hasValue($val) && $this->checkFieldAccess($field)) { + $this->applyFilter($field, $val); + } + } + } + + /** + * @param $fieldNameWithSuffix + * @return bool + */ + private function checkFieldAccess($fieldNameWithSuffix) { + [$fieldName] = explode(':', $fieldNameWithSuffix); + if ( + in_array($fieldName, $this->_apiParams['select'], TRUE) || + in_array($fieldNameWithSuffix, $this->_apiParams['select'], TRUE) || + in_array($fieldName, $this->savedSearch['api_params']['select'], TRUE) || + in_array($fieldNameWithSuffix, $this->savedSearch['api_params']['select'], TRUE) + ) { + return TRUE; + } + return FALSE; + } + } diff --git a/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php index aeb9648d5e..45e6f0c237 100644 --- a/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php +++ b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php @@ -54,6 +54,10 @@ trait SavedSearchInspectorTrait { ->addWhere('name', '=', $this->savedSearch) ->execute()->single(); } + if (is_array($this->savedSearch)) { + $this->savedSearch += ['api_params' => []]; + $this->savedSearch['api_params'] += ['select' => [], 'where' => []]; + } $this->_apiParams = ($this->savedSearch['api_params'] ?? []) + ['select' => [], 'where' => []]; } @@ -177,14 +181,15 @@ trait SavedSearchInspectorTrait { } /** - * @param array $fieldNames + * @param string|array $fieldName * If multiple field names are given they will be combined in an OR clause * @param mixed $value */ - protected function applyFilter(array $fieldNames, $value) { + protected function applyFilter($fieldName, $value) { // Global setting determines if % wildcard should be added to both sides (default) or only the end of a search string $prefixWithWildcard = \Civi::settings()->get('includeWildCardInName'); + $fieldNames = (array) $fieldName; // Based on the first field, decide which clause to add this condition to $fieldName = $fieldNames[0]; $field = $this->getField($fieldName); -- 2.25.1