From 82bf6674ff1d10d16a2de102b06263d05a737803 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Wed, 3 Mar 2021 15:04:38 -0500 Subject: [PATCH] SearchKit - Validate all filters as belonging to select clause or afform When viewing a SearchDisplay, this will verify all filters are permitted by first checking the SELECT clause, and secondly checking for a containing Afform with exposed filters. All other filter params will be silently ignored. --- .../core/ang/af/afFieldset.directive.js | 5 +- .../Civi/Api4/Action/SearchDisplay/Run.php | 151 +++++++++++++----- ext/search/ang/crmSearchDisplay.module.js | 3 +- 3 files changed, 120 insertions(+), 39 deletions(-) diff --git a/ext/afform/core/ang/af/afFieldset.directive.js b/ext/afform/core/ang/af/afFieldset.directive.js index 979915f21d..273651fe64 100644 --- a/ext/afform/core/ang/af/afFieldset.directive.js +++ b/ext/afform/core/ang/af/afFieldset.directive.js @@ -11,7 +11,7 @@ var self = ctrls[0]; self.afFormCtrl = ctrls[1]; }, - controller: function() { + controller: function($scope) { var ctrl = this, localData = []; @@ -31,6 +31,9 @@ } return data[0].fields; }; + this.getFormName = function() { + return ctrl.afFormCtrl ? ctrl.afFormCtrl.getMeta().name : $scope.meta.name; + }; } }; }); diff --git a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php index 1753ca099e..875b106527 100644 --- a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php @@ -40,14 +40,28 @@ class Run extends \Civi\Api4\Generic\AbstractAction { */ protected $return; - private $_selectQuery; - /** * Search conditions that will be automatically added to the WHERE or HAVING clauses * @var array */ protected $filters = []; + /** + * Name of Afform, if this display is embedded (used for permissioning) + * @var string + */ + protected $afform; + + /** + * @var \Civi\Api4\Query\Api4SelectQuery + */ + private $_selectQuery; + + /** + * @var array + */ + private $_afform; + /** * @param \Civi\Api4\Generic\Result $result * @throws UnauthorizedException @@ -63,12 +77,15 @@ class Run extends \Civi\Api4\Generic\AbstractAction { ->addWhere('name', '=', $this->savedSearch) ->execute()->first(); } - if (is_string($this->display)) { + if (is_string($this->display) && !empty($this->savedSearch['id'])) { $this->display = SearchDisplay::get(FALSE) ->addWhere('name', '=', $this->display) ->addWhere('saved_search_id', '=', $this->savedSearch['id']) ->execute()->first(); } + if (!$this->savedSearch || !$this->display) { + throw new \API_Exception("Error: SearchDisplay not found."); + } $entityName = $this->savedSearch['api_entity']; $apiParams =& $this->savedSearch['api_params']; $settings = $this->display['settings']; @@ -131,45 +148,67 @@ class Run extends \Civi\Api4\Generic\AbstractAction { * Applies supplied filters to the where clause */ private function applyFilters() { - // Global setting determines if % wildcard should be added to both sides (default) or only the end of the search term - $prefixWithWildcard = \Civi::settings()->get('includeWildCardInName'); + // Ignore empty strings + $filters = array_filter($this->filters, function($value) { + return isset($value) && (strlen($value) || !is_string($value)); + }); + if (!$filters) { + return; + } - foreach ($this->filters as $fieldName => $value) { - if ($value) { - $field = $this->getField($fieldName) ?? []; + // Process all filters that are included in SELECT clause. These filters are implicitly allowed. + foreach ($this->getSelectAliases() as $fieldName) { + if (isset($filters[$fieldName])) { + $value = $filters[$fieldName]; + unset($filters[$fieldName]); + $this->applyFilter($fieldName, $value); + } + } - // If the field doesn't exist, it could be an aggregated column - if (!$field) { - // Not a real field but in the SELECT clause. It must be an aggregated column. Add to HAVING clause. - if (in_array($fieldName, $this->getSelectAliases())) { - if ($prefixWithWildcard) { - $this->savedSearch['api_params']['having'][] = [$fieldName, 'CONTAINS', $value]; - } - else { - $this->savedSearch['api_params']['having'][] = [$fieldName, 'LIKE', $value . '%']; - } - } - // Error - field doesn't exist and isn't a column alias - else { - // Maybe throw an exception? Or just log a warning? - } - continue; + // Other filters may be allowed if display is embedded in an afform. + if ($filters) { + foreach ($this->getAfformFilters() as $fieldName) { + if (isset($filters[$fieldName])) { + $value = $filters[$fieldName]; + $this->applyFilter($fieldName, $value); } + } + } + } - $dataType = $field['data_type']; - if (!empty($field['serialize'])) { - $this->savedSearch['api_params']['where'][] = [$fieldName, 'CONTAINS', $value]; - } - elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) { - $this->savedSearch['api_params']['where'][] = [$fieldName, '=', $value]; - } - elseif ($prefixWithWildcard) { - $this->savedSearch['api_params']['where'][] = [$fieldName, 'CONTAINS', $value]; - } - else { - $this->savedSearch['api_params']['where'][] = [$fieldName, 'LIKE', $value . '%']; - } + /** + * @param string $fieldName + * @param string $value + */ + private function applyFilter(string $fieldName, string $value) { + $field = $this->getField($fieldName); + + // Global setting determines if % wildcard should be added to both sides (default) or only the end of the search term + $prefixWithWildcard = \Civi::settings()->get('includeWildCardInName'); + + // Not a real field. It must be an aggregated column. Add to HAVING clause. + if (!$field) { + if ($prefixWithWildcard) { + $this->savedSearch['api_params']['having'][] = [$fieldName, 'CONTAINS', $value]; + } + else { + $this->savedSearch['api_params']['having'][] = [$fieldName, 'LIKE', $value . '%']; } + return; + } + + $dataType = $field['data_type']; + if (!empty($field['serialize'])) { + $this->savedSearch['api_params']['where'][] = [$fieldName, 'CONTAINS', $value]; + } + elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) { + $this->savedSearch['api_params']['where'][] = [$fieldName, '=', $value]; + } + elseif ($prefixWithWildcard) { + $this->savedSearch['api_params']['where'][] = [$fieldName, 'CONTAINS', $value]; + } + else { + $this->savedSearch['api_params']['where'][] = [$fieldName, 'LIKE', $value . '%']; } } @@ -241,4 +280,42 @@ class Run extends \Civi\Api4\Generic\AbstractAction { return $this->_selectQuery->getField($fieldName, FALSE); } + /** + * @return array + */ + private function getAfformFilters() { + $afform = $this->loadAfform(); + return array_column(\CRM_Utils_Array::findAll( + $afform['layout'] ?? [], + ['#tag' => 'af-field'] + ), 'name'); + } + + /** + * Return afform with name specified in api call. + * + * Verifies the searchDisplay is embedded in the afform and the user has permission to view it. + * + * @return array|false|null + */ + private function loadAfform() { + // Only attempt to load afform once. + if ($this->afform && !isset($this->_afform)) { + $this->_afform = FALSE; + // Permission checks are enabled in this api call to ensure the user has permission to view the form + $afform = \Civi\Api4\Afform::get() + ->addWhere('name', '=', $this->afform) + ->setLayoutFormat('shallow') + ->execute()->first(); + // Validate that the afform contains this search display + if (\CRM_Utils_Array::findAll( + $afform['layout'] ?? [], + ['#tag' => "crm-search-display-{$this->display['type']}", 'display-name' => $this->display['name']]) + ) { + $this->_afform = $afform; + } + } + return $this->_afform; + } + } diff --git a/ext/search/ang/crmSearchDisplay.module.js b/ext/search/ang/crmSearchDisplay.module.js index 312782f536..7c134b9f47 100644 --- a/ext/search/ang/crmSearchDisplay.module.js +++ b/ext/search/ang/crmSearchDisplay.module.js @@ -68,7 +68,8 @@ savedSearch: ctrl.search, display: ctrl.display, sort: ctrl.sort, - filters: _.assign({}, (ctrl.afFieldset ? ctrl.afFieldset.getFieldData() : {}), ctrl.filters) + filters: _.assign({}, (ctrl.afFieldset ? ctrl.afFieldset.getFieldData() : {}), ctrl.filters), + afform: ctrl.afFieldset ? ctrl.afFieldset.getFormName() : null }; } -- 2.25.1