Commit | Line | Data |
---|---|---|
22601c92 CW |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
4 | | Copyright CiviCRM LLC. All rights reserved. | | |
5 | | | | |
6 | | This work is published under the GNU AGPLv3 license with some | | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
9 | +--------------------------------------------------------------------+ | |
10 | */ | |
11 | ||
12 | namespace Civi\Search; | |
13 | ||
14 | /** | |
15 | * Class Admin | |
16 | * @package Civi\Search | |
17 | */ | |
18 | class Admin { | |
19 | ||
20 | /** | |
21 | * @return array | |
22 | */ | |
23 | public static function getAdminSettings():array { | |
4f0729ed | 24 | $schema = self::getSchema(); |
22601c92 | 25 | return [ |
4f0729ed CW |
26 | 'schema' => $schema, |
27 | 'joins' => self::getJoins(array_column($schema, NULL, 'name')), | |
22601c92 CW |
28 | 'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()), |
29 | 'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(), | |
e7515b5b | 30 | 'displayTypes' => Display::getDisplayTypes(['name', 'label', 'description', 'icon']), |
22601c92 CW |
31 | ]; |
32 | } | |
33 | ||
34 | /** | |
35 | * @return string[] | |
36 | */ | |
37 | public static function getOperators():array { | |
38 | return [ | |
39 | '=' => '=', | |
40 | '!=' => '≠', | |
41 | '>' => '>', | |
42 | '<' => '<', | |
43 | '>=' => '≥', | |
44 | '<=' => '≤', | |
45 | 'CONTAINS' => ts('Contains'), | |
46 | 'IN' => ts('Is In'), | |
47 | 'NOT IN' => ts('Not In'), | |
48 | 'LIKE' => ts('Is Like'), | |
49 | 'NOT LIKE' => ts('Not Like'), | |
50 | 'BETWEEN' => ts('Is Between'), | |
51 | 'NOT BETWEEN' => ts('Not Between'), | |
52 | 'IS NULL' => ts('Is Null'), | |
53 | 'IS NOT NULL' => ts('Not Null'), | |
54 | ]; | |
55 | } | |
56 | ||
57 | /** | |
58 | * Fetch all entities the current user has permission to `get` | |
590c0e3f | 59 | * @return array |
22601c92 CW |
60 | */ |
61 | public static function getSchema() { | |
62 | $schema = []; | |
63 | $entities = \Civi\Api4\Entity::get() | |
4f0729ed | 64 | ->addSelect('name', 'title', 'type', 'title_plural', 'description', 'icon', 'paths', 'dao', 'bridge') |
09815e9c | 65 | ->addWhere('searchable', '=', TRUE) |
44111498 | 66 | ->addOrderBy('title_plural') |
22601c92 CW |
67 | ->setChain([ |
68 | 'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']], | |
69 | ])->execute(); | |
70 | $getFields = ['name', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'fk_entity']; | |
71 | foreach ($entities as $entity) { | |
72 | // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get | |
73 | if ($entity['get']) { | |
f9cf8797 CW |
74 | // Add paths (but only RUD actions) with translated titles |
75 | foreach ($entity['paths'] as $action => $path) { | |
76 | unset($entity['paths'][$action]); | |
77 | switch ($action) { | |
78 | case 'view': | |
79 | $title = ts('View %1', [1 => $entity['title']]); | |
80 | break; | |
81 | ||
5c385299 | 82 | case 'update': |
f9cf8797 CW |
83 | $title = ts('Edit %1', [1 => $entity['title']]); |
84 | break; | |
85 | ||
86 | case 'delete': | |
87 | $title = ts('Delete %1', [1 => $entity['title']]); | |
88 | break; | |
89 | ||
90 | default: | |
91 | continue 2; | |
92 | } | |
93 | $entity['paths'][] = [ | |
94 | 'path' => $path, | |
95 | 'title' => $title, | |
96 | 'action' => $action, | |
97 | ]; | |
98 | } | |
4f0729ed | 99 | $entity['fields'] = (array) civicrm_api4($entity['name'], 'getFields', [ |
22601c92 | 100 | 'select' => $getFields, |
393840b9 | 101 | 'where' => [['name', 'NOT IN', ['api_key', 'hash']]], |
22601c92 CW |
102 | 'orderBy' => ['label'], |
103 | ]); | |
104 | $params = $entity['get'][0]; | |
105 | // Entity must support at least these params or it is too weird for search kit | |
106 | if (!array_diff(['select', 'where', 'orderBy', 'limit', 'offset'], array_keys($params))) { | |
107 | \CRM_Utils_Array::remove($params, 'checkPermissions', 'debug', 'chain', 'language', 'select', 'where', 'orderBy', 'limit', 'offset'); | |
108 | unset($entity['get']); | |
109 | $schema[] = ['params' => array_keys($params)] + array_filter($entity); | |
110 | } | |
111 | } | |
112 | } | |
113 | return $schema; | |
114 | } | |
115 | ||
116 | /** | |
590c0e3f | 117 | * @param array $allowedEntities |
22601c92 CW |
118 | * @return array |
119 | */ | |
4f0729ed CW |
120 | public static function getJoins(array $allowedEntities) { |
121 | $joins = []; | |
122 | foreach ($allowedEntities as $entity) { | |
123 | if (!empty($entity['dao'])) { | |
124 | /* @var \CRM_Core_DAO $daoClass */ | |
125 | $daoClass = $entity['dao']; | |
126 | $references = $daoClass::getReferenceColumns(); | |
127 | // Only the first bridge reference gets processed, so if it's dynamic we want to be sure it's first in the list | |
128 | usort($references, function($reference) { | |
129 | return is_a($reference, 'CRM_Core_Reference_Dynamic') ? -1 : 1; | |
130 | }); | |
131 | $fields = array_column($entity['fields'], NULL, 'name'); | |
132 | $bridge = in_array('EntityBridge', $entity['type']) ? $entity['name'] : NULL; | |
133 | $baseEntity = $bridge && isset($entity['bridge'][1]) ? $allowedEntities[$fields[$entity['bridge'][1]]['fk_entity']] ?? NULL : NULL; | |
134 | if ($bridge && !$baseEntity) { | |
135 | continue; | |
136 | } | |
137 | foreach ($references as $reference) { | |
138 | $keyField = $fields[$reference->getReferenceKey()] ?? NULL; | |
139 | // Exclude any joins that are better represented by pseudoconstants | |
140 | if (is_a($reference, 'CRM_Core_Reference_OptionValue') | |
141 | || !$keyField || !empty($keyField['options']) | |
142 | // Limit bridge joins to just the first | |
143 | || $bridge && array_search($keyField['name'], $entity['bridge']) !== 0 | |
144 | // Sanity check - table should match | |
145 | || $daoClass::getTableName() !== $reference->getReferenceTable() | |
146 | ) { | |
147 | continue; | |
148 | } | |
149 | // Dynamic references use a column like "entity_table" | |
150 | $dynamicCol = $reference->getTypeColumn(); | |
151 | if ($dynamicCol) { | |
152 | $targetTables = $daoClass::buildOptions($dynamicCol); | |
153 | if (!$targetTables) { | |
154 | continue; | |
155 | } | |
156 | $targetTables = array_keys($targetTables); | |
157 | } | |
158 | else { | |
159 | $targetTables = [$reference->getTargetTable()]; | |
160 | } | |
161 | foreach ($targetTables as $targetTable) { | |
162 | $targetDao = \CRM_Core_DAO_AllCoreTables::getClassForTable($targetTable); | |
163 | $targetEntityName = \CRM_Core_DAO_AllCoreTables::getBriefName($targetDao); | |
164 | if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) { | |
165 | continue; | |
166 | } | |
167 | $targetEntity = $allowedEntities[$targetEntityName]; | |
168 | if (!$bridge) { | |
169 | // Add the straight 1-1 join | |
170 | $alias = $entity['name'] . '_' . $targetEntityName . '_' . $keyField['name']; | |
171 | $joins[$entity['name']][] = [ | |
172 | 'label' => $entity['title'] . ' ' . $targetEntity['title'], | |
173 | 'description' => $dynamicCol ? '' : $keyField['label'], | |
174 | 'entity' => $targetEntityName, | |
175 | 'conditions' => self::getJoinConditions($keyField['name'], $alias . '.' . $reference->getTargetKey(), $targetTable, $dynamicCol), | |
176 | 'alias' => $alias, | |
177 | 'multi' => FALSE, | |
178 | ]; | |
179 | // Flip the conditions & add the reverse (1-n) join | |
180 | $alias = $targetEntityName . '_' . $entity['name'] . '_' . $keyField['name']; | |
181 | $joins[$targetEntityName][] = [ | |
182 | 'label' => $targetEntity['title'] . ' ' . $entity['title_plural'], | |
183 | 'description' => $dynamicCol ? '' : $keyField['label'], | |
184 | 'entity' => $entity['name'], | |
185 | 'conditions' => self::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ? $alias . '.' . $dynamicCol : NULL), | |
186 | 'alias' => $alias, | |
187 | 'multi' => TRUE, | |
188 | ]; | |
189 | } | |
190 | else { | |
191 | // Add joins for the two entities that connect through this bridge (n-n) | |
192 | $symmetric = $baseEntity['name'] === $targetEntityName; | |
193 | $targetsTitle = $symmetric ? $allowedEntities[$bridge]['title_plural'] : $targetEntity['title_plural']; | |
194 | $joins[$baseEntity['name']][] = [ | |
195 | 'label' => $baseEntity['title'] . ' ' . $targetsTitle, | |
196 | 'description' => ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]), | |
197 | 'entity' => $targetEntityName, | |
198 | 'conditions' => [$bridge], | |
199 | 'bridge' => $bridge, | |
200 | 'alias' => $baseEntity['name'] . "_{$bridge}_" . $targetEntityName, | |
201 | 'multi' => TRUE, | |
202 | ]; | |
203 | if (!$symmetric) { | |
204 | $joins[$targetEntityName][] = [ | |
205 | 'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'], | |
206 | 'description' => ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]), | |
207 | 'entity' => $baseEntity['name'], | |
208 | 'conditions' => [$bridge], | |
209 | 'bridge' => $bridge, | |
210 | 'alias' => $targetEntityName . "_{$bridge}_" . $baseEntity['name'], | |
211 | 'multi' => TRUE, | |
212 | ]; | |
213 | } | |
214 | } | |
215 | } | |
22601c92 CW |
216 | } |
217 | } | |
22601c92 | 218 | } |
4f0729ed CW |
219 | return $joins; |
220 | } | |
221 | ||
222 | /** | |
223 | * Boilerplate join clause | |
224 | * | |
225 | * @param string $nearCol | |
226 | * @param string $farCol | |
227 | * @param string $targetTable | |
228 | * @param string|null $dynamicCol | |
229 | * @return array[] | |
230 | */ | |
231 | private static function getJoinConditions($nearCol, $farCol, $targetTable, $dynamicCol) { | |
232 | $conditions = [ | |
233 | [ | |
234 | $nearCol, | |
235 | '=', | |
236 | $farCol, | |
237 | ], | |
238 | ]; | |
239 | if ($dynamicCol) { | |
240 | $conditions[] = [ | |
241 | $dynamicCol, | |
242 | '=', | |
243 | "'$targetTable'", | |
244 | ]; | |
245 | } | |
246 | return $conditions; | |
22601c92 CW |
247 | } |
248 | ||
249 | } |