3 require_once 'afform.civix.php';
4 use CRM_Afform_ExtensionUtil
as E
;
5 use Civi\Api4\Action\Afform\Submit
;
8 * Filter the content of $params to only have supported afform fields.
10 * @param array $params
13 function _afform_fields_filter($params) {
15 $fields = \Civi\Api4\Afform
::getfields()->setCheckPermissions(FALSE)->setAction('create')->execute()->indexBy('name');
16 foreach ($fields as $fieldName => $field) {
17 if (isset($params[$fieldName])) {
18 $result[$fieldName] = $params[$fieldName];
20 if ($field['data_type'] === 'Boolean' && !is_bool($params[$fieldName])) {
21 $result[$fieldName] = CRM_Utils_String
::strtobool($params[$fieldName]);
29 * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
31 function afform_civicrm_container($container) {
32 $container->addResource(new \Symfony\Component\Config\
Resource\
FileResource(__FILE__
));
33 $container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\
Definition(
34 'CRM_Afform_AfformScanner',
40 * Implements hook_civicrm_config().
42 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config
44 function afform_civicrm_config(&$config) {
45 _afform_civix_civicrm_config($config);
47 if (isset(Civi
::$statics[__FUNCTION__
])) {
50 Civi
::$statics[__FUNCTION__
] = 1;
52 Civi
::dispatcher()->addListener(Submit
::EVENT_NAME
, [Submit
::class, 'processContacts'], 500);
53 Civi
::dispatcher()->addListener(Submit
::EVENT_NAME
, [Submit
::class, 'processGenericEntity'], -1000);
54 Civi
::dispatcher()->addListener('hook_civicrm_angularModules', '_afform_civicrm_angularModules_autoReq', -1000);
58 * Implements hook_civicrm_xmlMenu().
60 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu
62 function afform_civicrm_xmlMenu(&$files) {
63 _afform_civix_civicrm_xmlMenu($files);
67 * Implements hook_civicrm_install().
69 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install
71 function afform_civicrm_install() {
72 _afform_civix_civicrm_install();
76 * Implements hook_civicrm_postInstall().
78 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
80 function afform_civicrm_postInstall() {
81 _afform_civix_civicrm_postInstall();
85 * Implements hook_civicrm_uninstall().
87 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
89 function afform_civicrm_uninstall() {
90 _afform_civix_civicrm_uninstall();
94 * Implements hook_civicrm_enable().
96 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
98 function afform_civicrm_enable() {
99 _afform_civix_civicrm_enable();
103 * Implements hook_civicrm_disable().
105 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
107 function afform_civicrm_disable() {
108 _afform_civix_civicrm_disable();
112 * Implements hook_civicrm_upgrade().
114 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade
116 function afform_civicrm_upgrade($op, CRM_Queue_Queue
$queue = NULL) {
117 return _afform_civix_civicrm_upgrade($op, $queue);
121 * Implements hook_civicrm_managed().
123 * Generate a list of entities to create/deactivate/delete when this module
124 * is installed, disabled, uninstalled.
126 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed
128 function afform_civicrm_managed(&$entities) {
129 _afform_civix_civicrm_managed($entities);
133 * Implements hook_civicrm_caseTypes().
135 * Generate a list of case-types.
137 * Note: This hook only runs in CiviCRM 4.4+.
139 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes
141 function afform_civicrm_caseTypes(&$caseTypes) {
142 _afform_civix_civicrm_caseTypes($caseTypes);
146 * Implements hook_civicrm_angularModules().
148 * Generate a list of Afform Angular modules.
150 function afform_civicrm_angularModules(&$angularModules) {
151 _afform_civix_civicrm_angularModules($angularModules);
153 $afforms = \Civi\Api4\Afform
::get()
154 ->setCheckPermissions(FALSE)
155 ->setSelect(['name', 'requires', 'module_name', 'directive_name'])
158 foreach ($afforms as $afform) {
159 $angularModules[$afform['module_name']] = [
160 'ext' => E
::LONG_NAME
,
161 'js' => ['assetBuilder://afform.js?name=' . urlencode($afform['name'])],
162 'requires' => $afform['requires'],
164 'partialsCallback' => '_afform_get_partials',
165 '_afform' => $afform['name'],
167 $afform['directive_name'] => 'AE',
174 * Callback to retrieve partials for a given afform/angular module.
176 * @see afform_civicrm_angularModules
178 * @param string $moduleName
180 * @param array $module
181 * The module definition.
183 * Array(string $filename => string $html).
184 * @throws API_Exception
186 function _afform_get_partials($moduleName, $module) {
187 $afform = civicrm_api4('Afform', 'get', [
188 'where' => [['name', '=', $module['_afform']]],
189 'select' => ['layout'],
190 'layoutFormat' => 'html',
191 'checkPermissions' => FALSE,
194 "~/$moduleName/$moduleName.aff.html" => $afform['layout'],
199 * Scan the list of Angular modules and inject automatic-requirements.
201 * TLDR: if an afform uses element "<other-el/>", and if another module defines
202 * `$angularModules['otherMod']['exports']['el'][0] === 'other-el'`, then
203 * the 'otherMod' is automatically required.
205 * @param \Civi\Core\Event\GenericHookEvent $e
206 * @see CRM_Utils_Hook::angularModules()
208 function _afform_civicrm_angularModules_autoReq($e) {
209 /** @var CRM_Afform_AfformScanner $scanner */
210 $scanner = Civi
::service('afform_scanner');
211 $moduleEnvId = md5(\CRM_Core_Config_Runtime
::getId() . implode(',', array_keys($e->angularModules
)));
212 $depCache = CRM_Utils_Cache
::create([
213 'name' => 'afdep_' . substr($moduleEnvId, 0, 32 - 6),
214 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'],
215 'withArray' => 'fast',
218 $depCacheTtl = 2 * 60 * 60;
220 $revMap = _afform_reverse_deps($e->angularModules
);
222 $formNames = array_keys($scanner->findFilePaths());
223 foreach ($formNames as $formName) {
224 $angModule = _afform_angular_module_name($formName, 'camel');
225 $cacheLine = $depCache->get($formName, NULL);
227 $jFile = $scanner->findFilePath($formName, 'aff.json');
228 $hFile = $scanner->findFilePath($formName, 'aff.html');
230 $jStat = stat($jFile);
231 $hStat = stat($hFile);
233 if ($cacheLine === NULL) {
236 elseif ($jStat !== FALSE && $jStat['size'] !== $cacheLine['js']) {
239 elseif ($jStat !== FALSE && $jStat['mtime'] > $cacheLine['jm']) {
242 elseif ($hStat !== FALSE && $hStat['size'] !== $cacheLine['hs']) {
245 elseif ($hStat !== FALSE && $hStat['mtime'] > $cacheLine['hm']) {
249 $needsUpdate = FALSE;
254 'js' => $jStat['size'] ??
NULL,
255 'jm' => $jStat['mtime'] ??
NULL,
256 'hs' => $hStat['size'] ??
NULL,
257 'hm' => $hStat['mtime'] ??
NULL,
258 'r' => array_values(array_unique(array_merge(
259 [CRM_Afform_AfformScanner
::DEFAULT_REQUIRES
],
260 $e->angularModules
[$angModule]['requires'] ??
[],
261 _afform_reverse_deps_find($formName, file_get_contents($hFile), $revMap)
264 // print_r(['cache update:' . $formName => $cacheLine]);
265 $depCache->set($formName, $cacheLine, $depCacheTtl);
268 $e->angularModules
[$angModule]['requires'] = $cacheLine['r'];
273 * @param $angularModules
275 * 'attr': array(string $attrName => string $angModuleName)
276 * 'el': array(string $elementName => string $angModuleName)
278 function _afform_reverse_deps($angularModules) {
279 $revMap = ['attr' => [], 'el' => []];
280 foreach (array_keys($angularModules) as $module) {
281 if (!isset($angularModules[$module]['exports'])) {
284 foreach ($angularModules[$module]['exports'] as $symbolName => $symbolTypes) {
285 if (strpos($symbolTypes, 'A') !== FALSE) {
286 $revMap['attr'][$symbolName] = $module;
288 if (strpos($symbolTypes, 'E') !== FALSE) {
289 $revMap['el'][$symbolName] = $module;
297 * @param string $formName
298 * @param string $html
299 * @param array $revMap
300 * The reverse-dependencies map from _afform_reverse_deps().
302 * @see _afform_reverse_deps()
304 function _afform_reverse_deps_find($formName, $html, $revMap) {
305 $symbols = \Civi\Afform\Symbols
::scan($html);
306 $elems = array_intersect_key($revMap['el'], $symbols->elements
);
307 $attrs = array_intersect_key($revMap['attr'], $symbols->attributes
);
308 return array_values(array_unique(array_merge($elems, $attrs)));
312 * @param \Civi\Angular\Manager $angular
313 * @see CRM_Utils_Hook::alterAngular()
315 function afform_civicrm_alterAngular($angular) {
316 $fieldMetadata = \Civi\Angular\ChangeSet
::create('fieldMetadata')
317 ->alterHtml(';\\.aff\\.html$;', function($doc, $path) {
319 $module = \Civi
::service('angular')->getModule(basename($path, '.aff.html'));
320 $meta = \Civi\Api4\Afform
::get()->addWhere('name', '=', $module['_afform'])->setSelect(['join', 'block'])->setCheckPermissions(FALSE)->execute()->first();
322 catch (Exception
$e) {
325 $blockEntity = $meta['join'] ??
$meta['block'] ??
NULL;
327 $entities = _afform_getMetadata($doc);
330 foreach (pq('af-field', $doc) as $afField) {
331 /** @var DOMElement $afField */
332 $entityName = pq($afField)->parents('[af-fieldset]')->attr('af-fieldset');
333 $joinName = pq($afField)->parents('[af-join]')->attr('af-join');
334 if (!$blockEntity && !preg_match(';^[a-zA-Z0-9\_\-\. ]+$;', $entityName)) {
335 throw new \
CRM_Core_Exception("Cannot process $path: malformed entity name ($entityName)");
337 $entityType = $blockEntity ??
$entities[$entityName]['type'];
338 _af_fill_field_metadata($joinName ?
$joinName : $entityType, $afField);
341 $angular->add($fieldMetadata);
345 * Merge field definition metadata into an afform field's definition
348 * @param DOMElement $afField
349 * @throws API_Exception
351 function _af_fill_field_metadata($entityType, DOMElement
$afField) {
353 'action' => 'create',
354 'where' => [['name', '=', $afField->getAttribute('name')]],
355 'select' => ['title', 'input_type', 'input_attrs', 'options'],
356 'loadOptions' => TRUE,
358 if (in_array($entityType, CRM_Contact_BAO_ContactType
::basicTypes(TRUE))) {
359 $params['values'] = ['contact_type' => $entityType];
360 $entityType = 'Contact';
362 $getFields = civicrm_api4($entityType, 'getFields', $params);
363 // Merge field definition data with whatever's already in the markup
364 $deep = ['input_attrs'];
365 foreach ($getFields as $fieldInfo) {
366 $existingFieldDefn = trim(pq($afField)->attr('defn') ?
: '');
367 if ($existingFieldDefn && $existingFieldDefn[0] != '{') {
368 // If it's not an object, don't mess with it.
371 // TODO: Teach the api to return options in this format
372 if (!empty($fieldInfo['options'])) {
373 $fieldInfo['options'] = CRM_Utils_Array
::makeNonAssociative($fieldInfo['options'], 'key', 'label');
375 // Default placeholder for select inputs
376 if ($fieldInfo['input_type'] === 'Select') {
377 $fieldInfo['input_attrs'] = ($fieldInfo['input_attrs'] ??
[]) +
['placeholder' => ts('Select')];
380 $fieldDefn = $existingFieldDefn ? CRM_Utils_JS
::getRawProps($existingFieldDefn) : [];
381 foreach ($fieldInfo as $name => $prop) {
382 // Merge array props 1 level deep
383 if (in_array($name, $deep) && !empty($fieldDefn[$name])) {
384 $fieldDefn[$name] = CRM_Utils_JS
::writeObject(CRM_Utils_JS
::getRawProps($fieldDefn[$name]) +
array_map(['CRM_Utils_JS', 'encode'], $prop));
386 elseif (!isset($fieldDefn[$name])) {
387 $fieldDefn[$name] = CRM_Utils_JS
::encode($prop);
390 pq($afField)->attr('defn', htmlspecialchars(CRM_Utils_JS
::writeObject($fieldDefn)));
394 function _afform_getMetadata(phpQueryObject
$doc) {
396 foreach ($doc->find('af-entity') as $afmModelProp) {
397 $entities[$afmModelProp->getAttribute('name')] = [
398 'type' => $afmModelProp->getAttribute('type'),
405 * Implements hook_civicrm_alterSettingsFolders().
407 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders
409 function afform_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
410 _afform_civix_civicrm_alterSettingsFolders($metaDataFolders);
414 * Implements hook_civicrm_entityTypes().
416 * Declare entity types provided by this module.
418 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes
420 function afform_civicrm_entityTypes(&$entityTypes) {
421 _afform_civix_civicrm_entityTypes($entityTypes);
425 * Implements hook_civicrm_themes().
427 function afform_civicrm_themes(&$themes) {
428 _afform_civix_civicrm_themes($themes);
432 * Implements hook_civicrm_buildAsset().
434 function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
435 if ($asset !== 'afform.js') {
439 if (empty($params['name'])) {
440 throw new RuntimeException("Missing required parameter: afform.js?name=NAME");
443 $moduleName = _afform_angular_module_name($params['name'], 'camel');
444 $smarty = CRM_Core_Smarty
::singleton();
445 $smarty->assign('afform', [
446 'camel' => $moduleName,
447 'meta' => ['name' => $params['name']],
448 'templateUrl' => "~/$moduleName/$moduleName.aff.html",
450 $mimeType = 'text/javascript';
451 $content = $smarty->fetch('afform/AfformAngularModule.tpl');
455 * Implements hook_civicrm_alterMenu().
457 function afform_civicrm_alterMenu(&$items) {
458 if (Civi
::container()->has('afform_scanner')) {
459 $scanner = Civi
::service('afform_scanner');
462 // During installation...
463 $scanner = new CRM_Afform_AfformScanner();
465 foreach ($scanner->getMetas() as $name => $meta) {
466 if (!empty($meta['server_route'])) {
467 $items[$meta['server_route']] = [
468 'page_callback' => 'CRM_Afform_Page_AfformBase',
469 'page_arguments' => 'afform=' . urlencode($name),
470 'title' => $meta['title'] ??
'',
471 'access_arguments' => [["@afform:$name"], 'and'],
472 'is_public' => $meta['is_public'],
479 * Implements hook_civicrm_permission_check().
481 * This extends the list of permissions available in `CRM_Core_Permission:check()`
482 * by introducing virtual-permissions named `@afform:myForm`. The evaluation
483 * of these virtual-permissions is dependent on the settings for `myForm`.
484 * `myForm` may be exposed/integrated through multiple subsystems (routing,
485 * nav-menu, API, etc), and the use of virtual-permissions makes easy to enforce
486 * consistent permissions across any relevant subsystems.
488 * @see CRM_Utils_Hook::permission_check()
490 function afform_civicrm_permission_check($permission, &$granted, $contactId) {
491 if ($permission{0} !== '@') {
492 // Micro-optimization - this function may get hit a lot.
496 if (preg_match('/^@afform:(.*)/', $permission, $m)) {
499 $afform = \Civi\Api4\Afform
::get()
500 ->setCheckPermissions(FALSE)
501 ->addWhere('name', '=', $name)
502 ->setSelect(['permission'])
506 $granted = CRM_Core_Permission
::check($afform['permission'], $contactId);
512 * Clear any local/in-memory caches based on afform data.
514 function _afform_clear() {
515 $container = \Civi
::container();
516 $container->get('afform_scanner')->clear();
517 $container->get('angular')->clear();
521 * @param string $fileBaseName
523 * @param string $format
526 * Ex: 'FooBar' or 'foo-bar'.
529 function _afform_angular_module_name($fileBaseName, $format = 'camel') {
533 foreach (preg_split('/[-_ ]/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY
) as $shortNamePart) {
534 $camelCase .= ucfirst($shortNamePart);
536 return strtolower($camelCase{0}) . substr($camelCase, 1);
539 return strtolower(implode('-', preg_split('/[-_ ]|(?=[A-Z])/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
)));
542 throw new \
Exception("Unrecognized format");