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