Merge pull request #23401 from eileenmcnaughton/import_data_source
[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('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'processRelationships'], 1);
55 $dispatcher->addListener('hook_civicrm_angularModules', ['\Civi\Afform\AngularDependencyMapper', 'autoReq'], -1000);
56 $dispatcher->addListener('hook_civicrm_alterAngular', ['\Civi\Afform\AfformMetadataInjector', 'preprocess']);
57 $dispatcher->addListener('hook_civicrm_check', ['\Civi\Afform\StatusChecks', 'hook_civicrm_check']);
58
59 // Register support for email tokens
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 }
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, $modules) {
130 if ($modules && !in_array(E::LONG_NAME, $modules, TRUE)) {
131 return;
132 }
133 /** @var \CRM_Afform_AfformScanner $scanner */
134 if (\Civi::container()->has('afform_scanner')) {
135 $scanner = \Civi::service('afform_scanner');
136 }
137 else {
138 // This might happen at oddballs points - e.g. while you're in the middle of re-enabling the ext.
139 // This AfformScanner instance only lives during this method call, and it feeds off the regular cache.
140 $scanner = new CRM_Afform_AfformScanner();
141 }
142
143 foreach ($scanner->getMetas() as $afform) {
144 if (empty($afform['is_dashlet']) || empty($afform['name'])) {
145 continue;
146 }
147 $entities[] = [
148 'module' => E::LONG_NAME,
149 'name' => 'afform_dashlet_' . $afform['name'],
150 'entity' => 'Dashboard',
151 'update' => 'always',
152 // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused
153 'cleanup' => 'always',
154 'params' => [
155 'version' => 4,
156 'values' => [
157 // Q: Should we loop through all domains?
158 'domain_id' => 'current_domain',
159 'is_active' => TRUE,
160 'name' => $afform['name'],
161 'label' => $afform['title'] ?? E::ts('(Untitled)'),
162 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
163 'permission' => "@afform:" . $afform['name'],
164 'url' => NULL,
165 ],
166 ],
167 ];
168 }
169 }
170
171 /**
172 * Implements hook_civicrm_tabset().
173 *
174 * Adds afforms as contact summary tabs.
175 */
176 function afform_civicrm_tabset($tabsetName, &$tabs, $context) {
177 if ($tabsetName !== 'civicrm/contact/view') {
178 return;
179 }
180 $scanner = \Civi::service('afform_scanner');
181 $weight = 111;
182 foreach ($scanner->getMetas() as $afform) {
183 if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'tab') {
184 $module = _afform_angular_module_name($afform['name']);
185 $tabs[] = [
186 'id' => $afform['name'],
187 'title' => $afform['title'],
188 'weight' => $weight++,
189 'icon' => 'crm-i fa-list-alt',
190 'is_active' => TRUE,
191 'template' => 'afform/contactSummary/AfformTab.tpl',
192 'module' => $module,
193 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
194 ];
195 // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module.
196 if (empty($context['caller'])) {
197 Civi::service('angularjs.loader')->addModules($module);
198 }
199 }
200 }
201 }
202
203 /**
204 * Implements hook_civicrm_pageRun().
205 *
206 * Adds afforms as contact summary blocks.
207 */
208 function afform_civicrm_pageRun(&$page) {
209 if (get_class($page) !== 'CRM_Contact_Page_View_Summary') {
210 return;
211 }
212 $scanner = \Civi::service('afform_scanner');
213 $cid = $page->get('cid');
214 $side = 'left';
215 foreach ($scanner->getMetas() as $afform) {
216 if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') {
217 $module = _afform_angular_module_name($afform['name']);
218 $block = [
219 'module' => $module,
220 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
221 ];
222 $content = CRM_Core_Smarty::singleton()->fetchWith('afform/contactSummary/AfformBlock.tpl', ['contactId' => $cid, 'block' => $block]);
223 CRM_Core_Region::instance("contact-basic-info-$side")->add([
224 'markup' => '<div class="crm-summary-block">' . $content . '</div>',
225 'weight' => 1,
226 ]);
227 Civi::service('angularjs.loader')->addModules($module);
228 $side = $side === 'left' ? 'right' : 'left';
229 }
230 }
231 }
232
233 /**
234 * Implements hook_civicrm_contactSummaryBlocks().
235 *
236 * @link https://github.com/civicrm/org.civicrm.contactlayout
237 */
238 function afform_civicrm_contactSummaryBlocks(&$blocks) {
239 $afforms = \Civi\Api4\Afform::get(FALSE)
240 ->setSelect(['name', 'title', 'directive_name', 'module_name', 'type', 'type:icon', 'type:label'])
241 ->addWhere('contact_summary', '=', 'block')
242 ->execute();
243 foreach ($afforms as $index => $afform) {
244 // Create a group per afform type
245 $blocks += [
246 "afform_{$afform['type']}" => [
247 'title' => $afform['type:label'],
248 'icon' => $afform['type:icon'],
249 'blocks' => [],
250 ],
251 ];
252 $blocks["afform_{$afform['type']}"]['blocks'][$afform['name']] = [
253 'title' => $afform['title'],
254 'tpl_file' => 'afform/contactSummary/AfformBlock.tpl',
255 'module' => $afform['module_name'],
256 'directive' => $afform['directive_name'],
257 'sample' => [
258 $afform['type:label'],
259 ],
260 'edit' => 'civicrm/admin/afform#/edit/' . $afform['name'],
261 'system_default' => [0, $index % 2],
262 ];
263 }
264 }
265
266 /**
267 * Implements hook_civicrm_angularModules().
268 *
269 * Generate a list of Afform Angular modules.
270 */
271 function afform_civicrm_angularModules(&$angularModules) {
272 $afforms = \Civi\Api4\Afform::get(FALSE)
273 ->setSelect(['name', 'requires', 'module_name', 'directive_name'])
274 ->execute();
275
276 foreach ($afforms as $afform) {
277 $angularModules[$afform['module_name']] = [
278 'ext' => E::LONG_NAME,
279 'js' => ['assetBuilder://afform.js?name=' . urlencode($afform['name'])],
280 'requires' => $afform['requires'],
281 'basePages' => [],
282 'partialsCallback' => '_afform_get_partials',
283 '_afform' => $afform['name'],
284 // TODO: Allow afforms to declare their own theming requirements
285 'bundles' => ['bootstrap3'],
286 'exports' => [
287 $afform['directive_name'] => 'E',
288 ],
289 ];
290 }
291 }
292
293 /**
294 * Callback to retrieve partials for a given afform/angular module.
295 *
296 * @see afform_civicrm_angularModules
297 *
298 * @param string $moduleName
299 * The module name.
300 * @param array $module
301 * The module definition.
302 * @return array
303 * Array(string $filename => string $html).
304 * @throws API_Exception
305 */
306 function _afform_get_partials($moduleName, $module) {
307 $afform = civicrm_api4('Afform', 'get', [
308 'where' => [['name', '=', $module['_afform']]],
309 'select' => ['layout'],
310 'layoutFormat' => 'html',
311 'checkPermissions' => FALSE,
312 ], 0);
313 return [
314 "~/$moduleName/$moduleName.aff.html" => $afform['layout'],
315 ];
316 }
317
318 /**
319 * Implements hook_civicrm_entityTypes().
320 *
321 * Declare entity types provided by this module.
322 *
323 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes
324 */
325 function afform_civicrm_entityTypes(&$entityTypes) {
326 _afform_civix_civicrm_entityTypes($entityTypes);
327 }
328
329 /**
330 * Implements hook_civicrm_buildAsset().
331 */
332 function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
333 if ($asset !== 'afform.js') {
334 return;
335 }
336
337 if (empty($params['name'])) {
338 throw new RuntimeException("Missing required parameter: afform.js?name=NAME");
339 }
340
341 $moduleName = _afform_angular_module_name($params['name'], 'camel');
342 $formMetaData = (array) civicrm_api4('Afform', 'get', [
343 'checkPermissions' => FALSE,
344 'select' => ['redirect', 'name', 'title'],
345 'where' => [['name', '=', $params['name']]],
346 ], 0);
347 $smarty = CRM_Core_Smarty::singleton();
348 $smarty->assign('afform', [
349 'camel' => $moduleName,
350 'meta' => $formMetaData,
351 'templateUrl' => "~/$moduleName/$moduleName.aff.html",
352 ]);
353 $mimeType = 'text/javascript';
354 $content = $smarty->fetch('afform/AfformAngularModule.tpl');
355 }
356
357 /**
358 * Implements hook_civicrm_alterMenu().
359 */
360 function afform_civicrm_alterMenu(&$items) {
361 if (Civi::container()->has('afform_scanner')) {
362 $scanner = Civi::service('afform_scanner');
363 }
364 else {
365 // During installation...
366 $scanner = new CRM_Afform_AfformScanner();
367 }
368 foreach ($scanner->getMetas() as $name => $meta) {
369 if (!empty($meta['server_route'])) {
370 $items[$meta['server_route']] = [
371 'page_callback' => 'CRM_Afform_Page_AfformBase',
372 'page_arguments' => 'afform=' . urlencode($name),
373 'access_arguments' => [["@afform:$name"], 'and'],
374 'is_public' => $meta['is_public'],
375 ];
376 }
377 }
378 }
379
380 /**
381 * Implements hook_civicrm_permission_check().
382 *
383 * This extends the list of permissions available in `CRM_Core_Permission:check()`
384 * by introducing virtual-permissions named `@afform:myForm`. The evaluation
385 * of these virtual-permissions is dependent on the settings for `myForm`.
386 * `myForm` may be exposed/integrated through multiple subsystems (routing,
387 * nav-menu, API, etc), and the use of virtual-permissions makes easy to enforce
388 * consistent permissions across any relevant subsystems.
389 *
390 * @see CRM_Utils_Hook::permission_check()
391 */
392 function afform_civicrm_permission_check($permission, &$granted, $contactId) {
393 if ($permission[0] !== '@') {
394 // Micro-optimization - this function may get hit a lot.
395 return;
396 }
397
398 if (preg_match('/^@afform:(.*)/', $permission, $m)) {
399 $name = $m[1];
400
401 $afform = \Civi\Api4\Afform::get()
402 ->setCheckPermissions(FALSE)
403 ->addWhere('name', '=', $name)
404 ->setSelect(['permission'])
405 ->execute()
406 ->first();
407 if ($afform) {
408 $granted = CRM_Core_Permission::check($afform['permission'], $contactId);
409 }
410 }
411 }
412
413 /**
414 * Implements hook_civicrm_permissionList().
415 *
416 * @see CRM_Utils_Hook::permissionList()
417 */
418 function afform_civicrm_permissionList(&$permissions) {
419 $scanner = Civi::service('afform_scanner');
420 foreach ($scanner->getMetas() as $name => $meta) {
421 $permissions['@afform:' . $name] = [
422 'group' => 'afform',
423 'title' => E::ts('Afform: Inherit permission of %1', [
424 1 => $name,
425 ]),
426 ];
427 }
428 }
429
430 /**
431 * Clear any local/in-memory caches based on afform data.
432 */
433 function _afform_clear() {
434 $container = \Civi::container();
435 $container->get('afform_scanner')->clear();
436 $container->get('angular')->clear();
437 }
438
439 /**
440 * @param string $fileBaseName
441 * Ex: foo-bar
442 * @param string $format
443 * 'camel' or 'dash'.
444 * @return string
445 * Ex: 'FooBar' or 'foo-bar'.
446 * @throws \Exception
447 */
448 function _afform_angular_module_name($fileBaseName, $format = 'camel') {
449 switch ($format) {
450 case 'camel':
451 $camelCase = '';
452 foreach (preg_split('/[-_ ]/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY) as $shortNamePart) {
453 $camelCase .= ucfirst($shortNamePart);
454 }
455 return strtolower($camelCase[0]) . substr($camelCase, 1);
456
457 case 'dash':
458 return strtolower(implode('-', preg_split('/[-_ ]|(?=[A-Z])/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)));
459
460 default:
461 throw new \Exception("Unrecognized format");
462 }
463 }
464
465 /**
466 * Implements hook_civicrm_alterApiRoutePermissions().
467 *
468 * @see CRM_Utils_Hook::alterApiRoutePermissions
469 */
470 function afform_civicrm_alterApiRoutePermissions(&$permissions, $entity, $action) {
471 if ($entity == 'Afform') {
472 // These actions should be accessible to anonymous users; permissions are checked internally
473 $allowedActions = ['prefill', 'submit', 'submitFile', 'getOptions'];
474 if (in_array($action, $allowedActions, TRUE)) {
475 $permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION;
476 }
477 }
478 }
479
480 /**
481 * Implements hook_civicrm_preProcess().
482 *
483 * Wordpress only: Adds Afforms to the shortcode dialog (when editing pages/posts).
484 */
485 function afform_civicrm_preProcess($formName, &$form) {
486 if ($formName === 'CRM_Core_Form_ShortCode') {
487 $form->components['afform'] = [
488 'label' => E::ts('Form Builder'),
489 'select' => [
490 'key' => 'name',
491 'entity' => 'Afform',
492 'select' => ['minimumInputLength' => 0],
493 'api' => [
494 'params' => ['type' => ['IN' => ['form', 'search']]],
495 ],
496 ],
497 ];
498 }
499 }
500
501 /**
502 * Implements hook_civicrm_pre().
503 */
504 function afform_civicrm_pre($op, $entity, $id, &$params) {
505 // When deleting a searchDisplay, also delete any Afforms the display is embedded within
506 if ($entity === 'SearchDisplay' && $op === 'delete') {
507 $display = \Civi\Api4\SearchDisplay::get(FALSE)
508 ->addSelect('saved_search_id.name', 'name')
509 ->addWhere('id', '=', $id)
510 ->execute()->first();
511 \Civi\Api4\Afform::revert(FALSE)
512 ->addWhere('search_displays', 'CONTAINS', $display['saved_search_id.name'] . ".{$display['name']}")
513 ->execute();
514 }
515 // When deleting a savedSearch, delete any Afforms which use the default display
516 elseif ($entity === 'SavedSearch' && $op === 'delete') {
517 $search = \Civi\Api4\SavedSearch::get(FALSE)
518 ->addSelect('name')
519 ->addWhere('id', '=', $id)
520 ->execute()->first();
521 \Civi\Api4\Afform::revert(FALSE)
522 ->addWhere('search_displays', 'CONTAINS', $search['name'])
523 ->execute();
524 }
525 }
526
527 /**
528 * Implements hook_civicrm_referenceCounts().
529 */
530 function afform_civicrm_referenceCounts($dao, &$counts) {
531 // Count afforms which contain a search display
532 if (is_a($dao, 'CRM_Search_DAO_SearchDisplay') && $dao->id) {
533 if (empty($dao->saved_search_id) || empty($dao->name)) {
534 $dao->find(TRUE);
535 }
536 $search = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $dao->saved_search_id);
537 $afforms = \Civi\Api4\Afform::get(FALSE)
538 ->selectRowCount()
539 ->addWhere('search_displays', 'CONTAINS', "$search.$dao->name")
540 ->execute();
541 if ($afforms->count()) {
542 $counts[] = [
543 'name' => 'Afform',
544 'type' => 'Afform',
545 'count' => $afforms->count(),
546 ];
547 }
548 }
549 // Count afforms which contain any displays from a SavedSearch (including the default display)
550 elseif (is_a($dao, 'CRM_Contact_DAO_SavedSearch') && $dao->id) {
551 if (empty($dao->name)) {
552 $dao->find(TRUE);
553 }
554 $clauses = [
555 ['search_displays', 'CONTAINS', $dao->name],
556 ];
557 try {
558 $displays = civicrm_api4('SearchDisplay', 'get', [
559 'where' => [['saved_search_id', '=', $dao->id]],
560 'select' => 'name',
561 ], ['name']);
562 foreach ($displays as $displayName) {
563 $clauses[] = ['search_displays', 'CONTAINS', $dao->name . '.' . $displayName];
564 }
565 }
566 catch (Exception $e) {
567 // In case SearchKit is not installed, the api call would fail
568 }
569 $afforms = \Civi\Api4\Afform::get(FALSE)
570 ->selectRowCount()
571 ->addClause('OR', $clauses)
572 ->execute();
573 if ($afforms->count()) {
574 $counts[] = [
575 'name' => 'Afform',
576 'type' => 'Afform',
577 'count' => $afforms->count(),
578 ];
579 }
580 }
581 }
582
583 // Wordpress only: Register callback for rendering shortcodes
584 if (function_exists('add_filter')) {
585 add_filter('civicrm_shortcode_get_markup', 'afform_shortcode_content', 10, 4);
586 }
587
588 /**
589 * Wordpress only: Render Afform content for shortcodes.
590 *
591 * @param string $content
592 * HTML Markup
593 * @param array $atts
594 * Shortcode attributes.
595 * @param array $args
596 * Existing shortcode arguments.
597 * @param string $context
598 * How many shortcodes are present on the page: 'single' or 'multiple'.
599 * @return string
600 * Modified markup.
601 */
602 function afform_shortcode_content($content, $atts, $args, $context) {
603 if ($atts['component'] === 'afform') {
604 $afform = civicrm_api4('Afform', 'get', [
605 'select' => ['directive_name', 'module_name'],
606 'where' => [['name', '=', $atts['name']]],
607 ])->first();
608 if ($afform) {
609 Civi::service('angularjs.loader')->addModules($afform['module_name']);
610 $content = "
611 <div class='crm-container' id='bootstrap-theme'>
612 <crm-angular-js modules='{$afform['module_name']}'>
613 <{$afform['directive_name']}></{$afform['directive_name']}>
614 </crm-angular-js>
615 </div>";
616 }
617 }
618 return $content;
619 }