APIv4 autocomplete - Support filters
authorColeman Watts <coleman@civicrm.org>
Fri, 5 Aug 2022 14:53:39 +0000 (10:53 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 10 Aug 2022 02:28:44 +0000 (22:28 -0400)
Civi/Api4/Generic/AutocompleteAction.php
Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php

index 1fc9110ad83d8a5be77a74562435a1a7f1db77a4..700310ad6a85f8e39485ce5bad31b16417a13ff3 100644 (file)
@@ -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;
+  }
+
 }
index aeb9648d5ef6edeb8f366ecca683e165b5dc1103..45e6f0c237a16544f6336be2ffdbae0ef5f13db5 100644 (file)
@@ -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);