Merge pull request #21807 from braders/feature/replace-spacer-img-elements
[civicrm-core.git] / Civi / Token / TokenRow.php
1 <?php
2 namespace Civi\Token;
3
4 use Brick\Money\Money;
5
6 /**
7 * Class TokenRow
8 *
9 * @package Civi\Token
10 *
11 * A TokenRow is a helper/stub providing simplified access to the TokenProcessor.
12 * There are two common cases for using the TokenRow stub:
13 *
14 * (1) When setting up a job, you may specify general/baseline info.
15 * This is called the "context" data. Here, we create two rows:
16 *
17 * ```
18 * $proc->addRow()->context('contact_id', 123);
19 * $proc->addRow()->context('contact_id', 456);
20 * ```
21 *
22 * (2) When defining a token (eg `{profile.viewUrl}`), you might read the
23 * context-data (`contact_id`) and set the token-data (`profile => viewUrl`):
24 *
25 * ```
26 * foreach ($proc->getRows() as $row) {
27 * $row->tokens('profile', [
28 * 'viewUrl' => 'http://example.com/profile?cid=' . urlencode($row->context['contact_id'];
29 * ]);
30 * }
31 * ```
32 *
33 * The context and tokens can be accessed using either methods or attributes.
34 *
35 * ```
36 * # Setting context data
37 * $row->context('contact_id', 123);
38 * $row->context(['contact_id' => 123]);
39 *
40 * # Setting token data
41 * $row->tokens('profile', ['viewUrl' => 'http://example.com/profile?cid=123']);
42 * $row->tokens('profile', 'viewUrl, 'http://example.com/profile?cid=123');
43 *
44 * # Reading context data
45 * echo $row->context['contact_id'];
46 *
47 * # Reading token data
48 * echo $row->tokens['profile']['viewUrl'];
49 * ```
50 *
51 * Note: The methods encourage a "fluent" style. They were written for PHP 5.3
52 * (eg before short-array syntax was supported) and are fairly flexible about
53 * input notations (e.g. `context(string $key, mixed $value)` vs `context(array $keyValuePairs)`).
54 *
55 * Note: An instance of `TokenRow` is a stub which only contains references to the
56 * main data in `TokenProcessor`. There may be several `TokenRow` stubs
57 * referencing the same `TokenProcessor`. You can think of `TokenRow` objects as
58 * lightweight and disposable.
59 */
60 class TokenRow {
61
62 /**
63 * The token-processor is where most data is actually stored.
64 *
65 * Note: Not intended for public usage. However, this is marked public to allow
66 * interaction classes in this package (`TokenProcessor`<=>`TokenRow`<=>`TokenRowContext`).
67 *
68 * @var TokenProcessor
69 */
70 public $tokenProcessor;
71
72 /**
73 * Row ID - the record within TokenProcessor that we're accessing.
74 *
75 * @var int
76 */
77 public $tokenRow;
78
79 /**
80 * The MIME type associated with new token-values.
81 *
82 * This is generally manipulated as part of a fluent chain, eg
83 *
84 * $row->format('text/plain')->token(['display_name', 'Alice Bobdaughter']);
85 *
86 * @var string
87 */
88 public $format;
89
90 /**
91 * @var array|\ArrayAccess
92 * List of token values.
93 * This is a facade for the TokenProcessor::$rowValues.
94 * Ex: ['contact' => ['display_name' => 'Alice']]
95 */
96 public $tokens;
97
98 /**
99 * @var array|\ArrayAccess
100 * List of context values.
101 * This is a facade for the TokenProcessor::$rowContexts.
102 * Ex: ['controller' => 'CRM_Foo_Bar']
103 */
104 public $context;
105
106 public function __construct(TokenProcessor $tokenProcessor, $key) {
107 $this->tokenProcessor = $tokenProcessor;
108 $this->tokenRow = $key;
109 // Set a default.
110 $this->format('text/plain');
111 $this->context = new TokenRowContext($tokenProcessor, $key);
112 }
113
114 /**
115 * @param string $format
116 * @return TokenRow
117 */
118 public function format($format) {
119 $this->format = $format;
120 $this->tokens = &$this->tokenProcessor->rowValues[$this->tokenRow][$format];
121 return $this;
122 }
123
124 /**
125 * Update the value of a context element.
126 *
127 * @param string|array $a
128 * @param mixed $b
129 * @return TokenRow
130 */
131 public function context($a = NULL, $b = NULL) {
132 if (is_array($a)) {
133 \CRM_Utils_Array::extend($this->tokenProcessor->rowContexts[$this->tokenRow], $a);
134 }
135 elseif (is_array($b)) {
136 \CRM_Utils_Array::extend($this->tokenProcessor->rowContexts[$this->tokenRow][$a], $b);
137 }
138 else {
139 $this->tokenProcessor->rowContexts[$this->tokenRow][$a] = $b;
140 }
141 return $this;
142 }
143
144 /**
145 * Update the value of a token.
146 *
147 * @param string|array $a
148 * @param string|array $b
149 * @param mixed $c
150 * @return TokenRow
151 */
152 public function tokens($a = NULL, $b = NULL, $c = NULL) {
153 if (is_array($a)) {
154 \CRM_Utils_Array::extend($this->tokens, $a);
155 }
156 elseif (is_array($b)) {
157 \CRM_Utils_Array::extend($this->tokens[$a], $b);
158 }
159 elseif (is_array($c)) {
160 \CRM_Utils_Array::extend($this->tokens[$a][$b], $c);
161 }
162 elseif ($c === NULL) {
163 $this->tokens[$a] = $b;
164 }
165 else {
166 $this->tokens[$a][$b] = $c;
167 }
168 return $this;
169 }
170
171 /**
172 * Update the value of a custom field token.
173 *
174 * @param string $entity
175 * @param int $customFieldID
176 * @param int $entityID
177 * @return TokenRow
178 */
179 public function customToken($entity, $customFieldID, $entityID) {
180 $customFieldName = 'custom_' . $customFieldID;
181 $record = civicrm_api3($entity, 'getSingle', [
182 'return' => $customFieldName,
183 'id' => $entityID,
184 ]);
185 $fieldValue = \CRM_Utils_Array::value($customFieldName, $record, '');
186
187 // format the raw custom field value into proper display value
188 if (isset($fieldValue)) {
189 $fieldValue = (string) \CRM_Core_BAO_CustomField::displayValue($fieldValue, $customFieldID);
190 }
191
192 return $this->format('text/html')->tokens($entity, $customFieldName, $fieldValue);
193 }
194
195 /**
196 * Update the value of a token. Apply formatting based on DB schema.
197 *
198 * @param string $tokenEntity
199 * @param string $tokenField
200 * @param string $baoName
201 * @param string $baoField
202 * @param mixed $fieldValue
203 * @return TokenRow
204 * @throws \CRM_Core_Exception
205 */
206 public function dbToken($tokenEntity, $tokenField, $baoName, $baoField, $fieldValue) {
207 \CRM_Core_Error::deprecatedFunctionWarning('no alternative');
208 if ($fieldValue === NULL || $fieldValue === '') {
209 return $this->tokens($tokenEntity, $tokenField, '');
210 }
211
212 $fields = $baoName::fields();
213 if (!empty($fields[$baoField]['pseudoconstant'])) {
214 $options = $baoName::buildOptions($baoField, 'get');
215 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $options[$fieldValue]);
216 }
217
218 switch ($fields[$baoField]['type']) {
219 case \CRM_Utils_Type::T_DATE + \CRM_Utils_Type::T_TIME:
220 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Date::customFormat($fieldValue));
221
222 case \CRM_Utils_Type::T_MONEY:
223 // Is this something you should ever use? Seems like you need more context
224 // to know which currency to use.
225 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Money::format($fieldValue));
226
227 case \CRM_Utils_Type::T_STRING:
228 case \CRM_Utils_Type::T_BOOLEAN:
229 case \CRM_Utils_Type::T_INT:
230 case \CRM_Utils_Type::T_TEXT:
231 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $fieldValue);
232
233 }
234
235 throw new \CRM_Core_Exception("Cannot format token for field '$baoField' in '$baoName'");
236 }
237
238 /**
239 * Auto-convert between different formats
240 *
241 * @param string $format
242 *
243 * @return TokenRow
244 */
245 public function fill($format = NULL) {
246 if ($format === NULL) {
247 $format = $this->format;
248 }
249
250 if (!isset($this->tokenProcessor->rowValues[$this->tokenRow]['text/html'])) {
251 $this->tokenProcessor->rowValues[$this->tokenRow]['text/html'] = [];
252 }
253 if (!isset($this->tokenProcessor->rowValues[$this->tokenRow]['text/plain'])) {
254 $this->tokenProcessor->rowValues[$this->tokenRow]['text/plain'] = [];
255 }
256
257 $htmlTokens = &$this->tokenProcessor->rowValues[$this->tokenRow]['text/html'];
258 $textTokens = &$this->tokenProcessor->rowValues[$this->tokenRow]['text/plain'];
259
260 switch ($format) {
261 case 'text/html':
262 // Plain => HTML.
263 foreach ($textTokens as $entity => $values) {
264 $entityFields = civicrm_api3($entity, "getFields", ['api_action' => 'get']);
265 foreach ($values as $field => $value) {
266 if (!isset($htmlTokens[$entity][$field])) {
267 // CRM-18420 - Activity Details Field are enclosed within <p>,
268 // hence if $body_text is empty, htmlentities will lead to
269 // conversion of these tags resulting in raw HTML.
270 if ($entity == 'activity' && $field == 'details') {
271 $htmlTokens[$entity][$field] = $value;
272 }
273 elseif (\CRM_Utils_Array::value('data_type', \CRM_Utils_Array::value($field, $entityFields['values'])) == 'Memo') {
274 // Memo fields aka custom fields of type Note are html.
275 $htmlTokens[$entity][$field] = \CRM_Utils_String::purifyHTML($value);
276 }
277 else {
278 $htmlTokens[$entity][$field] = is_object($value) ? $value : htmlentities($value);
279 }
280 }
281 }
282 }
283 break;
284
285 case 'text/plain':
286 // HTML => Plain.
287 foreach ($htmlTokens as $entity => $values) {
288 foreach ($values as $field => $value) {
289 if (!$value instanceof \DateTime && !$value instanceof Money) {
290 $value = html_entity_decode(strip_tags($value));
291 }
292 if (!isset($textTokens[$entity][$field])) {
293 $textTokens[$entity][$field] = $value;
294 }
295 }
296 }
297 break;
298
299 default:
300 throw new \RuntimeException('Invalid format');
301 }
302
303 return $this;
304 }
305
306 /**
307 * Render a message.
308 *
309 * @param string $name
310 * The name previously registered with TokenProcessor::addMessage.
311 * @return string
312 * Fully rendered message, with tokens merged.
313 */
314 public function render($name) {
315 return $this->tokenProcessor->render($name, $this);
316 }
317
318 }
319
320 /**
321 * Class TokenRowContext
322 * @package Civi\Token
323 *
324 * Combine the row-context and general-context into a single array-like facade.
325 */
326 class TokenRowContext implements \ArrayAccess, \IteratorAggregate, \Countable {
327
328 /**
329 * @var TokenProcessor
330 */
331 protected $tokenProcessor;
332
333 protected $tokenRow;
334
335 /**
336 * Class constructor.
337 *
338 * @param array $tokenProcessor
339 * @param array $tokenRow
340 */
341 public function __construct($tokenProcessor, $tokenRow) {
342 $this->tokenProcessor = $tokenProcessor;
343 $this->tokenRow = $tokenRow;
344 }
345
346 /**
347 * Does offset exist.
348 *
349 * @param mixed $offset
350 *
351 * @return bool
352 */
353 public function offsetExists($offset) {
354 return isset($this->tokenProcessor->rowContexts[$this->tokenRow][$offset])
355 || isset($this->tokenProcessor->context[$offset]);
356 }
357
358 /**
359 * Get offset.
360 *
361 * @param string $offset
362 *
363 * @return string
364 */
365 public function &offsetGet($offset) {
366 if (isset($this->tokenProcessor->rowContexts[$this->tokenRow][$offset])) {
367 return $this->tokenProcessor->rowContexts[$this->tokenRow][$offset];
368 }
369 if (isset($this->tokenProcessor->context[$offset])) {
370 return $this->tokenProcessor->context[$offset];
371 }
372 $val = NULL;
373 return $val;
374 }
375
376 /**
377 * Set offset.
378 *
379 * @param string $offset
380 * @param mixed $value
381 */
382 public function offsetSet($offset, $value) {
383 $this->tokenProcessor->rowContexts[$this->tokenRow][$offset] = $value;
384 }
385
386 /**
387 * Unset offset.
388 *
389 * @param mixed $offset
390 */
391 public function offsetUnset($offset) {
392 unset($this->tokenProcessor->rowContexts[$this->tokenRow][$offset]);
393 }
394
395 /**
396 * Get iterator.
397 *
398 * @return \ArrayIterator
399 */
400 public function getIterator() {
401 return new \ArrayIterator($this->createMergedArray());
402 }
403
404 /**
405 * Count.
406 *
407 * @return int
408 */
409 public function count() {
410 return count($this->createMergedArray());
411 }
412
413 /**
414 * Create merged array.
415 *
416 * @return array
417 */
418 protected function createMergedArray() {
419 return array_merge(
420 $this->tokenProcessor->rowContexts[$this->tokenRow],
421 $this->tokenProcessor->context
422 );
423 }
424
425 }