setAction('create')->execute()->indexBy('name'); foreach ($fields as $fieldName => $field) { if (isset($params[$fieldName])) { $result[$fieldName] = $params[$fieldName]; if ($field['data_type'] === 'Boolean' && !is_bool($params[$fieldName])) { $result[$fieldName] = CRM_Utils_String::strtobool($params[$fieldName]); } } } return $result; } /** * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container */ function afform_civicrm_container($container) { $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__)); $container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\Definition( 'CRM_Afform_AfformScanner', [] ))->setPublic(TRUE); } /** * Implements hook_civicrm_config(). * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config */ function afform_civicrm_config(&$config) { _afform_civix_civicrm_config($config); if (isset(Civi::$statics[__FUNCTION__])) { return; } Civi::$statics[__FUNCTION__] = 1; $dispatcher = Civi::dispatcher(); $dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'processGenericEntity'], 0); $dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'preprocessContact'], 10); $dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'processRelationships'], 1); $dispatcher->addListener('hook_civicrm_angularModules', ['\Civi\Afform\AngularDependencyMapper', 'autoReq'], -1000); $dispatcher->addListener('hook_civicrm_alterAngular', ['\Civi\Afform\AfformMetadataInjector', 'preprocess']); $dispatcher->addListener('hook_civicrm_check', ['\Civi\Afform\StatusChecks', 'hook_civicrm_check']); $dispatcher->addListener('civi.afform.get', ['\Civi\Api4\Action\Afform\Get', 'getCustomGroupBlocks']); // Register support for email tokens if (CRM_Extension_System::singleton()->getMapper()->isActiveModule('authx')) { $dispatcher->addListener('hook_civicrm_alterMailContent', ['\Civi\Afform\Tokens', 'applyCkeditorWorkaround']); $dispatcher->addListener('hook_civicrm_tokens', ['\Civi\Afform\Tokens', 'hook_civicrm_tokens']); $dispatcher->addListener('hook_civicrm_tokenValues', ['\Civi\Afform\Tokens', 'hook_civicrm_tokenValues']); } } /** * Implements hook_civicrm_install(). * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install */ function afform_civicrm_install() { _afform_civix_civicrm_install(); } /** * Implements hook_civicrm_postInstall(). * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall */ function afform_civicrm_postInstall() { _afform_civix_civicrm_postInstall(); } /** * Implements hook_civicrm_uninstall(). * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall */ function afform_civicrm_uninstall() { _afform_civix_civicrm_uninstall(); } /** * Implements hook_civicrm_enable(). * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable */ function afform_civicrm_enable() { _afform_civix_civicrm_enable(); } /** * Implements hook_civicrm_disable(). * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable */ function afform_civicrm_disable() { _afform_civix_civicrm_disable(); } /** * Implements hook_civicrm_upgrade(). * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade */ function afform_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { return _afform_civix_civicrm_upgrade($op, $queue); } /** * Implements hook_civicrm_managed(). * * Generate a list of entities to create/deactivate/delete when this module * is installed, disabled, uninstalled. * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed */ function afform_civicrm_managed(&$entities, $modules) { if ($modules && !in_array(E::LONG_NAME, $modules, TRUE)) { return; } /** @var \CRM_Afform_AfformScanner $scanner */ if (\Civi::container()->has('afform_scanner')) { $scanner = \Civi::service('afform_scanner'); } else { // This might happen at oddballs points - e.g. while you're in the middle of re-enabling the ext. // This AfformScanner instance only lives during this method call, and it feeds off the regular cache. $scanner = new CRM_Afform_AfformScanner(); } foreach ($scanner->getMetas() as $afform) { if (empty($afform['is_dashlet']) || empty($afform['name'])) { continue; } $entities[] = [ 'module' => E::LONG_NAME, 'name' => 'afform_dashlet_' . $afform['name'], 'entity' => 'Dashboard', 'update' => 'always', // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused 'cleanup' => 'always', 'params' => [ 'version' => 4, 'values' => [ // Q: Should we loop through all domains? 'domain_id' => 'current_domain', 'is_active' => TRUE, 'name' => $afform['name'], 'label' => $afform['title'] ?? E::ts('(Untitled)'), 'directive' => _afform_angular_module_name($afform['name'], 'dash'), 'permission' => "@afform:" . $afform['name'], 'url' => NULL, ], ], ]; } } /** * Implements hook_civicrm_tabset(). * * Adds afforms as contact summary tabs. */ function afform_civicrm_tabset($tabsetName, &$tabs, $context) { if ($tabsetName !== 'civicrm/contact/view') { return; } $scanner = \Civi::service('afform_scanner'); $weight = 111; foreach ($scanner->getMetas() as $afform) { if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'tab') { $module = _afform_angular_module_name($afform['name']); $tabs[] = [ 'id' => $afform['name'], 'title' => $afform['title'], 'weight' => $weight++, 'icon' => 'crm-i fa-list-alt', 'is_active' => TRUE, 'template' => 'afform/contactSummary/AfformTab.tpl', 'module' => $module, 'directive' => _afform_angular_module_name($afform['name'], 'dash'), ]; // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module. if (empty($context['caller'])) { Civi::service('angularjs.loader')->addModules($module); } } } } /** * Implements hook_civicrm_pageRun(). * * Adds afforms as contact summary blocks. */ function afform_civicrm_pageRun(&$page) { if (get_class($page) !== 'CRM_Contact_Page_View_Summary') { return; } $scanner = \Civi::service('afform_scanner'); $cid = $page->get('cid'); $side = 'left'; foreach ($scanner->getMetas() as $afform) { if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') { $module = _afform_angular_module_name($afform['name']); $block = [ 'module' => $module, 'directive' => _afform_angular_module_name($afform['name'], 'dash'), ]; $content = CRM_Core_Smarty::singleton()->fetchWith('afform/contactSummary/AfformBlock.tpl', ['contactId' => $cid, 'block' => $block]); CRM_Core_Region::instance("contact-basic-info-$side")->add([ 'markup' => '
' . $content . '
', 'weight' => 1, ]); Civi::service('angularjs.loader')->addModules($module); $side = $side === 'left' ? 'right' : 'left'; } } } /** * Implements hook_civicrm_contactSummaryBlocks(). * * @link https://github.com/civicrm/org.civicrm.contactlayout */ function afform_civicrm_contactSummaryBlocks(&$blocks) { $afforms = \Civi\Api4\Afform::get(FALSE) ->setSelect(['name', 'title', 'directive_name', 'module_name', 'type', 'type:icon', 'type:label']) ->addWhere('contact_summary', '=', 'block') ->execute(); foreach ($afforms as $index => $afform) { // Create a group per afform type $blocks += [ "afform_{$afform['type']}" => [ 'title' => $afform['type:label'], 'icon' => $afform['type:icon'], 'blocks' => [], ], ]; $blocks["afform_{$afform['type']}"]['blocks'][$afform['name']] = [ 'title' => $afform['title'], 'tpl_file' => 'afform/contactSummary/AfformBlock.tpl', 'module' => $afform['module_name'], 'directive' => $afform['directive_name'], 'sample' => [ $afform['type:label'], ], 'edit' => 'civicrm/admin/afform#/edit/' . $afform['name'], 'system_default' => [0, $index % 2], ]; } } /** * Implements hook_civicrm_angularModules(). * * Generate a list of Afform Angular modules. */ function afform_civicrm_angularModules(&$angularModules) { $afforms = \Civi\Api4\Afform::get(FALSE) ->setSelect(['name', 'requires', 'module_name', 'directive_name']) ->execute(); foreach ($afforms as $afform) { $angularModules[$afform['module_name']] = [ 'ext' => E::LONG_NAME, 'js' => ['assetBuilder://afform.js?name=' . urlencode($afform['name'])], 'requires' => $afform['requires'], 'basePages' => [], 'partialsCallback' => '_afform_get_partials', '_afform' => $afform['name'], // TODO: Allow afforms to declare their own theming requirements 'bundles' => ['bootstrap3'], 'exports' => [ $afform['directive_name'] => 'E', ], ]; } } /** * Callback to retrieve partials for a given afform/angular module. * * @see afform_civicrm_angularModules * * @param string $moduleName * The module name. * @param array $module * The module definition. * @return array * Array(string $filename => string $html). * @throws API_Exception */ function _afform_get_partials($moduleName, $module) { $afform = civicrm_api4('Afform', 'get', [ 'where' => [['name', '=', $module['_afform']]], 'select' => ['layout'], 'layoutFormat' => 'html', 'checkPermissions' => FALSE, ], 0); return [ "~/$moduleName/$moduleName.aff.html" => $afform['layout'], ]; } /** * Implements hook_civicrm_entityTypes(). * * Declare entity types provided by this module. * * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes */ function afform_civicrm_entityTypes(&$entityTypes) { _afform_civix_civicrm_entityTypes($entityTypes); } /** * Implements hook_civicrm_buildAsset(). */ function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) { if ($asset !== 'afform.js') { return; } if (empty($params['name'])) { throw new RuntimeException("Missing required parameter: afform.js?name=NAME"); } $moduleName = _afform_angular_module_name($params['name'], 'camel'); $formMetaData = (array) civicrm_api4('Afform', 'get', [ 'checkPermissions' => FALSE, 'select' => ['redirect', 'name', 'title'], 'where' => [['name', '=', $params['name']]], ], 0); $smarty = CRM_Core_Smarty::singleton(); $smarty->assign('afform', [ 'camel' => $moduleName, 'meta' => $formMetaData, 'templateUrl' => "~/$moduleName/$moduleName.aff.html", ]); $mimeType = 'text/javascript'; $content = $smarty->fetch('afform/AfformAngularModule.tpl'); } /** * Implements hook_civicrm_alterMenu(). */ function afform_civicrm_alterMenu(&$items) { try { $afforms = \Civi\Api4\Afform::get(FALSE) ->addWhere('server_route', 'IS NOT EMPTY') ->addSelect('name', 'server_route', 'is_public') ->execute()->indexBy('name'); } catch (Exception $e) { // During installation... $scanner = new CRM_Afform_AfformScanner(); $afforms = $scanner->getMetas(); } foreach ($afforms as $name => $meta) { if (!empty($meta['server_route'])) { $items[$meta['server_route']] = [ 'page_callback' => 'CRM_Afform_Page_AfformBase', 'page_arguments' => 'afform=' . urlencode($name), 'access_arguments' => [["@afform:$name"], 'and'], 'is_public' => $meta['is_public'], ]; } } } /** * Implements hook_civicrm_permission(). * * Define Afform permissions. */ function afform_civicrm_permission(&$permissions) { $permissions['administer afform'] = [ E::ts('Form Builder: edit and delete forms'), E::ts('Allows non-admin users to create, update and delete forms'), ]; } /** * Implements hook_civicrm_permission_check(). * * This extends the list of permissions available in `CRM_Core_Permission:check()` * by introducing virtual-permissions named `@afform:myForm`. The evaluation * of these virtual-permissions is dependent on the settings for `myForm`. * `myForm` may be exposed/integrated through multiple subsystems (routing, * nav-menu, API, etc), and the use of virtual-permissions makes easy to enforce * consistent permissions across any relevant subsystems. * * @see CRM_Utils_Hook::permission_check() */ function afform_civicrm_permission_check($permission, &$granted, $contactId) { if ($permission[0] !== '@') { // Micro-optimization - this function may get hit a lot. return; } if (preg_match('/^@afform:(.*)/', $permission, $m)) { $name = $m[1]; $afform = \Civi\Api4\Afform::get() ->setCheckPermissions(FALSE) ->addWhere('name', '=', $name) ->setSelect(['permission']) ->execute() ->first(); if ($afform) { $granted = CRM_Core_Permission::check($afform['permission'], $contactId); } } } /** * Implements hook_civicrm_permissionList(). * * @see CRM_Utils_Hook::permissionList() */ function afform_civicrm_permissionList(&$permissions) { $scanner = Civi::service('afform_scanner'); foreach ($scanner->getMetas() as $name => $meta) { $permissions['@afform:' . $name] = [ 'group' => 'afform', 'title' => E::ts('Afform: Inherit permission of %1', [ 1 => $name, ]), ]; } } /** * Clear any local/in-memory caches based on afform data. */ function _afform_clear() { $container = \Civi::container(); $container->get('afform_scanner')->clear(); $container->get('angular')->clear(); } /** * @param string $fileBaseName * Ex: foo-bar * @param string $format * 'camel' or 'dash'. * @return string * Ex: 'FooBar' or 'foo-bar'. * @throws \Exception */ function _afform_angular_module_name($fileBaseName, $format = 'camel') { switch ($format) { case 'camel': $camelCase = ''; foreach (preg_split('/[-_ ]/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY) as $shortNamePart) { $camelCase .= ucfirst($shortNamePart); } return strtolower($camelCase[0]) . substr($camelCase, 1); case 'dash': return strtolower(implode('-', preg_split('/[-_ ]|(?=[A-Z])/', $fileBaseName, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE))); default: throw new \Exception("Unrecognized format"); } } /** * Implements hook_civicrm_alterApiRoutePermissions(). * * @see CRM_Utils_Hook::alterApiRoutePermissions */ function afform_civicrm_alterApiRoutePermissions(&$permissions, $entity, $action) { if ($entity == 'Afform') { // These actions should be accessible to anonymous users; permissions are checked internally $allowedActions = ['prefill', 'submit', 'submitFile', 'getOptions']; if (in_array($action, $allowedActions, TRUE)) { $permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION; } } } /** * Implements hook_civicrm_preProcess(). * * Wordpress only: Adds Afforms to the shortcode dialog (when editing pages/posts). */ function afform_civicrm_preProcess($formName, &$form) { if ($formName === 'CRM_Core_Form_ShortCode') { $form->components['afform'] = [ 'label' => E::ts('Form Builder'), 'select' => [ 'key' => 'name', 'entity' => 'Afform', 'select' => ['minimumInputLength' => 0], 'api' => [ 'params' => ['type' => ['IN' => ['form', 'search']]], ], ], ]; } } /** * Implements hook_civicrm_pre(). */ function afform_civicrm_pre($op, $entity, $id, &$params) { // When deleting a searchDisplay, also delete any Afforms the display is embedded within if ($entity === 'SearchDisplay' && $op === 'delete') { $display = \Civi\Api4\SearchDisplay::get(FALSE) ->addSelect('saved_search_id.name', 'name') ->addWhere('id', '=', $id) ->execute()->first(); \Civi\Api4\Afform::revert(FALSE) ->addWhere('search_displays', 'CONTAINS', $display['saved_search_id.name'] . ".{$display['name']}") ->execute(); } // When deleting a savedSearch, delete any Afforms which use the default display elseif ($entity === 'SavedSearch' && $op === 'delete') { $search = \Civi\Api4\SavedSearch::get(FALSE) ->addSelect('name') ->addWhere('id', '=', $id) ->execute()->first(); \Civi\Api4\Afform::revert(FALSE) ->addWhere('search_displays', 'CONTAINS', $search['name']) ->execute(); } } /** * Implements hook_civicrm_referenceCounts(). */ function afform_civicrm_referenceCounts($dao, &$counts) { // Count afforms which contain a search display if (is_a($dao, 'CRM_Search_DAO_SearchDisplay') && $dao->id) { if (empty($dao->saved_search_id) || empty($dao->name)) { $dao->find(TRUE); } $search = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $dao->saved_search_id); $afforms = \Civi\Api4\Afform::get(FALSE) ->selectRowCount() ->addWhere('search_displays', 'CONTAINS', "$search.$dao->name") ->execute(); if ($afforms->count()) { $counts[] = [ 'name' => 'Afform', 'type' => 'Afform', 'count' => $afforms->count(), ]; } } // Count afforms which contain any displays from a SavedSearch (including the default display) elseif (is_a($dao, 'CRM_Contact_DAO_SavedSearch') && $dao->id) { if (empty($dao->name)) { $dao->find(TRUE); } $clauses = [ ['search_displays', 'CONTAINS', $dao->name], ]; try { $displays = civicrm_api4('SearchDisplay', 'get', [ 'where' => [['saved_search_id', '=', $dao->id]], 'select' => 'name', ], ['name']); foreach ($displays as $displayName) { $clauses[] = ['search_displays', 'CONTAINS', $dao->name . '.' . $displayName]; } } catch (Exception $e) { // In case SearchKit is not installed, the api call would fail } $afforms = \Civi\Api4\Afform::get(FALSE) ->selectRowCount() ->addClause('OR', $clauses) ->execute(); if ($afforms->count()) { $counts[] = [ 'name' => 'Afform', 'type' => 'Afform', 'count' => $afforms->count(), ]; } } } // Wordpress only: Register callback for rendering shortcodes if (function_exists('add_filter')) { add_filter('civicrm_shortcode_get_markup', 'afform_shortcode_content', 10, 4); } /** * Wordpress only: Render Afform content for shortcodes. * * @param string $content * HTML Markup * @param array $atts * Shortcode attributes. * @param array $args * Existing shortcode arguments. * @param string $context * How many shortcodes are present on the page: 'single' or 'multiple'. * @return string * Modified markup. */ function afform_shortcode_content($content, $atts, $args, $context) { if ($atts['component'] === 'afform') { $afform = civicrm_api4('Afform', 'get', [ 'select' => ['directive_name', 'module_name'], 'where' => [['name', '=', $atts['name']]], ])->first(); if ($afform) { Civi::service('angularjs.loader')->addModules($afform['module_name']); $content = "
<{$afform['directive_name']}>
"; } } return $content; }