Afform - Fix form submit button
[civicrm-core.git] / ext / afform / core / afform.php
1 <?php
2
3 require_once 'afform.civix.php';
4 use CRM_Afform_ExtensionUtil as E;
5 use Civi\Api4\Action\Afform\Submit;
6
7 /**
8 * Filter the content of $params to only have supported afform fields.
9 *
10 * @param array $params
11 * @return array
12 */
13 function _afform_fields_filter($params) {
14 $result = [];
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];
19
20 if ($field['data_type'] === 'Boolean' && !is_bool($params[$fieldName])) {
21 $result[$fieldName] = CRM_Utils_String::strtobool($params[$fieldName]);
22 }
23 }
24 }
25 return $result;
26 }
27
28 /**
29 * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
30 */
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',
35 []
36 ))->setPublic(TRUE);
37 }
38
39 /**
40 * Implements hook_civicrm_config().
41 *
42 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config
43 */
44 function afform_civicrm_config(&$config) {
45 _afform_civix_civicrm_config($config);
46
47 if (isset(Civi::$statics[__FUNCTION__])) {
48 return;
49 }
50 Civi::$statics[__FUNCTION__] = 1;
51
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);
55 Civi::dispatcher()->addListener('hook_civicrm_alterAngular', ['\Civi\Afform\AfformMetadataInjector', 'preprocess']);
56 }
57
58 /**
59 * Implements hook_civicrm_xmlMenu().
60 *
61 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu
62 */
63 function afform_civicrm_xmlMenu(&$files) {
64 _afform_civix_civicrm_xmlMenu($files);
65 }
66
67 /**
68 * Implements hook_civicrm_install().
69 *
70 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install
71 */
72 function afform_civicrm_install() {
73 _afform_civix_civicrm_install();
74 }
75
76 /**
77 * Implements hook_civicrm_postInstall().
78 *
79 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
80 */
81 function afform_civicrm_postInstall() {
82 _afform_civix_civicrm_postInstall();
83 }
84
85 /**
86 * Implements hook_civicrm_uninstall().
87 *
88 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
89 */
90 function afform_civicrm_uninstall() {
91 _afform_civix_civicrm_uninstall();
92 }
93
94 /**
95 * Implements hook_civicrm_enable().
96 *
97 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
98 */
99 function afform_civicrm_enable() {
100 _afform_civix_civicrm_enable();
101 }
102
103 /**
104 * Implements hook_civicrm_disable().
105 *
106 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
107 */
108 function afform_civicrm_disable() {
109 _afform_civix_civicrm_disable();
110 }
111
112 /**
113 * Implements hook_civicrm_upgrade().
114 *
115 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade
116 */
117 function afform_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
118 return _afform_civix_civicrm_upgrade($op, $queue);
119 }
120
121 /**
122 * Implements hook_civicrm_managed().
123 *
124 * Generate a list of entities to create/deactivate/delete when this module
125 * is installed, disabled, uninstalled.
126 *
127 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed
128 */
129 function afform_civicrm_managed(&$entities) {
130 _afform_civix_civicrm_managed($entities);
131
132 /** @var \CRM_Afform_AfformScanner $scanner */
133 if (\Civi::container()->has('afform_scanner')) {
134 $scanner = \Civi::service('afform_scanner');
135 }
136 else {
137 // This might happen at oddballs points - e.g. while you're in the middle of re-enabling the ext.
138 // This AfformScanner instance only lives during this method call, and it feeds off the regular cache.
139 $scanner = new CRM_Afform_AfformScanner();
140 }
141
142 foreach ($scanner->getMetas() as $afform) {
143 if (empty($afform['is_dashlet']) || empty($afform['name'])) {
144 continue;
145 }
146 $entities[] = [
147 'module' => E::LONG_NAME,
148 'name' => 'afform_dashlet_' . $afform['name'],
149 'entity' => 'Dashboard',
150 'update' => 'always',
151 // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused
152 'cleanup' => 'always',
153 'params' => [
154 'version' => 3,
155 // Q: Should we loop through all domains?
156 'domain_id' => CRM_Core_BAO_Domain::getDomain()->id,
157 'is_active' => TRUE,
158 'name' => $afform['name'],
159 'label' => $afform['title'] ?? ts('(Untitled)'),
160 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
161 'permission' => "@afform:" . $afform['name'],
162 ],
163 ];
164 }
165 }
166
167 /**
168 * Implements hook_civicrm_caseTypes().
169 *
170 * Generate a list of case-types.
171 *
172 * Note: This hook only runs in CiviCRM 4.4+.
173 *
174 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes
175 */
176 function afform_civicrm_caseTypes(&$caseTypes) {
177 _afform_civix_civicrm_caseTypes($caseTypes);
178 }
179
180 /**
181 * Implements hook_civicrm_angularModules().
182 *
183 * Generate a list of Afform Angular modules.
184 */
185 function afform_civicrm_angularModules(&$angularModules) {
186 _afform_civix_civicrm_angularModules($angularModules);
187
188 $afforms = \Civi\Api4\Afform::get(FALSE)
189 ->setSelect(['name', 'requires', 'module_name', 'directive_name'])
190 ->execute();
191
192 foreach ($afforms as $afform) {
193 $angularModules[$afform['module_name']] = [
194 'ext' => E::LONG_NAME,
195 'js' => ['assetBuilder://afform.js?name=' . urlencode($afform['name'])],
196 'requires' => $afform['requires'],
197 'basePages' => [],
198 'partialsCallback' => '_afform_get_partials',
199 '_afform' => $afform['name'],
200 'exports' => [
201 $afform['directive_name'] => 'E',
202 ],
203 ];
204 }
205 }
206
207 /**
208 * Callback to retrieve partials for a given afform/angular module.
209 *
210 * @see afform_civicrm_angularModules
211 *
212 * @param string $moduleName
213 * The module name.
214 * @param array $module
215 * The module definition.
216 * @return array
217 * Array(string $filename => string $html).
218 * @throws API_Exception
219 */
220 function _afform_get_partials($moduleName, $module) {
221 $afform = civicrm_api4('Afform', 'get', [
222 'where' => [['name', '=', $module['_afform']]],
223 'select' => ['layout'],
224 'layoutFormat' => 'html',
225 'checkPermissions' => FALSE,
226 ], 0);
227 return [
228 "~/$moduleName/$moduleName.aff.html" => $afform['layout'],
229 ];
230 }
231
232 /**
233 * Scan the list of Angular modules and inject automatic-requirements.
234 *
235 * TLDR: if an afform uses element "<other-el/>", and if another module defines
236 * `$angularModules['otherMod']['exports']['el'][0] === 'other-el'`, then
237 * the 'otherMod' is automatically required.
238 *
239 * @param \Civi\Core\Event\GenericHookEvent $e
240 * @see CRM_Utils_Hook::angularModules()
241 */
242 function _afform_civicrm_angularModules_autoReq($e) {
243 /** @var CRM_Afform_AfformScanner $scanner */
244 $scanner = Civi::service('afform_scanner');
245 $moduleEnvId = md5(\CRM_Core_Config_Runtime::getId() . implode(',', array_keys($e->angularModules)));
246 $depCache = CRM_Utils_Cache::create([
247 'name' => 'afdep_' . substr($moduleEnvId, 0, 32 - 6),
248 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'],
249 'withArray' => 'fast',
250 'prefetch' => TRUE,
251 ]);
252 $depCacheTtl = 2 * 60 * 60;
253
254 $revMap = _afform_reverse_deps($e->angularModules);
255
256 $formNames = array_keys($scanner->findFilePaths());
257 foreach ($formNames as $formName) {
258 $angModule = _afform_angular_module_name($formName, 'camel');
259 $cacheLine = $depCache->get($formName, NULL);
260
261 $jFile = $scanner->findFilePath($formName, 'aff.json');
262 $hFile = $scanner->findFilePath($formName, 'aff.html');
263
264 $jStat = stat($jFile);
265 $hStat = stat($hFile);
266
267 if ($cacheLine === NULL) {
268 $needsUpdate = TRUE;
269 }
270 elseif ($jStat !== FALSE && $jStat['size'] !== $cacheLine['js']) {
271 $needsUpdate = TRUE;
272 }
273 elseif ($jStat !== FALSE && $jStat['mtime'] > $cacheLine['jm']) {
274 $needsUpdate = TRUE;
275 }
276 elseif ($hStat !== FALSE && $hStat['size'] !== $cacheLine['hs']) {
277 $needsUpdate = TRUE;
278 }
279 elseif ($hStat !== FALSE && $hStat['mtime'] > $cacheLine['hm']) {
280 $needsUpdate = TRUE;
281 }
282 else {
283 $needsUpdate = FALSE;
284 }
285
286 if ($needsUpdate) {
287 $cacheLine = [
288 'js' => $jStat['size'] ?? NULL,
289 'jm' => $jStat['mtime'] ?? NULL,
290 'hs' => $hStat['size'] ?? NULL,
291 'hm' => $hStat['mtime'] ?? NULL,
292 'r' => array_values(array_unique(array_merge(
293 [CRM_Afform_AfformScanner::DEFAULT_REQUIRES],
294 $e->angularModules[$angModule]['requires'] ?? [],
295 _afform_reverse_deps_find($formName, file_get_contents($hFile), $revMap)
296 ))),
297 ];
298 // print_r(['cache update:' . $formName => $cacheLine]);
299 $depCache->set($formName, $cacheLine, $depCacheTtl);
300 }
301
302 $e->angularModules[$angModule]['requires'] = $cacheLine['r'];
303 }
304 }
305
306 /**
307 * @param $angularModules
308 * @return array
309 * 'attr': array(string $attrName => string $angModuleName)
310 * 'el': array(string $elementName => string $angModuleName)
311 */
312 function _afform_reverse_deps($angularModules) {
313 $revMap = ['attr' => [], 'el' => []];
314 foreach (array_keys($angularModules) as $module) {
315 if (!isset($angularModules[$module]['exports'])) {
316 continue;
317 }
318 foreach ($angularModules[$module]['exports'] as $symbolName => $symbolTypes) {
319 if (strpos($symbolTypes, 'A') !== FALSE) {
320 $revMap['attr'][$symbolName] = $module;
321 }
322 if (strpos($symbolTypes, 'E') !== FALSE) {
323 $revMap['el'][$symbolName] = $module;
324 }
325 }
326 }
327 return $revMap;
328 }
329
330 /**
331 * @param string $formName
332 * @param string $html
333 * @param array $revMap
334 * The reverse-dependencies map from _afform_reverse_deps().
335 * @return array
336 * @see _afform_reverse_deps()
337 */
338 function _afform_reverse_deps_find($formName, $html, $revMap) {
339 $symbols = \Civi\Afform\Symbols::scan($html);
340 $elems = array_intersect_key($revMap['el'], $symbols->elements);
341 $attrs = array_intersect_key($revMap['attr'], $symbols->attributes);
342 return array_values(array_unique(array_merge($elems, $attrs)));
343 }
344
345 /**
346 * Implements hook_civicrm_alterSettingsFolders().
347 *
348 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders
349 */
350 function afform_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
351 _afform_civix_civicrm_alterSettingsFolders($metaDataFolders);
352 }
353
354 /**
355 * Implements hook_civicrm_entityTypes().
356 *
357 * Declare entity types provided by this module.
358 *
359 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes
360 */
361 function afform_civicrm_entityTypes(&$entityTypes) {
362 _afform_civix_civicrm_entityTypes($entityTypes);
363 }
364
365 /**
366 * Implements hook_civicrm_themes().
367 */
368 function afform_civicrm_themes(&$themes) {
369 _afform_civix_civicrm_themes($themes);
370 }
371
372 /**
373 * Implements hook_civicrm_buildAsset().
374 */
375 function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
376 if ($asset !== 'afform.js') {
377 return;
378 }
379
380 if (empty($params['name'])) {
381 throw new RuntimeException("Missing required parameter: afform.js?name=NAME");
382 }
383
384 $moduleName = _afform_angular_module_name($params['name'], 'camel');
385 $smarty = CRM_Core_Smarty::singleton();
386 $smarty->assign('afform', [
387 'camel' => $moduleName,
388 'meta' => ['name' => $params['name']],
389 'templateUrl' => "~/$moduleName/$moduleName.aff.html",
390 ]);
391 $mimeType = 'text/javascript';
392 $content = $smarty->fetch('afform/AfformAngularModule.tpl');
393 }
394
395 /**
396 * Implements hook_civicrm_alterMenu().
397 */
398 function afform_civicrm_alterMenu(&$items) {
399 if (Civi::container()->has('afform_scanner')) {
400 $scanner = Civi::service('afform_scanner');
401 }
402 else {
403 // During installation...
404 $scanner = new CRM_Afform_AfformScanner();
405 }
406 foreach ($scanner->getMetas() as $name => $meta) {
407 if (!empty($meta['server_route'])) {
408 $items[$meta['server_route']] = [
409 'page_callback' => 'CRM_Afform_Page_AfformBase',
410 'page_arguments' => 'afform=' . urlencode($name),
411 'title' => $meta['title'] ?? '',
412 'access_arguments' => [["@afform:$name"], 'and'],
413 'is_public' => $meta['is_public'],
414 ];
415 }
416 }
417 }
418
419 /**
420 * Implements hook_civicrm_permission_check().
421 *
422 * This extends the list of permissions available in `CRM_Core_Permission:check()`
423 * by introducing virtual-permissions named `@afform:myForm`. The evaluation
424 * of these virtual-permissions is dependent on the settings for `myForm`.
425 * `myForm` may be exposed/integrated through multiple subsystems (routing,
426 * nav-menu, API, etc), and the use of virtual-permissions makes easy to enforce
427 * consistent permissions across any relevant subsystems.
428 *
429 * @see CRM_Utils_Hook::permission_check()
430 */
431 function afform_civicrm_permission_check($permission, &$granted, $contactId) {
432 if ($permission[0] !== '@') {
433 // Micro-optimization - this function may get hit a lot.
434 return;
435 }
436
437 if (preg_match('/^@afform:(.*)/', $permission, $m)) {
438 $name = $m[1];
439
440 $afform = \Civi\Api4\Afform::get()
441 ->setCheckPermissions(FALSE)
442 ->addWhere('name', '=', $name)
443 ->setSelect(['permission'])
444 ->execute()
445 ->first();
446 if ($afform) {
447 $granted = CRM_Core_Permission::check($afform['permission'], $contactId);
448 }
449 }
450 }
451
452 /**
453 * Implements hook_civicrm_permissionList().
454 *
455 * @see CRM_Utils_Hook::permissionList()
456 */
457 function afform_civicrm_permissionList(&$permissions) {
458 $scanner = Civi::service('afform_scanner');
459 foreach ($scanner->getMetas() as $name => $meta) {
460 $permissions['@afform:' . $name] = [
461 'group' => 'afform',
462 'title' => ts('Afform: Inherit permission of %1', [
463 1 => $name,
464 ]),
465 ];
466 }
467 }
468
469 /**
470 * Clear any local/in-memory caches based on afform data.
471 */
472 function _afform_clear() {
473 $container = \Civi::container();
474 $container->get('afform_scanner')->clear();
475 $container->get('angular')->clear();
476 }
477
478 /**
479 * @param string $fileBaseName
480 * Ex: foo-bar
481 * @param string $format
482 * 'camel' or 'dash'.
483 * @return string
484 * Ex: 'FooBar' or 'foo-bar'.
485 * @throws \Exception
486 */
487 function _afform_angular_module_name($fileBaseName, $format = 'camel') {
488 switch ($format) {
489 case 'camel':
490 $camelCase = '';
491 foreach (preg_split('/[-_ ]/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY) as $shortNamePart) {
492 $camelCase .= ucfirst($shortNamePart);
493 }
494 return strtolower($camelCase[0]) . substr($camelCase, 1);
495
496 case 'dash':
497 return strtolower(implode('-', preg_split('/[-_ ]|(?=[A-Z])/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)));
498
499 default:
500 throw new \Exception("Unrecognized format");
501 }
502 }