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