3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
12 namespace Civi\Afform
;
14 use CRM_Afform_ExtensionUtil
as E
;
17 * Class AfformMetadataInjector
18 * @package Civi\Afform
20 class AfformMetadataInjector
{
23 * @param \Civi\Core\Event\GenericHookEvent $e
24 * @see CRM_Utils_Hook::alterAngular()
26 public static function preprocess($e) {
27 $changeSet = \Civi\Angular\ChangeSet
::create('fieldMetadata')
28 ->alterHtml(';\\.aff\\.html$;', function($doc, $path) {
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();
33 catch (\Exception
$e) {
36 $blockEntity = $meta['join'] ??
$meta['block'] ??
NULL;
38 $entities = self
::getFormEntities($doc);
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 */
45 $joinName = pq($afField)->parents('[af-join]')->attr('af-join');
47 self
::fillFieldMetadata($joinName, $action, $afField);
51 self
::fillFieldMetadata($blockEntity, $action, $afField);
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
60 $entityType = self
::getFieldEntityType($afField->getAttribute('name'), \CRM_Utils_JS
::decode($apiEntities));
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)");
68 $entityType = $entities[$entityName]['type'];
70 self
::fillFieldMetadata($entityType, $action, $afField);
73 $e->angular
->add($changeSet);
77 * Merge field definition metadata into an afform field's definition
79 * @param string $entityName
80 * @param string $action
81 * @param \DOMElement $afField
82 * @throws \API_Exception
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);
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,
99 if (in_array($entityName, \CRM_Contact_BAO_ContactType
::basicTypes(TRUE))) {
100 $params['values'] = ['contact_type' => $entityName];
101 $entityName = 'Contact';
103 $fieldInfo = civicrm_api4($entityName, 'getFields', $params)->first();
104 // Merge field definition data with whatever's already in the markup.
105 $deep = ['input_attrs'];
107 $existingFieldDefn = trim(pq($afField)->attr('defn') ?
: '');
108 if ($existingFieldDefn && $existingFieldDefn[0] != '{') {
109 // If it's not an object, don't mess with it.
112 // Default placeholder for select inputs
113 if ($fieldInfo['input_type'] === 'Select') {
114 $fieldInfo['input_attrs'] = ($fieldInfo['input_attrs'] ??
[]) +
['placeholder' => E
::ts('Select')];
117 $fieldDefn = $existingFieldDefn ? \CRM_Utils_JS
::getRawProps($existingFieldDefn) : [];
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');
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));
129 elseif (!isset($fieldDefn[$name])) {
130 $fieldDefn[$name] = \CRM_Utils_JS
::encode($prop);
133 pq($afField)->attr('defn', htmlspecialchars(\CRM_Utils_JS
::writeObject($fieldDefn)));
138 * Determines name of the api entity based on the field name prefix
140 * @param string $fieldName
141 * @param string[] $entityList
144 private static function getFieldEntityType($fieldName, $entityList) {
145 $prefix = strpos($fieldName, '.') ?
explode('.', $fieldName)[0] : NULL;
146 $baseEntity = array_shift($entityList);
148 foreach ($entityList as $entityAndAlias) {
149 [$entity, $alias] = explode(' AS ', $entityAndAlias);
150 if ($alias === $prefix) {
151 return $entityAndAlias;
158 private static function getFormEntities(\phpQueryObject
$doc) {
160 foreach ($doc->find('af-entity') as $afmModelProp) {
161 $entities[$afmModelProp->getAttribute('name')] = [
162 'type' => $afmModelProp->getAttribute('type'),