diff --git a/Civi/Api4/Utils/SelectUtil.php b/Civi/Api4/Utils/SelectUtil.php index 38b085ceefaa..00863f051c8e 100644 --- a/Civi/Api4/Utils/SelectUtil.php +++ b/Civi/Api4/Utils/SelectUtil.php @@ -43,17 +43,32 @@ public static function isFieldSelected($field, $selects) { } /** + * Filters a list of fieldnames by matching a pattern which may contain * wildcards. + * + * For fieldnames joined with a dot (e.g. email.contact_id), wildcards are only allowed after the last dot. + * * @param string $pattern * @param array $fieldNames * @return array */ public static function getMatchingFields($pattern, $fieldNames) { + // If the pattern is "select all" then we return all base fields (excluding those with a dot) if ($pattern === '*') { - return $fieldNames; + return array_values(array_filter($fieldNames, function($field) { + return strpos($field, '.') === FALSE; + })); } - $pattern = '/^' . str_replace('\*', '.*', preg_quote($pattern, '/')) . '$/'; - return array_values(array_filter($fieldNames, function($field) use ($pattern) { - return preg_match($pattern, $field); + $dot = strrpos($pattern, '.'); + $prefix = $dot === FALSE ? '' : substr($pattern, 0, $dot + 1); + $search = $dot === FALSE ? $pattern : substr($pattern, $dot + 1); + $search = '/^' . str_replace('\*', '.*', preg_quote($search, '/')) . '$/'; + return array_values(array_filter($fieldNames, function($field) use ($search, $prefix) { + // Exclude fields that don't have the same join prefix + if (($prefix !== '' && strpos($field, $prefix) !== 0) || substr_count($prefix, '.') !== substr_count($field, '.')) { + return FALSE; + } + // Now strip the prefix and compare field name to the pattern + return preg_match($search, substr($field, strlen($prefix))); })); } diff --git a/tests/phpunit/api/v4/Utils/SelectUtilTest.php b/tests/phpunit/api/v4/Utils/SelectUtilTest.php index 662d343cbc36..827634c831df 100644 --- a/tests/phpunit/api/v4/Utils/SelectUtilTest.php +++ b/tests/phpunit/api/v4/Utils/SelectUtilTest.php @@ -42,6 +42,12 @@ class SelectUtilTest extends UnitTestCase { 'reset_date', 'signature_text', 'signature_html', + 'contact.id', + 'contact.display_name', + 'contact.sort_name', + 'contact.phone.id', + 'contact.phone.phone', + 'contact.phone.phone_type_id', ]; public function getSelectExamples() { @@ -67,7 +73,7 @@ public function testIsFieldSelected($field, $selects, $expected) { public function getMatchingExamples() { return [ - [$this->emailFieldNames, '*'], + [array_slice($this->emailFieldNames, 0, 12), '*'], [[], 'nothing'], [['email'], 'email'], [['contact_id', 'location_type_id'], '*_id'], @@ -75,6 +81,10 @@ public function getMatchingExamples() { [['contact_id'], 'con*_id'], [['is_primary', 'is_billing', 'is_bulkmail'], 'is_*'], [['is_billing', 'is_bulkmail'], 'is_*l*'], + [['contact.id', 'contact.display_name', 'contact.sort_name'], 'contact.*'], + [['contact.display_name', 'contact.sort_name'], 'contact.*_name'], + [['contact.phone.id', 'contact.phone.phone', 'contact.phone.phone_type_id'], 'contact.phone.*'], + [['contact.phone.phone', 'contact.phone.phone_type_id'], 'contact.phone.phone*'], ]; }