diff --git a/Civi/Angular/Manager.php b/Civi/Angular/Manager.php
index d5888cc5d086..f2ea6b514f64 100644
--- a/Civi/Angular/Manager.php
+++ b/Civi/Angular/Manager.php
@@ -14,29 +14,13 @@ class Manager {
protected $res = NULL;
/**
- * Modules.
+ * Static cache of html partials.
*
- * @var array|null
- * Each item has some combination of these keys:
- * - ext: string
- * The Civi extension which defines the Angular module.
- * - js: array(string $relativeFilePath)
- * List of JS files (relative to the extension).
- * - css: array(string $relativeFilePath)
- * List of CSS files (relative to the extension).
- * - partials: array(string $relativeFilePath)
- * A list of partial-HTML folders (relative to the extension).
- * This will be mapped to "~/moduleName" by crmResource.
- * - settings: array(string $key => mixed $value)
- * List of settings to preload.
- * - settingsFactory: callable
- * Callback function to fetch settings.
- * - permissions: array
- * List of permissions to make available client-side
- * - requires: array
- * List of other modules required
+ * Stashing it here because it's too big to store in SqlCache
+ * FIXME: So that probably means we shouldn't be storing in memory either!
+ * @var array
*/
- protected $modules = NULL;
+ private $partials = [];
/**
* @var \CRM_Utils_Cache_Interface
@@ -68,7 +52,7 @@ public function __construct($res, \CRM_Utils_Cache_Interface $cache = NULL) {
*/
public function clear() {
$this->cache->clear();
- $this->modules = NULL;
+ $this->partials = [];
$this->changeSets = NULL;
// Force-refresh assetBuilder files
\Civi::container()->get('asset_builder')->clear(FALSE);
@@ -93,14 +77,15 @@ public function clear() {
* List of settings to preload.
*/
public function getModules() {
- if ($this->modules === NULL) {
+ $moduleNames = $this->cache->get('moduleNames');
+ $angularModules = [];
+ // Cache not set, fetch fresh list of modules and store in cache
+ if (!$moduleNames) {
$config = \CRM_Core_Config::singleton();
global $civicrm_root;
// Note: It would be nice to just glob("$civicrm_root/ang/*.ang.php"), but at time
// of writing CiviMail and CiviCase have special conditionals.
-
- $angularModules = [];
$angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.ang.php";
$angularModules['checklist-model'] = include "$civicrm_root/ang/checklist-model.ang.php";
$angularModules['crmApp'] = include "$civicrm_root/ang/crmApp.ang.php";
@@ -150,17 +135,26 @@ public function getModules() {
}
}
}
- $this->modules = $this->resolvePatterns($angularModules);
+ $angularModules = $this->resolvePatterns($angularModules);
+ $this->cache->set('moduleNames', array_keys($angularModules));
+ foreach ($angularModules as $moduleName => $moduleInfo) {
+ $this->cache->set("module $moduleName", $moduleInfo);
+ }
+ }
+ // Rehydrate modules from cache
+ else {
+ foreach ($moduleNames as $moduleName) {
+ $angularModules[$moduleName] = $this->cache->get("module $moduleName");
+ }
}
- return $this->modules;
+ return $angularModules;
}
/**
* Get the descriptor for an Angular module.
*
- * @param string $name
- * Module name.
+ * @param string $moduleName
* @return array
* Details about the module:
* - ext: string, the name of the Civi extension which defines the module
@@ -169,12 +163,12 @@ public function getModules() {
* - partials: array(string $relativeFilePath).
* @throws \Exception
*/
- public function getModule($name) {
- $modules = $this->getModules();
- if (!isset($modules[$name])) {
+ public function getModule($moduleName) {
+ $module = $this->cache->get("module $moduleName") ?? $this->getModules()[$moduleName] ?? NULL;
+ if (!isset($module)) {
throw new \Exception("Unrecognized Angular module");
}
- return $modules[$name];
+ return $module;
}
/**
@@ -292,13 +286,10 @@ public function getRawPartials($name) {
* Invalid partials configuration.
*/
public function getPartials($name) {
- $cacheKey = "angular-partials_$name";
- $cacheValue = $this->cache->get($cacheKey);
- if ($cacheValue === NULL) {
- $cacheValue = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name));
- $this->cache->set($cacheKey, $cacheValue);
+ if (!isset($this->partials[$name])) {
+ $this->partials[$name] = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name));
}
- return $cacheValue;
+ return $this->partials[$name];
}
/**
diff --git a/Civi/Api4/Utils/CoreUtil.php b/Civi/Api4/Utils/CoreUtil.php
index bb53f94cb4e1..d709ac091d39 100644
--- a/Civi/Api4/Utils/CoreUtil.php
+++ b/Civi/Api4/Utils/CoreUtil.php
@@ -239,6 +239,7 @@ public static function isCustomEntity($customGroupName): bool {
*/
public static function checkAccessRecord(AbstractAction $apiRequest, array $record, int $userID = NULL): ?bool {
$userID = $userID ?? \CRM_Core_Session::getLoggedInContactID() ?? 0;
+ $idField = self::getIdFieldName($apiRequest->getEntityName());
// Super-admins always have access to everything
if (\CRM_Core_Permission::check('all CiviCRM permissions and ACLs', $userID)) {
@@ -249,7 +250,7 @@ public static function checkAccessRecord(AbstractAction $apiRequest, array $reco
// It's a cheap trick and not as efficient as not running the query at all,
// but BAO::checkAccess doesn't consistently check permissions for the "get" action.
if (is_a($apiRequest, '\Civi\Api4\Generic\AbstractGetAction')) {
- return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
+ return (bool) $apiRequest->addSelect($idField)->addWhere($idField, '=', $record[$idField])->execute()->count();
}
$event = new \Civi\Api4\Event\AuthorizeRecordEvent($apiRequest, $record, $userID);
diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php
index bbe052765300..bdc23796dc8f 100644
--- a/Civi/Core/Container.php
+++ b/Civi/Core/Container.php
@@ -427,7 +427,14 @@ public function createContainer() {
* @return \Civi\Angular\Manager
*/
public function createAngularManager() {
- return new \Civi\Angular\Manager(\CRM_Core_Resources::singleton());
+ $moduleEnvId = md5(\CRM_Core_Config_Runtime::getId());
+ $angCache = \CRM_Utils_Cache::create([
+ 'name' => substr('angular_' . $moduleEnvId, 0, 32),
+ 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'],
+ 'withArray' => 'fast',
+ 'prefetch' => TRUE,
+ ]);
+ return new \Civi\Angular\Manager(\CRM_Core_Resources::singleton(), $angCache);
}
/**
diff --git a/ext/afform/core/CRM/Afform/AfformScanner.php b/ext/afform/core/CRM/Afform/AfformScanner.php
index 427c255b5f59..b965765c5770 100644
--- a/ext/afform/core/CRM/Afform/AfformScanner.php
+++ b/ext/afform/core/CRM/Afform/AfformScanner.php
@@ -129,54 +129,50 @@ public function clear(): void {
}
/**
- * Get the effective metadata for a form.
+ * Get metadata and optionally the layout for a file-based Afform.
*
* @param string $name
* Ex: 'afformViewIndividual'
- * @return array
- * An array with some mix of the following keys: name, title, description, server_route, requires, is_public.
- * NOTE: This is only data available in *.aff.json. It does *NOT* include layout.
- * Ex: [
- * 'name' => 'afformViewIndividual',
- * 'title' => 'View an individual contact',
- * 'server_route' => 'civicrm/view-individual',
- * 'requires' => ['afform'],
- * ]
+ * @param bool $getLayout
+ * Whether to fetch 'layout' from the related html file.
+ * @return array|null
+ * An array with some mix of the keys supported by getFields
+ * @see \Civi\Api4\Afform::getFields
*/
- public function getMeta(string $name): ?array {
- // FIXME error checking
-
- $defaults = [
- 'requires' => [],
- 'title' => '',
- 'description' => '',
- 'is_public' => FALSE,
- 'permission' => ['access CiviCRM'],
- 'type' => 'system',
- ];
+ public function getMeta(string $name, bool $getLayout = FALSE): ?array {
$defn = [];
+ $mtime = NULL;
- // If there is a local file it will be json - read from that first
$jsonFile = $this->findFilePath($name, self::METADATA_JSON);
+ $htmlFile = $this->findFilePath($name, self::LAYOUT_FILE);
+
+ // Meta file can be either php or json format.
+ // Json takes priority because local overrides are always saved in that format.
if ($jsonFile !== NULL) {
$defn = json_decode(file_get_contents($jsonFile), 1);
+ $mtime = filemtime($jsonFile);
}
// Extensions may provide afform definitions in php files
else {
$phpFile = $this->findFilePath($name, self::METADATA_PHP);
if ($phpFile !== NULL) {
$defn = include $phpFile;
+ $mtime = filemtime($phpFile);
}
}
- // A form must have at least a layout file (if no metadata file, just use defaults)
- if (!$defn && !$this->findFilePath($name, self::LAYOUT_FILE)) {
- return NULL;
+ if ($htmlFile !== NULL) {
+ $mtime = max($mtime, filemtime($htmlFile));
+ if ($getLayout) {
+ // If the defn file included a layout, the html file overrides
+ $defn['layout'] = file_get_contents($htmlFile);
+ }
}
- $defn = array_merge($defaults, $defn, ['name' => $name]);
- // Previous revisions of GUI allowed permission==''. array_merge() doesn't catch all forms of missing-ness.
- if (empty($defn['permission'])) {
- $defn['permission'] = $defaults['permission'];
+ // All 3 files don't exist!
+ elseif (!$defn) {
+ return NULL;
}
+ $defn['name'] = $name;
+ $defn['modified_date'] = date('Y-m-d H:i:s', $mtime);
return $defn;
}
@@ -198,13 +194,10 @@ public function addComputedFields(array &$record) {
}
/**
- * @param string $formName
- * Ex: 'view-individual'
- * @return string|NULL
- * Ex: 'Hello world!'
- * NULL if no layout exists
+ * @deprecated unused function
*/
public function getLayout($formName) {
+ CRM_Core_Error::deprecatedFunctionWarning('APIv4');
$filePath = $this->findFilePath($formName, self::LAYOUT_FILE);
return $filePath === NULL ? NULL : file_get_contents($filePath);
}
@@ -214,7 +207,7 @@ public function getLayout($formName) {
*
* @return array
* A list of all forms, keyed by form name.
- * NOTE: This is only data available in metadata files. It does *NOT* include layout.
+ * NOTE: This is only data available in *.aff.(json|php) files. It does *NOT* include layout.
* Ex: ['afformViewIndividual' => ['title' => 'View an individual contact', ...]]
*/
public function getMetas(): array {
diff --git a/ext/afform/core/Civi/Afform/AngularDependencyMapper.php b/ext/afform/core/Civi/Afform/AngularDependencyMapper.php
index 692ad8aebf89..889e318e7588 100644
--- a/ext/afform/core/Civi/Afform/AngularDependencyMapper.php
+++ b/ext/afform/core/Civi/Afform/AngularDependencyMapper.php
@@ -18,89 +18,37 @@
class AngularDependencyMapper {
/**
- * Scan the list of Angular modules and inject automatic-requirements.
+ * @var array{attr: array, el: array}
+ */
+ private $revMap;
+
+ public function __construct(array $angularModules) {
+ $this->revMap = $this->getRevMap($angularModules);
+ }
+
+ /**
+ * Adds angular dependencies based on the html contents of an afform.
*
* TLDR: if an afform uses element "", and if another module defines
* `$angularModules['otherMod']['exports']['el'][0] === 'other-el'`, then
* the 'otherMod' is automatically required.
*
- * @param \Civi\Core\Event\GenericHookEvent $e
+ * @param array $afform
* @see CRM_Utils_Hook::angularModules()
*/
- public static function autoReq($e) {
- /** @var \CRM_Afform_AfformScanner $scanner */
- $scanner = \Civi::service('afform_scanner');
- $moduleEnvId = md5(\CRM_Core_Config_Runtime::getId() . implode(',', array_keys($e->angularModules)));
- $depCache = \CRM_Utils_Cache::create([
- 'name' => 'afdep_' . substr($moduleEnvId, 0, 32 - 6),
- 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'],
- 'withArray' => 'fast',
- 'prefetch' => TRUE,
- ]);
- $depCacheTtl = 2 * 60 * 60;
-
- $revMap = self::reverseDeps($e->angularModules);
-
- $formNames = array_keys($scanner->findFilePaths());
- foreach ($formNames as $formName) {
- $angModule = _afform_angular_module_name($formName, 'camel');
- $cacheLine = $depCache->get($formName, NULL);
-
- $jFile = $scanner->findFilePath($formName, 'aff.json');
- $hFile = $scanner->findFilePath($formName, 'aff.html');
-
- if (!$hFile) {
- \Civi::log()->warning("Missing html file for Afform: '$jFile'");
- continue;
- }
- $jStat = $jFile ? stat($jFile) : FALSE;
- $hStat = stat($hFile);
-
- if ($cacheLine === NULL) {
- $needsUpdate = TRUE;
- }
- elseif ($jStat !== FALSE && $jStat['size'] !== $cacheLine['js']) {
- $needsUpdate = TRUE;
- }
- elseif ($jStat !== FALSE && $jStat['mtime'] > $cacheLine['jm']) {
- $needsUpdate = TRUE;
- }
- elseif ($hStat !== FALSE && $hStat['size'] !== $cacheLine['hs']) {
- $needsUpdate = TRUE;
- }
- elseif ($hStat !== FALSE && $hStat['mtime'] > $cacheLine['hm']) {
- $needsUpdate = TRUE;
- }
- else {
- $needsUpdate = FALSE;
- }
-
- if ($needsUpdate) {
- $cacheLine = [
- 'js' => $jStat['size'] ?? NULL,
- 'jm' => $jStat['mtime'] ?? NULL,
- 'hs' => $hStat['size'] ?? NULL,
- 'hm' => $hStat['mtime'] ?? NULL,
- 'r' => array_values(array_unique(array_merge(
- [\CRM_Afform_AfformScanner::DEFAULT_REQUIRES],
- $e->angularModules[$angModule]['requires'] ?? [],
- self::reverseDepsFind(file_get_contents($hFile), $revMap)
- ))),
- ];
- $depCache->set($formName, $cacheLine, $depCacheTtl);
- }
-
- $e->angularModules[$angModule]['requires'] = $cacheLine['r'];
- }
+ public function autoReq(array $afform) {
+ $afform['requires'][] = \CRM_Afform_AfformScanner::DEFAULT_REQUIRES;
+ $dependencies = empty($afform['layout']) ? [] : $this->reverseDepsFind($afform['layout']);
+ return array_values(array_unique(array_merge($afform['requires'], $dependencies)));
}
/**
- * @param $angularModules
- * @return array
- * 'attr': array(string $attrName => string $angModuleName)
- * 'el': array(string $elementName => string $angModuleName)
+ * @param array $angularModules
+ * @return array{attr: array, el: array}
+ * 'attr': [string $attrName => string $angModuleName]
+ * 'el': [string $elementName => string $angModuleName]
*/
- private static function reverseDeps($angularModules):array {
+ private function getRevMap(array $angularModules): array {
$revMap = ['attr' => [], 'el' => []];
foreach (array_keys($angularModules) as $module) {
if (!isset($angularModules[$module]['exports'])) {
@@ -120,15 +68,13 @@ private static function reverseDeps($angularModules):array {
/**
* @param string $html
- * @param array $revMap
- * The reverse-dependencies map from reverseDeps().
* @return array
*/
- private static function reverseDepsFind($html, $revMap):array {
+ private function reverseDepsFind(string $html): array {
$symbols = \Civi\Afform\Symbols::scan($html);
- $elems = array_intersect_key($revMap['el'], $symbols->elements);
- $attrs = array_intersect_key($revMap['attr'], $symbols->attributes);
- return array_values(array_unique(array_merge($elems, $attrs)));
+ $elems = array_intersect_key($this->revMap['el'], $symbols->elements);
+ $attrs = array_intersect_key($this->revMap['attr'], $symbols->attributes);
+ return array_merge($elems, $attrs);
}
}
diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Get.php b/ext/afform/core/Civi/Api4/Action/Afform/Get.php
index 9559b619aece..a309fd55107f 100644
--- a/ext/afform/core/Civi/Api4/Action/Afform/Get.php
+++ b/ext/afform/core/Civi/Api4/Action/Afform/Get.php
@@ -16,19 +16,22 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
use \Civi\Api4\Utils\AfformFormatTrait;
public function getRecords() {
+ $afforms = [];
+
/** @var \CRM_Afform_AfformScanner $scanner */
$scanner = \Civi::service('afform_scanner');
+
+ // Optimization: only fetch extra data if requested
$getComputed = $this->_isFieldSelected('has_local', 'has_base', 'base_module');
$getLayout = $this->_isFieldSelected('layout');
$getSearchDisplays = $this->_isFieldSelected('search_displays');
- $afforms = [];
-
- // This helps optimize lookups by file/module/directive name
+ // To optimize lookups by file/module/directive name
$getNames = array_filter([
'name' => $this->_itemsToGet('name'),
'module_name' => $this->_itemsToGet('module_name'),
'directive_name' => $this->_itemsToGet('directive_name'),
]);
+ // To optimize lookups by type
$getTypes = $this->_itemsToGet('type');
$names = $getNames['name'] ?? array_keys($scanner->findFilePaths());
@@ -40,22 +43,12 @@ public function getRecords() {
\Civi::dispatcher()->dispatch('civi.afform.get', $event);
// Set defaults for Afforms supplied by hook
foreach ($hookForms as $afform) {
- $name = $afform['name'];
- $afform += [
- 'has_base' => TRUE,
- 'type' => 'form',
- ];
- // afCore and af would normally get required by AngularDependencyMapper but that only works on file-based afforms
- $afform['requires'] = array_unique(array_merge(['afCore', 'af'], $afform['requires'] ?? []));
- if (!in_array($name, $names)) {
- $names[] = $name;
- }
- $afforms[$name] = $afform;
+ $names[] = $afform['name'];
+ $afform['has_base'] = TRUE;
+ $afforms[$afform['name']] = $afform;
}
- if ($this->checkPermissions) {
- $names = array_filter($names, [$this, 'checkPermission']);
- }
+ $names = array_unique($names);
foreach ($names as $name) {
$info = [
@@ -66,29 +59,35 @@ public function getRecords() {
// Skip if afform does not match requested name
foreach ($getNames as $key => $names) {
if (!in_array($info[$key], $names)) {
+ unset($afforms[$name]);
continue 2;
}
}
- $record = $scanner->getMeta($name);
+ $record = $scanner->getMeta($name, $getLayout || $getSearchDisplays);
// Skip if afform does not exist or is not of requested type(s)
if (
(!$record && !isset($afforms[$name])) ||
($getTypes && isset($record['type']) && !in_array($record['type'], $getTypes, TRUE))
) {
+ unset($afforms[$name]);
continue;
}
$afforms[$name] = array_merge($afforms[$name] ?? [], $record ?? [], $info);
- if ($getComputed) {
- $scanner->addComputedFields($afforms[$name]);
- }
if (isset($afforms[$name]['permission']) && is_string($afforms[$name]['permission'])) {
$afforms[$name]['permission'] = explode(',', $afforms[$name]['permission']);
}
- if ($getLayout || $getSearchDisplays) {
- // Autogenerated layouts will already be in values but can be overridden; scanner takes priority
- $afforms[$name]['layout'] = $scanner->getLayout($name) ?? $afforms[$name]['layout'] ?? '';
+ // No permissions specified, set default.
+ if (empty($afforms[$name]['permission'])) {
+ $afforms[$name]['permission'] = ['access CiviCRM'];
}
- if ($getSearchDisplays) {
+ if (!$this->checkPermission($afforms[$name])) {
+ unset($afforms[$name]);
+ continue;
+ }
+ if ($getComputed) {
+ $scanner->addComputedFields($afforms[$name]);
+ }
+ if ($getSearchDisplays && !empty($afforms[$name]['layout'])) {
$afforms[$name]['search_displays'] = $this->getSearchDisplays($afforms[$name]['layout']);
}
if (!isset($afforms[$name]['placement']) && $this->_isFieldSelected('placement')) {
@@ -98,7 +97,7 @@ public function getRecords() {
if ($getLayout && $this->layoutFormat !== 'html') {
foreach ($afforms as $name => $record) {
- $afforms[$name]['layout'] = $this->convertHtmlToOutput($record['layout']);
+ $afforms[$name]['layout'] = isset($record['layout']) ? $this->convertHtmlToOutput($record['layout']) : NULL;
}
}
@@ -124,8 +123,14 @@ public function getRecords() {
*
* @return bool
*/
- protected function checkPermission($name) {
- return \CRM_Core_Permission::check("@afform:$name");
+ protected function checkPermission($afform) {
+ if (!$this->checkPermissions) {
+ return TRUE;
+ }
+ if (($afform['permission_operator'] ?? NULL) === 'OR') {
+ $afform['permission'] = [$afform['permission']];
+ }
+ return \CRM_Core_Permission::check($afform['permission']);
}
/**
diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php
index 214ed88e5a6c..cac1f9f79937 100644
--- a/ext/afform/core/Civi/Api4/Afform.php
+++ b/ext/afform/core/Civi/Api4/Afform.php
@@ -243,6 +243,12 @@ public static function getFields($checkPermissions = TRUE) {
'data_type' => 'Array',
'description' => 'HTML form layout; format is controlled by layoutFormat param',
],
+ [
+ 'name' => 'modified_date',
+ 'title' => E::ts('Date Modified'),
+ 'data_type' => 'Timestamp',
+ 'readonly' => TRUE,
+ ],
];
// Calculated fields returned by get action
if ($self->getAction() === 'get') {
diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php
index ad422e8aa168..18f24071d24a 100644
--- a/ext/afform/core/afform.php
+++ b/ext/afform/core/afform.php
@@ -54,7 +54,7 @@ function afform_civicrm_config(&$config) {
$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_angularModules', '_afform_hook_civicrm_angularModules', -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']);
@@ -135,7 +135,7 @@ function afform_civicrm_managed(&$entities, $modules) {
'values' => [
'name' => $afform['name'],
'label' => $afform['navigation']['label'] ?: $afform['title'],
- 'permission' => $afform['permission'],
+ 'permission' => (array) (empty($afform['permission']) ? 'access CiviCRM' : $afform['permission']),
'permission_operator' => $afform['permission_operator'] ?? 'AND',
'weight' => $afform['navigation']['weight'] ?? 0,
'url' => $afform['server_route'],
@@ -301,17 +301,22 @@ function _afform_get_contact_types(array $mixedTypes): array {
}
/**
- * Implements hook_civicrm_angularModules().
+ * Late-listener for Angular modules: adds all Afforms and their dependencies.
*
- * Generate a list of Afform Angular modules.
+ * Must run last so that all other modules are present for reverse-dependency mapping.
+ *
+ * @implements CRM_Utils_Hook::angularModules
+ * @param \Civi\Core\Event\GenericHookEvent $e
*/
-function afform_civicrm_angularModules(&$angularModules) {
+function _afform_hook_civicrm_angularModules($e) {
$afforms = \Civi\Api4\Afform::get(FALSE)
- ->setSelect(['name', 'requires', 'module_name', 'directive_name'])
+ ->setSelect(['name', 'requires', 'module_name', 'directive_name', 'layout'])
+ ->setLayoutFormat('html')
->execute();
+ // 1st pass, add each Afform as angular module
foreach ($afforms as $afform) {
- $angularModules[$afform['module_name']] = [
+ $e->angularModules[$afform['module_name']] = [
'ext' => E::LONG_NAME,
'js' => ['assetBuilder://afform.js?name=' . urlencode($afform['name'])],
'requires' => $afform['requires'],
@@ -325,6 +330,12 @@ function afform_civicrm_angularModules(&$angularModules) {
],
];
}
+
+ // 2nd pass, now that all Angular modules are declared, add reverse dependencies
+ $dependencyMapper = new \Civi\Afform\AngularDependencyMapper($e->angularModules);
+ foreach ($afforms as $afform) {
+ $e->angularModules[$afform['module_name']]['requires'] = $dependencyMapper->autoReq($afform);
+ }
}
/**
@@ -433,31 +444,18 @@ function afform_civicrm_permission(&$permissions) {
* @see CRM_Utils_Hook::permission_check()
*/
function afform_civicrm_permission_check($permission, &$granted, $contactId) {
- if ($permission[0] !== '@') {
+ if (!str_starts_with($permission, '@afform:') || strlen($permission) < 9) {
// Micro-optimization - this function may get hit a lot.
return;
}
-
- if (preg_match('/^@afform:(.*)/', $permission, $m)) {
- $name = $m[1];
-
- $afform = \Civi\Api4\Afform::get(FALSE)
- ->addWhere('name', '=', $name)
- ->addSelect('permission', 'permission_operator')
- ->execute()
- ->first();
- // No permissions found... this shouldn't happen but just in case, set default.
- if ($afform && empty($afform['permission'])) {
- $afform['permission'] = ['access CiviCRM'];
- }
- if ($afform) {
- $check = (array) $afform['permission'];
- if ($afform['permission_operator'] === 'OR') {
- $check = [$check];
- }
- $granted = CRM_Core_Permission::check($check, $contactId);
- }
- }
+ [, $name] = explode(':', $permission, 2);
+ // Delegate permission check to APIv4
+ $check = \Civi\Api4\Afform::checkAccess()
+ ->addValue('name', $name)
+ ->setAction('get')
+ ->execute()
+ ->first();
+ $granted = $check['access'];
}
/**
diff --git a/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php b/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php
index fa5766e857bd..2478ea0695d2 100644
--- a/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php
+++ b/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php
@@ -34,6 +34,10 @@ public function testGetReturnFields() {
$this->assertEquals($this->formName, $result['name']);
$this->assertArrayNotHasKey('directive_name', $result);
$this->assertArrayNotHasKey('has_base', $result);
+ // Check modified date is reasonable
+ $this->assertGreaterThan('2023-01-01 12:00:00', $result['modified_date']);
+ // Hopefully this test won't need updating for the next 2000 years or so...
+ $this->assertLessThan('4000-01-01 12:00:00', $result['modified_date']);
// Select * should also return regular fields only
$result = Afform::get()
diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php
index b3f8c6307553..0d83ebdfb8c9 100644
--- a/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php
+++ b/ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php
@@ -3,7 +3,7 @@
use Civi\Api4\Afform;
/**
- * Test case for Afform.prefill and Afform.submit.
+ * Test case for Afform.checkAccess, Afform.prefill and Afform.submit.
*
* @group headless
*/
@@ -198,6 +198,31 @@ public function testCheckEntityReferenceFieldsReplacement(): void {
$this->callAPISuccessGetSingle('ActivityContact', ['contact_id' => $contact['id'], 'activity_id' => $activity['id']]);
}
+ public function testCheckAccess(): void {
+ $this->useValues([
+ 'layout' => self::$layouts['aboutMe'],
+ 'permission' => ['access CiviCRM'],
+ ]);
+ $this->createLoggedInUser();
+ CRM_Core_Config::singleton()->userPermissionTemp = NULL;
+ CRM_Core_Config::singleton()->userPermissionClass->permissions = [
+ 'access Contact Dashboard',
+ ];
+ $check = Afform::checkAccess()
+ ->addValue('name', $this->formName)
+ ->setAction('get')
+ ->execute()->first();
+ $this->assertFalse($check['access']);
+ CRM_Core_Config::singleton()->userPermissionClass->permissions = [
+ 'access CiviCRM',
+ ];
+ $check = Afform::checkAccess()
+ ->addValue('name', $this->formName)
+ ->setAction('get')
+ ->execute()->first();
+ $this->assertTrue($check['access']);
+ }
+
public function testAboutMeForbidden(): void {
$this->useValues([
'layout' => self::$layouts['aboutMe'],
diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php
index a408249e51d5..15172613c589 100644
--- a/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php
+++ b/ext/afform/mock/tests/phpunit/api/v4/AfformTest.php
@@ -279,8 +279,9 @@ public function testAutoRequires(): void {
// The default mockPage has 1 explicit requirement + 2 automatic requirements.
Afform::revert()->addWhere('name', '=', $formName)->execute();
$angModule = Civi::service('angular')->getModule($formName);
- $this->assertEquals(['afCore', 'mockBespoke', 'mockBareFile', 'mockFoo'], $angModule['requires']);
+ sort($angModule['requires']);
$storedRequires = Afform::get()->addWhere('name', '=', $formName)->addSelect('requires')->execute();
+ $this->assertEquals(['afCore', 'mockBareFile', 'mockBespoke', 'mockFoo'], $angModule['requires']);
$this->assertEquals(['mockBespoke'], $storedRequires[0]['requires']);
// Knock down to 1 explicit + 1 automatic.
@@ -290,8 +291,9 @@ public function testAutoRequires(): void {
->setValues(['layout' => '
The bare file says ""
'])
->execute();
$angModule = Civi::service('angular')->getModule($formName);
- $this->assertEquals(['afCore', 'mockBespoke', 'mockBareFile'], $angModule['requires']);
+ sort($angModule['requires']);
$storedRequires = Afform::get()->addWhere('name', '=', $formName)->addSelect('requires')->execute();
+ $this->assertEquals(['afCore', 'mockBareFile', 'mockBespoke'], $angModule['requires']);
$this->assertEquals(['mockBespoke'], $storedRequires[0]['requires']);
// Remove the last explict and implicit requirements.
@@ -310,7 +312,8 @@ public function testAutoRequires(): void {
Afform::revert()->addWhere('name', '=', $formName)->execute();
$angModule = Civi::service('angular')->getModule($formName);
- $this->assertEquals(['afCore', 'mockBespoke', 'mockBareFile', 'mockFoo'], $angModule['requires']);
+ sort($angModule['requires']);
+ $this->assertEquals(['afCore', 'mockBareFile', 'mockBespoke', 'mockFoo'], $angModule['requires']);
}
}
diff --git a/tests/phpunit/Civi/Angular/LoaderTest.php b/tests/phpunit/Civi/Angular/LoaderTest.php
index 0136ffde8744..2968a46aab13 100644
--- a/tests/phpunit/Civi/Angular/LoaderTest.php
+++ b/tests/phpunit/Civi/Angular/LoaderTest.php
@@ -23,6 +23,7 @@ public function setUp(): void {
$this->hookClass->setHook('civicrm_angularModules', [$this, 'hook_angularModules']);
self::$dummy_callback_count = 0;
$this->createLoggedInUser();
+ \Civi::container()->get('angular')->clear();
}
public function factoryScenarios() {