diff --git a/Civi/Api4/Query/SqlExpression.php b/Civi/Api4/Query/SqlExpression.php index de05876e4894..a58a580a73f9 100644 --- a/Civi/Api4/Query/SqlExpression.php +++ b/Civi/Api4/Query/SqlExpression.php @@ -68,6 +68,26 @@ public function __construct(string $expr, $alias = NULL) { abstract protected function initialize(); + private static function munge($name, $char = '_', $len = 63) { + // Replace all white space and non-alpha numeric with $char + // we only use the ascii character set since mysql does not create table names / field names otherwise + // CRM-11744 + $name = preg_replace('/[^a-zA-Z0-9_]+/', $char, trim($name)); + + // If there are no ascii characters present. + if (!strlen(trim($name, $char))) { + $name = \CRM_Utils_String::createRandom($len, \CRM_Utils_String::ALPHANUMERIC); + } + + if ($len) { + // lets keep variable names short + return substr($name, 0, $len); + } + else { + return $name; + } + } + /** * Converts a string to a SqlExpression object. * @@ -82,7 +102,7 @@ abstract protected function initialize(); public static function convert(string $expression, $parseAlias = FALSE, $mustBe = []) { $as = $parseAlias ? strrpos($expression, ' AS ') : FALSE; $expr = $as ? substr($expression, 0, $as) : $expression; - $alias = $as ? \CRM_Utils_String::munge(substr($expression, $as + 4), '_', 256) : NULL; + $alias = $as ? self::munge(substr($expression, $as + 4), '_', 256) : NULL; $bracketPos = strpos($expr, '('); $firstChar = substr($expr, 0, 1); $lastChar = substr($expr, -1); diff --git a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunWithCustomFieldTest.php b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunWithCustomFieldTest.php index 2cf32ba633b5..ac81e735ed8b 100644 --- a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunWithCustomFieldTest.php +++ b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunWithCustomFieldTest.php @@ -502,4 +502,90 @@ public function testEntityReferenceJoins() { $this->assertEquals('Dewey', $result[0]['columns'][0]['val']); } + public function testJoinWithCustomFieldEndingIn_() { + $subject = uniqid(__FUNCTION__); + + $contact = Contact::create(FALSE) + ->execute()->single(); + + // CustomGroup based on Activity Type + CustomGroup::create(FALSE) + ->addValue('extends', 'Activity') + ->addValue('title', 'testactivity2') + ->addChain('field', CustomField::create() + ->addValue('custom_group_id', '$id') + ->addValue('label', 'testactivity_') + ->addValue('data_type', 'Boolean') + ->addValue('html_type', 'Radio') + ) + ->execute(); + + $sampleData = [ + ['activity_type_id:name' => 'Meeting', 'testactivity2.testactivity_' => TRUE], + ]; + $this->saveTestRecords('Activity', [ + 'defaults' => ['subject' => $subject, 'source_contact_id', $contact['id']], + 'records' => $sampleData, + ]); + + $params = [ + 'checkPermissions' => FALSE, + 'return' => 'page:1', + 'savedSearch' => [ + 'api_entity' => 'Contact', + 'api_params' => [ + 'version' => 4, + 'select' => [ + 'id', + 'GROUP_CONCAT(DISTINCT Contact_ActivityContact_Activity_01.testactivity2.testactivity_:label) AS GROUP_CONCAT_Contact_ActivityContact_Activity_01_testactivity2_testactivity__label', + ], + 'orderBy' => [], + 'where' => [['contact_type:name', '=', 'Individual']], + 'groupBy' => ['id'], + 'join' => [ + ['Activity AS Contact_ActivityContact_Activity_01', 'INNER', 'ActivityContact', + ['id', '=', 'Contact_ActivityContact_Activity_01.contact_id'], + ['Contact_ActivityContact_Activity_01.record_type_id:name', '=', '"Activity Source"'], + ['Contact_ActivityContact_Activity_01.activity_type_id:name', '=', '"Meeting"'], + ], + ], + 'having' => [], + ], + ], + 'display' => [ + 'type' => 'table', + 'label' => '', + 'settings' => [ + 'actions' => TRUE, + 'pager' => [], + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'id', + 'dataType' => 'Integer', + 'label' => 'Contact ID', + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'GROUP_CONCAT_Contact_ActivityContact_Activity_01_testactivity2_testactivity__label', + 'dataType' => 'Boolean', + 'label' => '(List) Contact Activities: testactivity2: testactivity_', + 'sortable' => TRUE, + ], + ], + 'sort' => [ + ['id', 'ASC'], + ], + ], + ], + 'afform' => NULL, + ]; + + $result = civicrm_api4('SearchDisplay', 'run', $params); + + $this->assertArrayHasKey('GROUP_CONCAT_Contact_ActivityContact_Activity_01_testactivity2_testactivity__label', $result[0]['data']); + $this->assertEquals('Yes', $result[0]['data']['GROUP_CONCAT_Contact_ActivityContact_Activity_01_testactivity2_testactivity__label'][0]); + } + }