4 * Class CRM_Afform_AfformScanner
6 * The AfformScanner searches the extensions and `civicrm.files` for subfolders
7 * named `afform`. Each item in there is interpreted as a form instance.
9 * To reduce file-scanning, we keep a cache of file paths.
11 class CRM_Afform_AfformScanner
{
13 const METADATA_FILE
= 'aff.json';
15 const LAYOUT_FILE
= 'aff.html';
17 const FILE_REGEXP
= '/\.aff\.(json|html)$/';
19 const DEFAULT_REQUIRES
= 'afCore';
22 * @var CRM_Utils_Cache_Interface
27 * CRM_Afform_AfformScanner constructor.
29 public function __construct() {
30 $this->cache
= Civi
::cache('long');
34 * Get a list of all forms and their file paths.
37 * Ex: ['view-individual' => ['/var/www/foo/afform/view-individual']]
39 public function findFilePaths() {
40 if (!CRM_Core_Config
::singleton()->debug
) {
41 // FIXME: Use a separate setting. Maybe use the asset-builder cache setting?
42 $paths = $this->cache
->get('afformAllPaths');
43 if ($paths !== NULL) {
50 $mapper = CRM_Extension_System
::singleton()->getMapper();
51 foreach ($mapper->getModules() as $module) {
52 /** @var $module CRM_Core_Module */
54 if ($module->is_active
) {
55 $this->appendFilePaths($paths, dirname($mapper->keyToPath($module->name
)) . DIRECTORY_SEPARATOR
. 'ang', 20);
58 catch (CRM_Extension_Exception_MissingException
$e) {
59 // If the extension is missing skip & continue.
63 $this->appendFilePaths($paths, $this->getSiteLocalPath(), 10);
65 $this->cache
->set('afformAllPaths', $paths);
70 * Get the full path to the given file.
72 * @param string $formName
73 * Ex: 'view-individual'
74 * @param string $suffix
77 * Ex: '/var/www/sites/default/files/civicrm/afform/view-individual.aff.json'
79 public function findFilePath($formName, $suffix) {
80 $paths = $this->findFilePaths();
82 if (isset($paths[$formName])) {
83 foreach ($paths[$formName] as $path) {
84 if (file_exists($path . '.' . $suffix)) {
85 return $path . '.' . $suffix;
94 * Determine the path where we can write our own customized/overriden
97 * @param string $formName
98 * Ex: 'view-individual'
101 * @return string|NULL
102 * Ex: '/var/www/sites/default/files/civicrm/afform/view-individual.aff.json'
104 public function createSiteLocalPath($formName, $file) {
105 return $this->getSiteLocalPath() . DIRECTORY_SEPARATOR
. $formName . '.' . $file;
108 public function clear() {
109 $this->cache
->delete('afformAllPaths');
113 * Get the effective metadata for a form.
115 * @param string $name
116 * Ex: 'view-individual'
118 * An array with some mix of the following keys: name, title, description, server_route, requires, is_public.
119 * NOTE: This is only data available in *.aff.json. It does *NOT* include layout.
121 * 'name' => 'view-individual',
122 * 'title' => 'View an individual contact',
123 * 'server_route' => 'civicrm/view-individual',
124 * 'requires' => ['afform'],
127 public function getMeta($name) {
128 // FIXME error checking
135 'is_dashlet' => FALSE,
136 'is_public' => FALSE,
138 'permission' => 'access CiviCRM',
142 $metaFile = $this->findFilePath($name, self
::METADATA_FILE
);
143 if ($metaFile !== NULL) {
144 $r = array_merge($defaults, json_decode(file_get_contents($metaFile), 1));
145 // Previous revisions of GUI allowed permission==''. array_merge() doesn't catch all forms of missing-ness.
146 if ($r['permission'] === '') {
147 $r['permission'] = $defaults['permission'];
151 elseif ($this->findFilePath($name, self
::LAYOUT_FILE
)) {
160 * Adds has_local & has_base to an afform metadata record
162 * @param array $record
164 public function addComputedFields(&$record) {
165 $name = $record['name'];
166 // Ex: $allPaths['viewIndividual'][0] == '/var/www/foo/afform/view-individual'].
167 $allPaths = $this->findFilePaths()[$name] ??
[];
168 // $activeLayoutPath = $this->findFilePath($name, self::LAYOUT_FILE);
169 // $activeMetaPath = $this->findFilePath($name, self::METADATA_FILE);
170 $localLayoutPath = $this->createSiteLocalPath($name, self
::LAYOUT_FILE
);
171 $localMetaPath = $this->createSiteLocalPath($name, self
::METADATA_FILE
);
173 $record['has_local'] = file_exists($localLayoutPath) ||
file_exists($localMetaPath);
174 if (!isset($record['has_base'])) {
175 $record['has_base'] = ($record['has_local'] && count($allPaths) > 1)
176 ||
(!$record['has_local'] && count($allPaths) > 0);
181 * @param string $formName
182 * Ex: 'view-individual'
183 * @return string|NULL
184 * Ex: '<em>Hello world!</em>'
185 * NULL if no layout exists
187 public function getLayout($formName) {
188 $filePath = $this->findFilePath($formName, self
::LAYOUT_FILE
);
189 return $filePath === NULL ?
NULL : file_get_contents($filePath);
193 * Get the effective metadata for all forms.
196 * A list of all forms, keyed by form name.
197 * NOTE: This is only data available in *.aff.json. It does *NOT* include layout.
198 * Ex: ['view-individual' => ['title' => 'View an individual contact', ...]]
200 public function getMetas() {
202 foreach (array_keys($this->findFilePaths()) as $name) {
203 $result[$name] = $this->getMeta($name);
209 * @param array $formPaths
210 * List of all form paths.
211 * Ex: ['foo' => [0 => '/var/www/org.example.foobar/ang']]
212 * @param string $parent
213 * Ex: '/var/www/org.example.foobar/afform/'
214 * @param int $priority
215 * Lower priority files override higher priority files.
217 private function appendFilePaths(&$formPaths, $parent, $priority) {
218 $files = preg_grep(self
::FILE_REGEXP
, (array) glob("$parent/*"));
220 foreach ($files as $file) {
221 $fileBase = preg_replace(self
::FILE_REGEXP
, '', $file);
222 $name = basename($fileBase);
223 $formPaths[$name][$priority] = $fileBase;
224 ksort($formPaths[$name]);
229 * Get the path where site-local form customizations are stored.
231 * @return mixed|string
232 * Ex: '/var/www/sites/default/files/civicrm/afform'.
234 public function getSiteLocalPath() {
235 // TODO Allow a setting override.
236 // return Civi::paths()->getPath(Civi::settings()->get('afformPath'));
237 return Civi
::paths()->getPath('[civicrm.files]/ang');
243 private function getMarkerRegexp() {
246 $v = '/\.(' . preg_quote(self
::LAYOUT_FILE
, '/') . '|' . preg_quote(self
::METADATA_FILE
, '/') . ')$/';