Use dynamic snippet for afform module layout.
[civicrm-core.git] / ext / afform / core / afform.php
CommitLineData
66aa0f5e
TO
1<?php
2
3require_once 'afform.civix.php';
4use CRM_Afform_ExtensionUtil as E;
fb388832 5use Civi\Api4\Action\Afform\Submit;
66aa0f5e 6
bc3b7c5b
TO
7/**
8 * Filter the content of $params to only have supported afform fields.
9 *
10 * @param array $params
11 * @return array
12 */
13function _afform_fields_filter($params) {
5591cfbf 14 $result = [];
50868e8d
CW
15 $fields = \Civi\Api4\Afform::getfields()->setCheckPermissions(FALSE)->execute()->indexBy('name');
16 foreach ($fields as $fieldName => $field) {
17 if (isset($params[$fieldName])) {
18 $result[$fieldName] = $params[$fieldName];
bc3b7c5b 19 }
d1ec770c 20
50868e8d
CW
21 if (isset($result[$fieldName])) {
22 switch ($fieldName) {
d1ec770c 23 case 'is_public':
50868e8d 24 $result[$fieldName] = CRM_Utils_String::strtobool($result[$fieldName]);
d1ec770c
TO
25 break;
26
27 }
28 }
bc3b7c5b
TO
29 }
30 return $result;
31}
32
8f4a0ee9 33/**
5591cfbf 34 * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
8f4a0ee9
TO
35 */
36function afform_civicrm_container($container) {
77dccccb 37 $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
8f4a0ee9
TO
38 $container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\Definition(
39 'CRM_Afform_AfformScanner',
5591cfbf 40 []
8f4a0ee9
TO
41 ));
42}
43
66aa0f5e
TO
44/**
45 * Implements hook_civicrm_config().
46 *
47 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config
48 */
49function afform_civicrm_config(&$config) {
50 _afform_civix_civicrm_config($config);
77dccccb
TO
51
52 if (isset(Civi::$statics[__FUNCTION__])) {
53 return;
54 }
55 Civi::$statics[__FUNCTION__] = 1;
56
fb388832
TO
57 // Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processContacts'], -500);
58 Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processGenericEntity'], -1000);
77dccccb 59 Civi::dispatcher()->addListener('hook_civicrm_angularModules', '_afform_civicrm_angularModules_autoReq', -1000);
66aa0f5e
TO
60}
61
62/**
63 * Implements hook_civicrm_xmlMenu().
64 *
65 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu
66 */
67function afform_civicrm_xmlMenu(&$files) {
68 _afform_civix_civicrm_xmlMenu($files);
69}
70
71/**
72 * Implements hook_civicrm_install().
73 *
74 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install
75 */
76function afform_civicrm_install() {
77 _afform_civix_civicrm_install();
78}
79
80/**
81 * Implements hook_civicrm_postInstall().
82 *
83 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
84 */
85function afform_civicrm_postInstall() {
86 _afform_civix_civicrm_postInstall();
87}
88
89/**
90 * Implements hook_civicrm_uninstall().
91 *
92 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
93 */
94function afform_civicrm_uninstall() {
95 _afform_civix_civicrm_uninstall();
96}
97
98/**
99 * Implements hook_civicrm_enable().
100 *
101 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
102 */
103function afform_civicrm_enable() {
104 _afform_civix_civicrm_enable();
105}
106
107/**
108 * Implements hook_civicrm_disable().
109 *
110 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
111 */
112function afform_civicrm_disable() {
113 _afform_civix_civicrm_disable();
114}
115
116/**
117 * Implements hook_civicrm_upgrade().
118 *
119 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade
120 */
121function afform_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
122 return _afform_civix_civicrm_upgrade($op, $queue);
123}
124
125/**
126 * Implements hook_civicrm_managed().
127 *
128 * Generate a list of entities to create/deactivate/delete when this module
129 * is installed, disabled, uninstalled.
130 *
131 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed
132 */
133function afform_civicrm_managed(&$entities) {
134 _afform_civix_civicrm_managed($entities);
135}
136
137/**
138 * Implements hook_civicrm_caseTypes().
139 *
140 * Generate a list of case-types.
141 *
142 * Note: This hook only runs in CiviCRM 4.4+.
143 *
144 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes
145 */
146function afform_civicrm_caseTypes(&$caseTypes) {
147 _afform_civix_civicrm_caseTypes($caseTypes);
148}
149
150/**
151 * Implements hook_civicrm_angularModules().
152 *
153 * Generate a list of Angular modules.
154 *
155 * Note: This hook only runs in CiviCRM 4.5+. It may
156 * use features only available in v4.6+.
157 *
158 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
159 */
160function afform_civicrm_angularModules(&$angularModules) {
161 _afform_civix_civicrm_angularModules($angularModules);
bb56ac78 162
77dccccb 163 /** @var CRM_Afform_AfformScanner $scanner */
8f4a0ee9 164 $scanner = Civi::service('afform_scanner');
bb56ac78
TO
165 $names = array_keys($scanner->findFilePaths());
166 foreach ($names as $name) {
167 $meta = $scanner->getMeta($name);
27a7e641 168 $layout = $scanner->getLayout($name);
87dde5eb 169 $angularModules[_afform_angular_module_name($name, 'camel')] = [
bb56ac78
TO
170 'ext' => E::LONG_NAME,
171 'js' => ['assetBuilder://afform.js?name=' . urlencode($name)],
172 'requires' => $meta['requires'],
173 'basePages' => [],
27a7e641
CW
174 'snippets' => [
175 "~afform/$name.aff.html" => $layout,
176 ],
77dccccb 177 'exports' => [
23315411 178 _afform_angular_module_name($name, 'dash') => 'AE',
77dccccb 179 ],
bb56ac78 180 ];
bb56ac78 181 }
66aa0f5e
TO
182}
183
77dccccb
TO
184/**
185 * Scan the list of Angular modules and inject automatic-requirements.
186 *
187 * TLDR: if an afform uses element "<other-el/>", and if another module defines
188 * `$angularModules['otherMod']['exports']['el'][0] === 'other-el'`, then
189 * the 'otherMod' is automatically required.
190 *
191 * @param \Civi\Core\Event\GenericHookEvent $e
192 * @see CRM_Utils_Hook::angularModules()
193 */
194function _afform_civicrm_angularModules_autoReq($e) {
195 /** @var CRM_Afform_AfformScanner $scanner */
196 $scanner = Civi::service('afform_scanner');
197 $moduleEnvId = md5(\CRM_Core_Config_Runtime::getId() . implode(',', array_keys($e->angularModules)));
198 $depCache = CRM_Utils_Cache::create([
199 'name' => 'afdep_' . substr($moduleEnvId, 0, 32 - 6),
200 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'],
201 'withArray' => 'fast',
202 'prefetch' => TRUE,
203 ]);
204 $depCacheTtl = 2 * 60 * 60;
205
206 $revMap = _afform_reverse_deps($e->angularModules);
207
208 $formNames = array_keys($scanner->findFilePaths());
209 foreach ($formNames as $formName) {
210 $angModule = _afform_angular_module_name($formName, 'camel');
211 $cacheLine = $depCache->get($formName, NULL);
212
213 $jFile = $scanner->findFilePath($formName, 'aff.json');
214 $hFile = $scanner->findFilePath($formName, 'aff.html');
215
216 $jStat = stat($jFile);
217 $hStat = stat($hFile);
218
219 if ($cacheLine === NULL) {
220 $needsUpdate = TRUE;
221 }
222 elseif ($jStat !== FALSE && $jStat['size'] !== $cacheLine['js']) {
223 $needsUpdate = TRUE;
224 }
225 elseif ($jStat !== FALSE && $jStat['mtime'] > $cacheLine['jm']) {
226 $needsUpdate = TRUE;
227 }
228 elseif ($hStat !== FALSE && $hStat['size'] !== $cacheLine['hs']) {
229 $needsUpdate = TRUE;
230 }
231 elseif ($hStat !== FALSE && $hStat['mtime'] > $cacheLine['hm']) {
232 $needsUpdate = TRUE;
233 }
234 else {
235 $needsUpdate = FALSE;
236 }
237
238 if ($needsUpdate) {
239 $cacheLine = [
240 'js' => $jStat['size'] ?? NULL,
241 'jm' => $jStat['mtime'] ?? NULL,
242 'hs' => $hStat['size'] ?? NULL,
243 'hm' => $hStat['mtime'] ?? NULL,
244 'r' => array_values(array_unique(array_merge(
245 [CRM_Afform_AfformScanner::DEFAULT_REQUIRES],
246 $e->angularModules[$angModule]['requires'] ?? [],
247 _afform_reverse_deps_find($formName, file_get_contents($hFile), $revMap)
248 ))),
249 ];
250 // print_r(['cache update:' . $formName => $cacheLine]);
251 $depCache->set($formName, $cacheLine, $depCacheTtl);
252 }
253
254 $e->angularModules[$angModule]['requires'] = $cacheLine['r'];
255 }
256}
257
258/**
259 * @param $angularModules
260 * @return array
261 * 'attr': array(string $attrName => string $angModuleName)
262 * 'el': array(string $elementName => string $angModuleName)
263 */
264function _afform_reverse_deps($angularModules) {
23315411
TO
265 $revMap = ['attr' => [], 'el' => []];
266 foreach (array_keys($angularModules) as $module) {
267 if (!isset($angularModules[$module]['exports'])) {
268 continue;
269 }
270 foreach ($angularModules[$module]['exports'] as $symbolName => $symbolTypes) {
271 if (strpos($symbolTypes, 'A') !== FALSE) {
272 $revMap['attr'][$symbolName] = $module;
273 }
274 if (strpos($symbolTypes, 'E') !== FALSE) {
275 $revMap['el'][$symbolName] = $module;
77dccccb
TO
276 }
277 }
278 }
279 return $revMap;
280}
281
23315411
TO
282/**
283 * @param string $formName
284 * @param string $html
285 * @param array $revMap
286 * The reverse-dependencies map from _afform_reverse_deps().
287 * @return array
288 * @see _afform_reverse_deps()
289 */
77dccccb
TO
290function _afform_reverse_deps_find($formName, $html, $revMap) {
291 $symbols = \Civi\Afform\Symbols::scan($html);
292 $elems = array_intersect_key($revMap['el'], $symbols->elements);
293 $attrs = array_intersect_key($revMap['attr'], $symbols->attributes);
294 return array_values(array_unique(array_merge($elems, $attrs)));
295}
296
9384980c
TO
297/**
298 * @param \Civi\Angular\Manager $angular
299 * @see CRM_Utils_Hook::alterAngular()
300 */
301function afform_civicrm_alterAngular($angular) {
302 $fieldMetadata = \Civi\Angular\ChangeSet::create('fieldMetadata')
3cd5c38b
CW
303 ->alterHtml(';^~afform/;', function($doc, $path) {
304 $entities = _afform_getMetadata($doc);
9384980c
TO
305
306 foreach (pq('af-field', $doc) as $afField) {
307 /** @var DOMElement $afField */
0eaa6e27 308 $fieldName = $afField->getAttribute('name');
56b9319d 309 $entityName = pq($afField)->parents('[af-fieldset]')->attr('af-fieldset');
9384980c
TO
310 if (!preg_match(';^[a-zA-Z0-9\_\-\. ]+$;', $entityName)) {
311 throw new \CRM_Core_Exception("Cannot process $path: malformed entity name ($entityName)");
312 }
313 $entityType = $entities[$entityName]['type'];
314 $getFields = civicrm_api4($entityType, 'getFields', [
65c9e7ae 315 'action' => 'create',
9384980c 316 'where' => [['name', '=', $fieldName]],
cef2e16a 317 'select' => ['title', 'input_type', 'input_attrs', 'options'],
c9920ea7 318 'loadOptions' => TRUE,
9384980c 319 ]);
cef2e16a 320 // Merge field definition data with whatever's already in the markup
2c33aefd
CW
321 $deep = ['input_attrs'];
322 foreach ($getFields as $fieldInfo) {
a715f6d4 323 $existingFieldDefn = trim(pq($afField)->attr('defn') ?: '');
cef2e16a
CW
324 if ($existingFieldDefn && $existingFieldDefn[0] != '{') {
325 // If it's not an object, don't mess with it.
326 continue;
327 }
b4def6e9
CW
328 // TODO: Teach the api to return options in this format
329 if (!empty($fieldInfo['options'])) {
330 $fieldInfo['options'] = CRM_Utils_Array::makeNonAssociative($fieldInfo['options'], 'key', 'label');
331 }
22946dcf
CW
332 // Default placeholder for select inputs
333 if ($fieldInfo['input_type'] === 'Select') {
334 $fieldInfo['input_attrs'] = ($fieldInfo['input_attrs'] ?? []) + ['placeholder' => ts('Select')];
335 }
b4def6e9 336
2c33aefd
CW
337 $fieldDefn = $existingFieldDefn ? CRM_Utils_JS::getRawProps($existingFieldDefn) : [];
338 foreach ($fieldInfo as $name => $prop) {
339 // Merge array props 1 level deep
340 if (in_array($name, $deep) && !empty($fieldDefn[$name])) {
341 $fieldDefn[$name] = CRM_Utils_JS::writeObject(CRM_Utils_JS::getRawProps($fieldDefn[$name]) + array_map(['CRM_Utils_JS', 'encode'], $prop));
342 }
343 elseif (!isset($fieldDefn[$name])) {
344 $fieldDefn[$name] = CRM_Utils_JS::encode($prop);
345 }
cef2e16a 346 }
2c33aefd 347 pq($afField)->attr('defn', htmlspecialchars(CRM_Utils_JS::writeObject($fieldDefn)));
9384980c
TO
348 }
349 }
350 });
351 $angular->add($fieldMetadata);
352}
353
354function _afform_getMetadata(phpQueryObject $doc) {
355 $entities = [];
c37151d2 356 foreach ($doc->find('af-entity') as $afmModelProp) {
8410442c 357 $entities[$afmModelProp->getAttribute('name')] = [
bbd6df21 358 'type' => $afmModelProp->getAttribute('type'),
9384980c
TO
359 ];
360 }
361 return $entities;
362}
363
66aa0f5e
TO
364/**
365 * Implements hook_civicrm_alterSettingsFolders().
366 *
367 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders
368 */
369function afform_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
370 _afform_civix_civicrm_alterSettingsFolders($metaDataFolders);
371}
372
373/**
374 * Implements hook_civicrm_entityTypes().
375 *
376 * Declare entity types provided by this module.
377 *
378 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes
379 */
380function afform_civicrm_entityTypes(&$entityTypes) {
381 _afform_civix_civicrm_entityTypes($entityTypes);
382}
383
98f4a7cb
TO
384/**
385 * Implements hook_civicrm_themes().
386 */
387function afform_civicrm_themes(&$themes) {
388 _afform_civix_civicrm_themes($themes);
389}
390
66aa0f5e
TO
391// --- Functions below this ship commented out. Uncomment as required. ---
392
bb56ac78
TO
393/**
394 * Implements hook_civicrm_buildAsset().
395 */
396function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
397 if ($asset !== 'afform.js') {
398 return;
399 }
400
401 if (empty($params['name'])) {
402 throw new RuntimeException("Missing required parameter: afform.js?name=NAME");
403 }
404
405 $name = $params['name'];
854558d3 406 /** @var \CRM_Afform_AfformScanner $scanner */
8f4a0ee9 407 $scanner = Civi::service('afform_scanner');
f4017340 408 $meta = $scanner->getMeta($name);
7d2f28fe 409
bb56ac78
TO
410 $smarty = CRM_Core_Smarty::singleton();
411 $smarty->assign('afform', [
87dde5eb 412 'camel' => _afform_angular_module_name($name, 'camel'),
bb56ac78 413 'meta' => $meta,
f4017340 414 'metaJson' => json_encode($meta),
27a7e641 415 'templateUrl' => "~afform/$name.aff.html",
bb56ac78
TO
416 ]);
417 $mimeType = 'text/javascript';
9ec944f2 418 $content = $smarty->fetch('afform/AfformAngularModule.tpl');
bb56ac78
TO
419}
420
8bed1c84
TO
421/**
422 * Apply any filters to an HTML partial.
423 *
424 * @param string $formName
425 * @param string $html
426 * Original HTML.
427 * @return string
428 * Modified HTML.
429 */
430function _afform_html_filter($formName, $html) {
431 $fileName = '~afform/' . _afform_angular_module_name($formName, 'camel');
432 $htmls = [$fileName => $html];
433 $htmls = \Civi\Angular\ChangeSet::applyResourceFilters(Civi::service('angular')->getChangeSets(), 'partials', $htmls);
434 return $htmls[$fileName];
435}
436
8775c48a
TO
437/**
438 * Implements hook_civicrm_alterMenu().
439 */
440function afform_civicrm_alterMenu(&$items) {
8f4a0ee9
TO
441 if (Civi::container()->has('afform_scanner')) {
442 $scanner = Civi::service('afform_scanner');
443 }
444 else {
445 // During installation...
446 $scanner = new CRM_Afform_AfformScanner();
447 }
8775c48a
TO
448 foreach ($scanner->getMetas() as $name => $meta) {
449 if (!empty($meta['server_route'])) {
450 $items[$meta['server_route']] = [
451 'page_callback' => 'CRM_Afform_Page_AfformBase',
452 'page_arguments' => 'afform=' . urlencode($name),
13bdd6d2 453 'title' => $meta['title'] ?? '',
254f01f0
ML
454 'access_arguments' => [['access CiviCRM'], 'and'], // FIXME
455 'is_public' => $meta['is_public'],
8775c48a
TO
456 ];
457 }
458 }
8775c48a
TO
459}
460
74f862e4
TO
461/**
462 * Clear any local/in-memory caches based on afform data.
463 */
464function _afform_clear() {
465 $container = \Civi::container();
466 $container->get('afform_scanner')->clear();
467
468 // Civi\Angular\Manager doesn't currently have a way to clear its in-memory
469 // data, so we just reset the whole object.
470 $container->set('angular', NULL);
471}
472
bb56ac78 473/**
87dde5eb
TO
474 * @param string $fileBaseName
475 * Ex: foo-bar
841850b1
TO
476 * @param string $format
477 * 'camel' or 'dash'.
bb56ac78 478 * @return string
841850b1 479 * Ex: 'FooBar' or 'foo-bar'.
3cd5c38b 480 * @throws \Exception
bb56ac78 481 */
87dde5eb 482function _afform_angular_module_name($fileBaseName, $format = 'camel') {
841850b1
TO
483 switch ($format) {
484 case 'camel':
87dde5eb
TO
485 $camelCase = '';
486 foreach (explode('-', $fileBaseName) as $shortNamePart) {
487 $camelCase .= ucfirst($shortNamePart);
488 }
489 return strtolower($camelCase{0}) . substr($camelCase, 1);
841850b1
TO
490
491 case 'dash':
87dde5eb 492 return strtolower(implode('-', array_filter(preg_split('/(?=[A-Z])/', $fileBaseName))));
841850b1
TO
493
494 default:
495 throw new \Exception("Unrecognized format");
496 }
bb56ac78 497}