Commit | Line | Data |
---|---|---|
17b6f179 EM |
1 | <?php |
2 | ||
3 | /* | |
4 | +--------------------------------------------------------------------+ | |
5 | | Copyright CiviCRM LLC. All rights reserved. | | |
6 | | | | |
7 | | This work is published under the GNU AGPLv3 license with some | | |
8 | | permitted exceptions and without any warranty. For full license | | |
9 | | and copyright information, see https://civicrm.org/licensing | | |
10 | +--------------------------------------------------------------------+ | |
11 | */ | |
12 | ||
13 | use Civi\Token\AbstractTokenSubscriber; | |
14 | use Civi\Token\TokenRow; | |
45e8391a EM |
15 | use Civi\ActionSchedule\Event\MailingQueryEvent; |
16 | use Civi\Token\TokenProcessor; | |
17b6f179 EM |
17 | |
18 | /** | |
19 | * Class CRM_Core_EntityTokens | |
20 | * | |
21 | * Parent class for generic entity token functionality. | |
22 | * | |
23 | * WARNING - this class is highly likely to be temporary and | |
24 | * to be consolidated with the TokenTrait and / or the | |
25 | * AbstractTokenSubscriber in future. It is being used to clarify | |
26 | * functionality but should NOT be used from outside of core tested code. | |
27 | */ | |
28 | class CRM_Core_EntityTokens extends AbstractTokenSubscriber { | |
29 | ||
eba15732 EM |
30 | /** |
31 | * @var array | |
32 | */ | |
33 | protected $prefetch = []; | |
34 | ||
17b6f179 | 35 | /** |
17b6f179 | 36 | * @inheritDoc |
45e8391a | 37 | * @throws \CRM_Core_Exception |
17b6f179 EM |
38 | */ |
39 | public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) { | |
eba15732 | 40 | $this->prefetch = (array) $prefetch; |
45e8391a EM |
41 | $fieldValue = $this->getFieldValue($row, $field); |
42 | ||
43 | if ($this->isPseudoField($field)) { | |
0f4031da EM |
44 | if (!empty($fieldValue)) { |
45 | // If it's set here it has already been loaded in pre-fetch. | |
46 | return $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue); | |
47 | } | |
48 | // Once prefetch is fully standardised we can remove this - as long | |
49 | // as tests pass we should be fine as tests cover this. | |
45e8391a EM |
50 | $split = explode(':', $field); |
51 | return $row->tokens($entity, $field, $this->getPseudoValue($split[0], $split[1], $this->getFieldValue($row, $split[0]))); | |
52 | } | |
0f4031da EM |
53 | if ($this->isCustomField($field)) { |
54 | return $row->customToken($entity, \CRM_Core_BAO_CustomField::getKeyID($field), $this->getFieldValue($row, 'id')); | |
55 | } | |
45e8391a EM |
56 | if ($this->isMoneyField($field)) { |
57 | return $row->format('text/plain')->tokens($entity, $field, | |
eba15732 | 58 | \CRM_Utils_Money::format($fieldValue, $this->getCurrency($row))); |
45e8391a EM |
59 | } |
60 | if ($this->isDateField($field)) { | |
61 | return $row->format('text/plain')->tokens($entity, $field, \CRM_Utils_Date::customFormat($fieldValue)); | |
62 | } | |
0f4031da | 63 | $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue); |
17b6f179 EM |
64 | } |
65 | ||
45e8391a EM |
66 | /** |
67 | * Metadata about the entity fields. | |
68 | * | |
69 | * @var array | |
70 | */ | |
71 | protected $fieldMetadata = []; | |
72 | ||
29f2b53e EM |
73 | /** |
74 | * Get the entity name for api v4 calls. | |
75 | * | |
76 | * @return string | |
77 | */ | |
78 | protected function getApiEntityName(): string { | |
79 | return ''; | |
80 | } | |
81 | ||
82 | /** | |
83 | * Get the entity alias to use within queries. | |
84 | * | |
85 | * The default has a double underscore which should prevent any | |
86 | * ambiguity with an existing table name. | |
87 | * | |
88 | * @return string | |
89 | */ | |
90 | protected function getEntityAlias(): string { | |
91 | return $this->getApiEntityName() . '__'; | |
92 | } | |
93 | ||
3fcf0983 EM |
94 | /** |
95 | * Get the name of the table this token class can extend. | |
96 | * | |
97 | * The default is based on the entity but some token classes, | |
98 | * specifically the event class, latch on to other tables - ie | |
99 | * the participant table. | |
100 | */ | |
101 | public function getExtendableTableName(): string { | |
102 | return CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getApiEntityName()); | |
103 | } | |
104 | ||
29f2b53e EM |
105 | /** |
106 | * Get the relevant bao name. | |
107 | */ | |
108 | public function getBAOName(): string { | |
109 | return CRM_Core_DAO_AllCoreTables::getFullName($this->getApiEntityName()); | |
110 | } | |
111 | ||
3fcf0983 EM |
112 | /** |
113 | * Get an array of fields to be requested. | |
114 | * | |
115 | * @return string[] | |
116 | */ | |
117 | public function getReturnFields(): array { | |
118 | return array_keys($this->getBasicTokens()); | |
119 | } | |
120 | ||
4a526c1b EM |
121 | /** |
122 | * Get all the tokens supported by this processor. | |
123 | * | |
124 | * @return array|string[] | |
125 | */ | |
126 | public function getAllTokens(): array { | |
97ca72e4 EM |
127 | $basicTokens = $this->getBasicTokens(); |
128 | foreach (array_keys($basicTokens) as $fieldName) { | |
129 | // The goal is to be able to render more complete tokens | |
130 | // (eg. actual booleans, field names, raw ids) for a more | |
131 | // advanced audiences - ie those using conditionals | |
132 | // and to specify that audience in the api that retrieves. | |
133 | // But, for now, let's not advertise, given that most of these fields | |
134 | // aren't really needed even once... | |
135 | if ($this->isBooleanField($fieldName)) { | |
136 | unset($basicTokens[$fieldName]); | |
137 | } | |
138 | } | |
139 | return array_merge($basicTokens, $this->getPseudoTokens(), CRM_Utils_Token::getCustomFieldTokens($this->getApiEntityName())); | |
140 | } | |
141 | ||
142 | /** | |
143 | * Is the given field a boolean field. | |
144 | * | |
145 | * @param string $fieldName | |
146 | * | |
147 | * @return bool | |
148 | */ | |
149 | public function isBooleanField(string $fieldName): bool { | |
150 | return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Boolean'; | |
4a526c1b EM |
151 | } |
152 | ||
17b6f179 EM |
153 | /** |
154 | * Is the given field a date field. | |
155 | * | |
156 | * @param string $fieldName | |
157 | * | |
158 | * @return bool | |
159 | */ | |
160 | public function isDateField(string $fieldName): bool { | |
0f4031da | 161 | return in_array($this->getFieldMetadata()[$fieldName]['data_type'], ['Timestamp', 'Date'], TRUE); |
29f2b53e EM |
162 | } |
163 | ||
164 | /** | |
165 | * Is the given field a pseudo field. | |
166 | * | |
167 | * @param string $fieldName | |
168 | * | |
169 | * @return bool | |
170 | */ | |
171 | public function isPseudoField(string $fieldName): bool { | |
172 | return strpos($fieldName, ':') !== FALSE; | |
173 | } | |
174 | ||
175 | /** | |
176 | * Is the given field a custom field. | |
177 | * | |
178 | * @param string $fieldName | |
179 | * | |
180 | * @return bool | |
181 | */ | |
182 | public function isCustomField(string $fieldName) : bool { | |
183 | return (bool) \CRM_Core_BAO_CustomField::getKeyID($fieldName); | |
17b6f179 EM |
184 | } |
185 | ||
186 | /** | |
187 | * Is the given field a date field. | |
188 | * | |
189 | * @param string $fieldName | |
190 | * | |
191 | * @return bool | |
192 | */ | |
193 | public function isMoneyField(string $fieldName): bool { | |
29f2b53e | 194 | return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Money'; |
17b6f179 EM |
195 | } |
196 | ||
197 | /** | |
198 | * Get the metadata for the available fields. | |
199 | * | |
200 | * @return array | |
201 | */ | |
202 | protected function getFieldMetadata(): array { | |
203 | if (empty($this->fieldMetadata)) { | |
29f2b53e EM |
204 | try { |
205 | // Tests fail without checkPermissions = FALSE | |
206 | $this->fieldMetadata = (array) civicrm_api4($this->getApiEntityName(), 'getfields', ['checkPermissions' => FALSE], 'name'); | |
207 | } | |
208 | catch (API_Exception $e) { | |
209 | $this->fieldMetadata = []; | |
17b6f179 EM |
210 | } |
211 | } | |
212 | return $this->fieldMetadata; | |
213 | } | |
214 | ||
29f2b53e EM |
215 | /** |
216 | * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant. | |
217 | * | |
218 | * @internal - this function will likely be made protected soon. | |
219 | * | |
220 | * @return array | |
221 | */ | |
222 | public function getPseudoTokens(): array { | |
223 | $return = []; | |
224 | foreach (array_keys($this->getBasicTokens()) as $fieldName) { | |
225 | if ($this->isAddPseudoTokens($fieldName)) { | |
3d9604f0 EM |
226 | $fieldLabel = $this->fieldMetadata[$fieldName]['input_attrs']['label'] ?? $this->fieldMetadata[$fieldName]['label']; |
227 | $return[$fieldName . ':label'] = $fieldLabel; | |
228 | $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $fieldLabel; | |
29f2b53e | 229 | } |
97ca72e4 EM |
230 | if ($this->isBooleanField($fieldName)) { |
231 | $return[$fieldName . ':label'] = $this->getFieldMetadata()[$fieldName]['title']; | |
232 | } | |
29f2b53e EM |
233 | } |
234 | return $return; | |
235 | } | |
236 | ||
237 | /** | |
238 | * Is this a field we should add pseudo-tokens to? | |
239 | * | |
240 | * Pseudo-tokens allow access to name and label fields - e.g | |
241 | * | |
242 | * {contribution.contribution_status_id:name} might resolve to 'Completed' | |
243 | * | |
244 | * @param string $fieldName | |
245 | */ | |
246 | public function isAddPseudoTokens($fieldName): bool { | |
247 | if ($fieldName === 'currency') { | |
248 | // 'currency' is manually added to the skip list as an anomaly. | |
249 | // name & label aren't that suitable for 'currency' (symbol, which | |
250 | // possibly maps to 'abbr' would be) and we can't gather that | |
251 | // from the metadata as yet. | |
252 | return FALSE; | |
253 | } | |
bd6b5299 EM |
254 | if ($this->getFieldMetadata()[$fieldName]['type'] === 'Custom') { |
255 | // If we remove this early return then we get that extra nuanced goodness | |
256 | // and support for the more portable v4 style field names | |
257 | // on custom fields - where labels or names can be returned. | |
258 | // At present the gap is that the metadata for the label is not accessed | |
259 | // and tests failed on the enotice and we don't have a clear plan about | |
260 | // v4 style custom tokens - but medium term this IF will probably go. | |
261 | return FALSE; | |
262 | } | |
3d9604f0 | 263 | return (bool) ($this->getFieldMetadata()[$fieldName]['options'] || !empty($this->getFieldMetadata()[$fieldName]['suffixes'])); |
29f2b53e EM |
264 | } |
265 | ||
266 | /** | |
267 | * Get the value for the relevant pseudo field. | |
268 | * | |
269 | * @param string $realField e.g contribution_status_id | |
270 | * @param string $pseudoKey e.g name | |
271 | * @param int|string $fieldValue e.g 1 | |
272 | * | |
273 | * @return string | |
274 | * Eg. 'Completed' in the example above. | |
275 | * | |
276 | * @internal function will likely be protected soon. | |
277 | */ | |
278 | public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { | |
279 | if ($pseudoKey === 'name') { | |
280 | $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue); | |
281 | } | |
282 | if ($pseudoKey === 'label') { | |
283 | $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($this->getBAOName(), $realField, $fieldValue); | |
284 | } | |
285 | return (string) $fieldValue; | |
286 | } | |
287 | ||
9c9c61af EM |
288 | /** |
289 | * @param \Civi\Token\TokenRow $row | |
290 | * @param string $field | |
291 | * @return string|int | |
292 | */ | |
293 | protected function getFieldValue(TokenRow $row, string $field) { | |
294 | $actionSearchResult = $row->context['actionSearchResult']; | |
295 | $aliasedField = $this->getEntityAlias() . $field; | |
eba15732 EM |
296 | if (isset($actionSearchResult->{$aliasedField})) { |
297 | return $actionSearchResult->{$aliasedField}; | |
298 | } | |
299 | $entityID = $row->context[$this->getEntityIDField()]; | |
300 | return $this->prefetch[$entityID][$field] ?? ''; | |
9c9c61af EM |
301 | } |
302 | ||
45e8391a EM |
303 | /** |
304 | * Class constructor. | |
305 | */ | |
306 | public function __construct() { | |
307 | $tokens = $this->getAllTokens(); | |
308 | parent::__construct($this->getEntityName(), $tokens); | |
309 | } | |
310 | ||
311 | /** | |
312 | * Check if the token processor is active. | |
313 | * | |
314 | * @param \Civi\Token\TokenProcessor $processor | |
315 | * | |
316 | * @return bool | |
317 | */ | |
318 | public function checkActive(TokenProcessor $processor) { | |
eba15732 EM |
319 | return (!empty($processor->context['actionMapping']) |
320 | // This makes the 'schema context compulsory - which feels accidental | |
321 | // since recent discu | |
322 | && $processor->context['actionMapping']->getEntity()) || in_array($this->getEntityIDField(), $processor->context['schema']); | |
45e8391a EM |
323 | } |
324 | ||
325 | /** | |
326 | * Alter action schedule query. | |
327 | * | |
328 | * @param \Civi\ActionSchedule\Event\MailingQueryEvent $e | |
329 | */ | |
330 | public function alterActionScheduleQuery(MailingQueryEvent $e): void { | |
331 | if ($e->mapping->getEntity() !== $this->getExtendableTableName()) { | |
332 | return; | |
333 | } | |
334 | foreach ($this->getReturnFields() as $token) { | |
335 | $e->query->select('e.' . $token . ' AS ' . $this->getEntityAlias() . $token); | |
336 | } | |
337 | } | |
338 | ||
bd6b5299 EM |
339 | /** |
340 | * Get tokens supporting the syntax we are migrating to. | |
341 | * | |
342 | * In general these are tokens that were not previously supported | |
343 | * so we can add them in the preferred way or that we have | |
344 | * undertaken some, as yet to be written, db update. | |
345 | * | |
346 | * See https://lab.civicrm.org/dev/core/-/issues/2650 | |
347 | * | |
348 | * @return string[] | |
349 | * @throws \API_Exception | |
350 | */ | |
351 | public function getBasicTokens(): array { | |
352 | $return = []; | |
353 | foreach ($this->getExposedFields() as $fieldName) { | |
c2f1897e EM |
354 | // Custom fields are still added v3 style - we want to keep v4 naming 'unpoluted' |
355 | // for now to allow us to consider how to handle names vs labels vs values | |
356 | // and other raw vs not raw options. | |
357 | if ($this->getFieldMetadata()[$fieldName]['type'] !== 'Custom') { | |
358 | $return[$fieldName] = $this->getFieldMetadata()[$fieldName]['title']; | |
359 | } | |
bd6b5299 EM |
360 | } |
361 | return $return; | |
362 | } | |
363 | ||
364 | /** | |
365 | * Get entity fields that should be exposed as tokens. | |
366 | * | |
367 | * @return string[] | |
368 | * | |
369 | */ | |
370 | public function getExposedFields(): array { | |
371 | $return = []; | |
372 | foreach ($this->getFieldMetadata() as $field) { | |
373 | if (!in_array($field['name'], $this->getSkippedFields(), TRUE)) { | |
374 | $return[] = $field['name']; | |
375 | } | |
376 | } | |
377 | return $return; | |
378 | } | |
379 | ||
380 | /** | |
381 | * Get entity fields that should not be exposed as tokens. | |
382 | * | |
383 | * @return string[] | |
384 | */ | |
385 | public function getSkippedFields(): array { | |
386 | $fields = ['contact_id']; | |
387 | if (!CRM_Campaign_BAO_Campaign::isCampaignEnable()) { | |
388 | $fields[] = 'campaign_id'; | |
389 | } | |
390 | return $fields; | |
391 | } | |
392 | ||
393 | /** | |
394 | * @return string | |
395 | */ | |
396 | protected function getEntityName(): string { | |
397 | return CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($this->getApiEntityName()); | |
398 | } | |
399 | ||
eba15732 EM |
400 | public function getEntityIDField() { |
401 | return $this->getEntityName() . 'Id'; | |
402 | } | |
403 | ||
404 | public function prefetch(\Civi\Token\Event\TokenValueEvent $e): ?array { | |
405 | $entityIDs = $e->getTokenProcessor()->getContextValues($this->getEntityIDField()); | |
406 | if (empty($entityIDs)) { | |
407 | return []; | |
408 | } | |
409 | $select = $this->getPrefetchFields($e); | |
410 | $result = (array) civicrm_api4($this->getApiEntityName(), 'get', [ | |
411 | 'checkPermissions' => FALSE, | |
412 | // Note custom fields are not yet added - I need to | |
413 | // re-do the unit tests to support custom fields first. | |
414 | 'select' => $select, | |
415 | 'where' => [['id', 'IN', $entityIDs]], | |
416 | ], 'id'); | |
417 | return $result; | |
418 | } | |
419 | ||
420 | public function getCurrencyFieldName() { | |
421 | return []; | |
422 | } | |
423 | ||
424 | /** | |
425 | * Get the currency to use for formatting money. | |
426 | * @param $row | |
427 | * | |
428 | * @return string | |
429 | */ | |
430 | public function getCurrency($row): string { | |
431 | if (!empty($this->getCurrencyFieldName())) { | |
432 | return $this->getFieldValue($row, $this->getCurrencyFieldName()[0]); | |
433 | } | |
434 | return CRM_Core_Config::singleton()->defaultCurrency; | |
435 | } | |
436 | ||
437 | public function getPrefetchFields(\Civi\Token\Event\TokenValueEvent $e): array { | |
0f4031da | 438 | return array_intersect(array_merge($this->getActiveTokens($e), $this->getCurrencyFieldName()), array_keys($this->getAllTokens())); |
eba15732 EM |
439 | } |
440 | ||
17b6f179 | 441 | } |