Afform - add entityRef widget support.
[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 = [];
67d666c6 15 $fields = \Civi\Api4\Afform::getfields(FALSE)->setAction('create')->execute()->indexBy('name');
50868e8d
CW
16 foreach ($fields as $fieldName => $field) {
17 if (isset($params[$fieldName])) {
18 $result[$fieldName] = $params[$fieldName];
d1ec770c 19
e38db494
CW
20 if ($field['data_type'] === 'Boolean' && !is_bool($params[$fieldName])) {
21 $result[$fieldName] = CRM_Utils_String::strtobool($params[$fieldName]);
d1ec770c
TO
22 }
23 }
bc3b7c5b
TO
24 }
25 return $result;
26}
27
8f4a0ee9 28/**
5591cfbf 29 * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
8f4a0ee9
TO
30 */
31function afform_civicrm_container($container) {
77dccccb 32 $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
8f4a0ee9
TO
33 $container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\Definition(
34 'CRM_Afform_AfformScanner',
5591cfbf 35 []
b53fe171 36 ))->setPublic(TRUE);
8f4a0ee9
TO
37}
38
66aa0f5e
TO
39/**
40 * Implements hook_civicrm_config().
41 *
42 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config
43 */
44function afform_civicrm_config(&$config) {
45 _afform_civix_civicrm_config($config);
77dccccb
TO
46
47 if (isset(Civi::$statics[__FUNCTION__])) {
48 return;
49 }
50 Civi::$statics[__FUNCTION__] = 1;
51
b7edd04e
TO
52 $dispatcher = Civi::dispatcher();
53 $dispatcher->addListener(Submit::EVENT_NAME, [Submit::class, 'processContacts'], 500);
54 $dispatcher->addListener(Submit::EVENT_NAME, [Submit::class, 'processGenericEntity'], -1000);
55 $dispatcher->addListener('hook_civicrm_angularModules', ['\Civi\Afform\AngularDependencyMapper', 'autoReq'], -1000);
56 $dispatcher->addListener('hook_civicrm_alterAngular', ['\Civi\Afform\AfformMetadataInjector', 'preprocess']);
bdceaf62 57 $dispatcher->addListener('hook_civicrm_check', ['\Civi\Afform\StatusChecks', 'hook_civicrm_check']);
b7edd04e
TO
58
59 // Register support for email tokens
bdceaf62
TO
60 if (CRM_Extension_System::singleton()->getMapper()->isActiveModule('authx')) {
61 $dispatcher->addListener('hook_civicrm_alterMailContent', ['\Civi\Afform\Tokens', 'applyCkeditorWorkaround']);
62 $dispatcher->addListener('hook_civicrm_tokens', ['\Civi\Afform\Tokens', 'hook_civicrm_tokens']);
63 $dispatcher->addListener('hook_civicrm_tokenValues', ['\Civi\Afform\Tokens', 'hook_civicrm_tokenValues']);
64 }
66aa0f5e
TO
65}
66
67/**
68 * Implements hook_civicrm_xmlMenu().
69 *
70 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu
71 */
72function afform_civicrm_xmlMenu(&$files) {
73 _afform_civix_civicrm_xmlMenu($files);
74}
75
76/**
77 * Implements hook_civicrm_install().
78 *
79 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install
80 */
81function afform_civicrm_install() {
82 _afform_civix_civicrm_install();
83}
84
85/**
86 * Implements hook_civicrm_postInstall().
87 *
88 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
89 */
90function afform_civicrm_postInstall() {
91 _afform_civix_civicrm_postInstall();
92}
93
94/**
95 * Implements hook_civicrm_uninstall().
96 *
97 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
98 */
99function afform_civicrm_uninstall() {
100 _afform_civix_civicrm_uninstall();
101}
102
103/**
104 * Implements hook_civicrm_enable().
105 *
106 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
107 */
108function afform_civicrm_enable() {
109 _afform_civix_civicrm_enable();
110}
111
112/**
113 * Implements hook_civicrm_disable().
114 *
115 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
116 */
117function afform_civicrm_disable() {
118 _afform_civix_civicrm_disable();
119}
120
121/**
122 * Implements hook_civicrm_upgrade().
123 *
124 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade
125 */
126function afform_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
127 return _afform_civix_civicrm_upgrade($op, $queue);
128}
129
130/**
131 * Implements hook_civicrm_managed().
132 *
133 * Generate a list of entities to create/deactivate/delete when this module
134 * is installed, disabled, uninstalled.
135 *
136 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed
137 */
138function afform_civicrm_managed(&$entities) {
139 _afform_civix_civicrm_managed($entities);
521b232a
TO
140
141 /** @var \CRM_Afform_AfformScanner $scanner */
142 if (\Civi::container()->has('afform_scanner')) {
143 $scanner = \Civi::service('afform_scanner');
144 }
145 else {
146 // This might happen at oddballs points - e.g. while you're in the middle of re-enabling the ext.
147 // This AfformScanner instance only lives during this method call, and it feeds off the regular cache.
148 $scanner = new CRM_Afform_AfformScanner();
149 }
150
151 foreach ($scanner->getMetas() as $afform) {
152 if (empty($afform['is_dashlet']) || empty($afform['name'])) {
153 continue;
154 }
155 $entities[] = [
156 'module' => E::LONG_NAME,
157 'name' => 'afform_dashlet_' . $afform['name'],
158 'entity' => 'Dashboard',
159 'update' => 'always',
160 // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused
161 'cleanup' => 'always',
162 'params' => [
163 'version' => 3,
164 // Q: Should we loop through all domains?
165 'domain_id' => CRM_Core_BAO_Domain::getDomain()->id,
166 'is_active' => TRUE,
167 'name' => $afform['name'],
67d666c6 168 'label' => $afform['title'] ?? E::ts('(Untitled)'),
521b232a
TO
169 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
170 'permission' => "@afform:" . $afform['name'],
171 ],
172 ];
173 }
66aa0f5e
TO
174}
175
176/**
c63f20d3 177 * Implements hook_civicrm_tabset().
66aa0f5e 178 *
c63f20d3
CW
179 * Adds afforms as contact summary tabs.
180 */
181function afform_civicrm_tabset($tabsetName, &$tabs, $context) {
182 if ($tabsetName !== 'civicrm/contact/view') {
183 return;
184 }
185 $scanner = \Civi::service('afform_scanner');
186 $weight = 111;
187 foreach ($scanner->getMetas() as $afform) {
188 if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'tab') {
189 $module = _afform_angular_module_name($afform['name']);
190 $tabs[] = [
191 'id' => $afform['name'],
192 'title' => $afform['title'],
193 'weight' => $weight++,
194 'icon' => 'crm-i fa-list-alt',
195 'is_active' => TRUE,
196 'template' => 'afform/contactSummary/AfformTab.tpl',
197 'module' => $module,
198 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
199 ];
200 // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module.
201 if (empty($context['caller'])) {
202 Civi::service('angularjs.loader')->addModules($module);
203 }
204 }
205 }
206}
207
208/**
209 * Implements hook_civicrm_pageRun().
66aa0f5e 210 *
c63f20d3
CW
211 * Adds afforms as contact summary blocks.
212 */
213function afform_civicrm_pageRun(&$page) {
214 if (get_class($page) !== 'CRM_Contact_Page_View_Summary') {
215 return;
216 }
217 $scanner = \Civi::service('afform_scanner');
218 $cid = $page->get('cid');
219 $side = 'left';
220 foreach ($scanner->getMetas() as $afform) {
221 if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') {
222 $module = _afform_angular_module_name($afform['name']);
223 $block = [
224 'module' => $module,
225 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
226 ];
227 $content = CRM_Core_Smarty::singleton()->fetchWith('afform/contactSummary/AfformBlock.tpl', ['contactId' => $cid, 'block' => $block]);
228 CRM_Core_Region::instance("contact-basic-info-$side")->add([
229 'markup' => '<div class="crm-summary-block">' . $content . '</div>',
230 'weight' => 1,
231 ]);
232 Civi::service('angularjs.loader')->addModules($module);
233 $side = $side === 'left' ? 'right' : 'left';
234 }
235 }
236}
237
238/**
239 * Implements hook_civicrm_contactSummaryBlocks().
66aa0f5e 240 *
c63f20d3 241 * @link https://github.com/civicrm/org.civicrm.contactlayout
66aa0f5e 242 */
c63f20d3
CW
243function afform_civicrm_contactSummaryBlocks(&$blocks) {
244 $scanner = \Civi::service('afform_scanner');
245 foreach ($scanner->getMetas() as $afform) {
246 if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') {
247 // Provide our own group for this block to visually distinguish it on the contact summary editor palette.
248 $blocks += [
249 'afform' => [
250 'title' => ts('Form Builder'),
251 'icon' => 'fa-list-alt',
252 'blocks' => [],
253 ],
254 ];
255 $blocks['afform']['blocks'][$afform['name']] = [
256 'title' => $afform['title'],
257 'tpl_file' => 'afform/contactSummary/AfformBlock.tpl',
258 'module' => _afform_angular_module_name($afform['name']),
259 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
260 'sample' => [],
261 'edit' => 'civicrm/admin/afform#/edit/' . $afform['name'],
262 ];
263 }
264 }
66aa0f5e
TO
265}
266
267/**
268 * Implements hook_civicrm_angularModules().
269 *
e1aca853 270 * Generate a list of Afform Angular modules.
66aa0f5e
TO
271 */
272function afform_civicrm_angularModules(&$angularModules) {
273 _afform_civix_civicrm_angularModules($angularModules);
bb56ac78 274
5e04a2d4 275 $afforms = \Civi\Api4\Afform::get(FALSE)
e38db494 276 ->setSelect(['name', 'requires', 'module_name', 'directive_name'])
2d4bfef1
CW
277 ->execute();
278
279 foreach ($afforms as $afform) {
e38db494 280 $angularModules[$afform['module_name']] = [
bb56ac78 281 'ext' => E::LONG_NAME,
2d4bfef1
CW
282 'js' => ['assetBuilder://afform.js?name=' . urlencode($afform['name'])],
283 'requires' => $afform['requires'],
bb56ac78 284 'basePages' => [],
aa6abb77 285 'partialsCallback' => '_afform_get_partials',
2d4bfef1 286 '_afform' => $afform['name'],
292054ac
CW
287 // TODO: Allow afforms to declare their own theming requirements
288 'bundles' => ['bootstrap3'],
77dccccb 289 'exports' => [
04417491 290 $afform['directive_name'] => 'E',
77dccccb 291 ],
bb56ac78 292 ];
bb56ac78 293 }
66aa0f5e
TO
294}
295
aa6abb77 296/**
2d4bfef1
CW
297 * Callback to retrieve partials for a given afform/angular module.
298 *
299 * @see afform_civicrm_angularModules
aa6abb77
TO
300 *
301 * @param string $moduleName
302 * The module name.
303 * @param array $module
304 * The module definition.
305 * @return array
306 * Array(string $filename => string $html).
2d4bfef1 307 * @throws API_Exception
aa6abb77
TO
308 */
309function _afform_get_partials($moduleName, $module) {
2d4bfef1
CW
310 $afform = civicrm_api4('Afform', 'get', [
311 'where' => [['name', '=', $module['_afform']]],
312 'select' => ['layout'],
313 'layoutFormat' => 'html',
314 'checkPermissions' => FALSE,
315 ], 0);
aa6abb77 316 return [
2d4bfef1 317 "~/$moduleName/$moduleName.aff.html" => $afform['layout'],
aa6abb77
TO
318 ];
319}
320
66aa0f5e
TO
321/**
322 * Implements hook_civicrm_alterSettingsFolders().
323 *
324 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders
325 */
326function afform_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
327 _afform_civix_civicrm_alterSettingsFolders($metaDataFolders);
328}
329
330/**
331 * Implements hook_civicrm_entityTypes().
332 *
333 * Declare entity types provided by this module.
334 *
335 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes
336 */
337function afform_civicrm_entityTypes(&$entityTypes) {
338 _afform_civix_civicrm_entityTypes($entityTypes);
339}
340
98f4a7cb
TO
341/**
342 * Implements hook_civicrm_themes().
343 */
344function afform_civicrm_themes(&$themes) {
345 _afform_civix_civicrm_themes($themes);
346}
347
bb56ac78
TO
348/**
349 * Implements hook_civicrm_buildAsset().
350 */
351function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
352 if ($asset !== 'afform.js') {
353 return;
354 }
355
356 if (empty($params['name'])) {
357 throw new RuntimeException("Missing required parameter: afform.js?name=NAME");
358 }
359
2d4bfef1 360 $moduleName = _afform_angular_module_name($params['name'], 'camel');
bb56ac78
TO
361 $smarty = CRM_Core_Smarty::singleton();
362 $smarty->assign('afform', [
aa6abb77 363 'camel' => $moduleName,
2d4bfef1 364 'meta' => ['name' => $params['name']],
aa6abb77 365 'templateUrl' => "~/$moduleName/$moduleName.aff.html",
bb56ac78
TO
366 ]);
367 $mimeType = 'text/javascript';
9ec944f2 368 $content = $smarty->fetch('afform/AfformAngularModule.tpl');
bb56ac78
TO
369}
370
8775c48a
TO
371/**
372 * Implements hook_civicrm_alterMenu().
373 */
374function afform_civicrm_alterMenu(&$items) {
8f4a0ee9
TO
375 if (Civi::container()->has('afform_scanner')) {
376 $scanner = Civi::service('afform_scanner');
377 }
378 else {
379 // During installation...
380 $scanner = new CRM_Afform_AfformScanner();
381 }
8775c48a
TO
382 foreach ($scanner->getMetas() as $name => $meta) {
383 if (!empty($meta['server_route'])) {
384 $items[$meta['server_route']] = [
385 'page_callback' => 'CRM_Afform_Page_AfformBase',
386 'page_arguments' => 'afform=' . urlencode($name),
13bdd6d2 387 'title' => $meta['title'] ?? '',
f16b2aee 388 'access_arguments' => [["@afform:$name"], 'and'],
254f01f0 389 'is_public' => $meta['is_public'],
8775c48a
TO
390 ];
391 }
392 }
f16b2aee
TO
393}
394
395/**
396 * Implements hook_civicrm_permission_check().
397 *
586344a7
TO
398 * This extends the list of permissions available in `CRM_Core_Permission:check()`
399 * by introducing virtual-permissions named `@afform:myForm`. The evaluation
400 * of these virtual-permissions is dependent on the settings for `myForm`.
401 * `myForm` may be exposed/integrated through multiple subsystems (routing,
402 * nav-menu, API, etc), and the use of virtual-permissions makes easy to enforce
403 * consistent permissions across any relevant subsystems.
404 *
f16b2aee
TO
405 * @see CRM_Utils_Hook::permission_check()
406 */
407function afform_civicrm_permission_check($permission, &$granted, $contactId) {
14b26ac5 408 if ($permission[0] !== '@') {
f16b2aee
TO
409 // Micro-optimization - this function may get hit a lot.
410 return;
411 }
412
413 if (preg_match('/^@afform:(.*)/', $permission, $m)) {
414 $name = $m[1];
415
2d4bfef1
CW
416 $afform = \Civi\Api4\Afform::get()
417 ->setCheckPermissions(FALSE)
418 ->addWhere('name', '=', $name)
419 ->setSelect(['permission'])
420 ->execute()
421 ->first();
422 if ($afform) {
423 $granted = CRM_Core_Permission::check($afform['permission'], $contactId);
424 }
f16b2aee 425 }
8775c48a
TO
426}
427
c4e6b413
TO
428/**
429 * Implements hook_civicrm_permissionList().
430 *
431 * @see CRM_Utils_Hook::permissionList()
432 */
433function afform_civicrm_permissionList(&$permissions) {
434 $scanner = Civi::service('afform_scanner');
435 foreach ($scanner->getMetas() as $name => $meta) {
436 $permissions['@afform:' . $name] = [
437 'group' => 'afform',
67d666c6 438 'title' => E::ts('Afform: Inherit permission of %1', [
c4e6b413
TO
439 1 => $name,
440 ]),
441 ];
442 }
443}
444
74f862e4
TO
445/**
446 * Clear any local/in-memory caches based on afform data.
447 */
448function _afform_clear() {
449 $container = \Civi::container();
450 $container->get('afform_scanner')->clear();
76b9562a 451 $container->get('angular')->clear();
74f862e4
TO
452}
453
bb56ac78 454/**
87dde5eb
TO
455 * @param string $fileBaseName
456 * Ex: foo-bar
841850b1
TO
457 * @param string $format
458 * 'camel' or 'dash'.
bb56ac78 459 * @return string
841850b1 460 * Ex: 'FooBar' or 'foo-bar'.
3cd5c38b 461 * @throws \Exception
bb56ac78 462 */
87dde5eb 463function _afform_angular_module_name($fileBaseName, $format = 'camel') {
841850b1
TO
464 switch ($format) {
465 case 'camel':
87dde5eb 466 $camelCase = '';
9c84a124 467 foreach (preg_split('/[-_ ]/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY) as $shortNamePart) {
87dde5eb
TO
468 $camelCase .= ucfirst($shortNamePart);
469 }
14b26ac5 470 return strtolower($camelCase[0]) . substr($camelCase, 1);
841850b1
TO
471
472 case 'dash':
9c84a124 473 return strtolower(implode('-', preg_split('/[-_ ]|(?=[A-Z])/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)));
841850b1
TO
474
475 default:
476 throw new \Exception("Unrecognized format");
477 }
bb56ac78 478}
355881ac
SL
479
480/**
481 * Implements hook_civicrm_alterApiRoutePermissions().
482 *
483 * @see CRM_Utils_Hook::alterApiRoutePermissions
484 */
485function afform_civicrm_alterApiRoutePermissions(&$permissions, $entity, $action) {
f362c531
TO
486 if ($entity == 'Afform') {
487 if ($action == 'prefill' || $action == 'submit') {
488 $permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION;
489 }
355881ac
SL
490 }
491}