Afform - Fix help_pre and help_post from custom fields
[civicrm-core.git] / ext / afform / core / Civi / Afform / AfformMetadataInjector.php
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\Afform;
13
14 use CRM_Afform_ExtensionUtil as E;
15
16 /**
17 * Class AfformMetadataInjector
18 * @package Civi\Afform
19 */
20 class AfformMetadataInjector {
21
22 /**
23 * @param \Civi\Core\Event\GenericHookEvent $e
24 * @see CRM_Utils_Hook::alterAngular()
25 */
26 public static function preprocess($e) {
27 $changeSet = \Civi\Angular\ChangeSet::create('fieldMetadata')
28 ->alterHtml(';\\.aff\\.html$;', function($doc, $path) {
29 try {
30 $module = \Civi::service('angular')->getModule(basename($path, '.aff.html'));
31 $meta = \Civi\Api4\Afform::get()->addWhere('name', '=', $module['_afform'])->setSelect(['join', 'block'])->setCheckPermissions(FALSE)->execute()->first();
32 }
33 catch (\Exception $e) {
34 }
35
36 $blockEntity = $meta['join'] ?? $meta['block'] ?? NULL;
37 if (!$blockEntity) {
38 $entities = self::getFormEntities($doc);
39 }
40
41 // Each field can be nested within a fieldset, a join or a block
42 foreach (pq('af-field', $doc) as $afField) {
43 /** @var \DOMElement $afField */
44 $action = 'create';
45 $joinName = pq($afField)->parents('[af-join]')->attr('af-join');
46 if ($joinName) {
47 self::fillFieldMetadata($joinName, $action, $afField);
48 continue;
49 }
50 if ($blockEntity) {
51 self::fillFieldMetadata($blockEntity, $action, $afField);
52 continue;
53 }
54 // Not a block or a join, get metadata from fieldset
55 $fieldset = pq($afField)->parents('[af-fieldset]');
56 $apiEntities = pq($fieldset)->attr('api-entities');
57 // If this fieldset is standalone (not linked to an af-entity) it is for get rather than create
58 if ($apiEntities) {
59 $action = 'get';
60 $entityType = self::getFieldEntityType($afField->getAttribute('name'), \CRM_Utils_JS::decode($apiEntities));
61 }
62 else {
63 $entityName = pq($fieldset)->attr('af-fieldset');
64 if (!preg_match(';^[a-zA-Z0-9\_\-\. ]+$;', $entityName)) {
65 \Civi::log()->error("Afform error: cannot process $path: malformed entity name ($entityName)");
66 return;
67 }
68 $entityType = $entities[$entityName]['type'];
69 }
70 self::fillFieldMetadata($entityType, $action, $afField);
71 }
72 });
73 $e->angular->add($changeSet);
74 }
75
76 /**
77 * Merge field definition metadata into an afform field's definition
78 *
79 * @param string $entityName
80 * @param string $action
81 * @param \DOMElement $afField
82 * @throws \API_Exception
83 */
84 private static function fillFieldMetadata($entityName, $action, \DOMElement $afField) {
85 $fieldName = $afField->getAttribute('name');
86 // For explicit joins, strip the alias off the field name
87 if (strpos($entityName, ' AS ')) {
88 [$entityName, $alias] = explode(' AS ', $entityName);
89 $fieldName = preg_replace('/^' . preg_quote($alias . '.', '/') . '/', '', $fieldName);
90 }
91 $params = [
92 'action' => $action,
93 'where' => [['name', '=', $fieldName]],
94 'select' => ['label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options'],
95 'loadOptions' => ['id', 'label'],
96 // If the admin included this field on the form, then it's OK to get metadata about the field regardless of user permissions.
97 'checkPermissions' => FALSE,
98 ];
99 if (in_array($entityName, \CRM_Contact_BAO_ContactType::basicTypes(TRUE))) {
100 $params['values'] = ['contact_type' => $entityName];
101 $entityName = 'Contact';
102 }
103 $fieldInfo = civicrm_api4($entityName, 'getFields', $params)->first();
104 // Merge field definition data with whatever's already in the markup.
105 $deep = ['input_attrs'];
106 if ($fieldInfo) {
107 $existingFieldDefn = trim(pq($afField)->attr('defn') ?: '');
108 if ($existingFieldDefn && $existingFieldDefn[0] != '{') {
109 // If it's not an object, don't mess with it.
110 return;
111 }
112 // Default placeholder for select inputs
113 if ($fieldInfo['input_type'] === 'Select') {
114 $fieldInfo['input_attrs'] = ($fieldInfo['input_attrs'] ?? []) + ['placeholder' => E::ts('Select')];
115 }
116
117 $fieldDefn = $existingFieldDefn ? \CRM_Utils_JS::getRawProps($existingFieldDefn) : [];
118
119 if ('Date' === $fieldInfo['input_type'] && !empty($fieldDefn['input_type']) && \CRM_Utils_JS::decode($fieldDefn['input_type']) === 'Select') {
120 $fieldInfo['input_attrs']['placeholder'] = E::ts('Select');
121 $fieldInfo['options'] = \CRM_Utils_Array::makeNonAssociative(\CRM_Core_OptionGroup::values('relative_date_filters'), 'id', 'label');
122 }
123
124 foreach ($fieldInfo as $name => $prop) {
125 // Merge array props 1 level deep
126 if (in_array($name, $deep) && !empty($fieldDefn[$name])) {
127 $fieldDefn[$name] = \CRM_Utils_JS::writeObject(\CRM_Utils_JS::getRawProps($fieldDefn[$name]) + array_map(['\CRM_Utils_JS', 'encode'], $prop));
128 }
129 elseif (!isset($fieldDefn[$name])) {
130 $fieldDefn[$name] = \CRM_Utils_JS::encode($prop);
131 }
132 }
133 pq($afField)->attr('defn', htmlspecialchars(\CRM_Utils_JS::writeObject($fieldDefn)));
134 }
135 }
136
137 /**
138 * Determines name of the api entity based on the field name prefix
139 *
140 * @param string $fieldName
141 * @param string[] $entityList
142 * @return string
143 */
144 private static function getFieldEntityType($fieldName, $entityList) {
145 $prefix = strpos($fieldName, '.') ? explode('.', $fieldName)[0] : NULL;
146 $baseEntity = array_shift($entityList);
147 if ($prefix) {
148 foreach ($entityList as $entityAndAlias) {
149 [$entity, $alias] = explode(' AS ', $entityAndAlias);
150 if ($alias === $prefix) {
151 return $entityAndAlias;
152 }
153 }
154 }
155 return $baseEntity;
156 }
157
158 private static function getFormEntities(\phpQueryObject $doc) {
159 $entities = [];
160 foreach ($doc->find('af-entity') as $afmModelProp) {
161 $entities[$afmModelProp->getAttribute('name')] = [
162 'type' => $afmModelProp->getAttribute('type'),
163 ];
164 }
165 return $entities;
166 }
167
168 }