Afform - Rename blocks and joins for clarity
[civicrm-core.git] / ext / afform / core / CRM / Afform / AfformScanner.php
1 <?php
2
3 /**
4 * Class CRM_Afform_AfformScanner
5 *
6 * The AfformScanner searches the extensions and `civicrm.files` for subfolders
7 * named `afform`. Each item in there is interpreted as a form instance.
8 *
9 * To reduce file-scanning, we keep a cache of file paths.
10 */
11 class CRM_Afform_AfformScanner {
12
13 const METADATA_FILE = 'aff.json';
14
15 const LAYOUT_FILE = 'aff.html';
16
17 const FILE_REGEXP = '/\.aff\.(json|html)$/';
18
19 const DEFAULT_REQUIRES = 'afCore';
20
21 /**
22 * @var CRM_Utils_Cache_Interface
23 */
24 protected $cache;
25
26 /**
27 * CRM_Afform_AfformScanner constructor.
28 */
29 public function __construct() {
30 $this->cache = Civi::cache('long');
31 }
32
33 /**
34 * Get a list of all forms and their file paths.
35 *
36 * @return array
37 * Ex: ['view-individual' => ['/var/www/foo/afform/view-individual']]
38 */
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) {
44 return $paths;
45 }
46 }
47
48 $paths = [];
49
50 $mapper = CRM_Extension_System::singleton()->getMapper();
51 foreach ($mapper->getModules() as $module) {
52 /** @var $module CRM_Core_Module */
53 try {
54 if ($module->is_active) {
55 $this->appendFilePaths($paths, dirname($mapper->keyToPath($module->name)) . DIRECTORY_SEPARATOR . 'ang', 20);
56 }
57 }
58 catch (CRM_Extension_Exception_MissingException $e) {
59 // If the extension is missing skip & continue.
60 }
61 }
62
63 $this->appendFilePaths($paths, $this->getSiteLocalPath(), 10);
64
65 $this->cache->set('afformAllPaths', $paths);
66 return $paths;
67 }
68
69 /**
70 * Get the full path to the given file.
71 *
72 * @param string $formName
73 * Ex: 'view-individual'
74 * @param string $suffix
75 * Ex: 'aff.json'
76 * @return string|NULL
77 * Ex: '/var/www/sites/default/files/civicrm/afform/view-individual.aff.json'
78 */
79 public function findFilePath($formName, $suffix) {
80 $paths = $this->findFilePaths();
81
82 if (isset($paths[$formName])) {
83 foreach ($paths[$formName] as $path) {
84 if (file_exists($path . '.' . $suffix)) {
85 return $path . '.' . $suffix;
86 }
87 }
88 }
89
90 return NULL;
91 }
92
93 /**
94 * Determine the path where we can write our own customized/overriden
95 * version of a file.
96 *
97 * @param string $formName
98 * Ex: 'view-individual'
99 * @param string $file
100 * Ex: 'aff.json'
101 * @return string|NULL
102 * Ex: '/var/www/sites/default/files/civicrm/afform/view-individual.aff.json'
103 */
104 public function createSiteLocalPath($formName, $file) {
105 return $this->getSiteLocalPath() . DIRECTORY_SEPARATOR . $formName . '.' . $file;
106 }
107
108 public function clear() {
109 $this->cache->delete('afformAllPaths');
110 }
111
112 /**
113 * Get the effective metadata for a form.
114 *
115 * @param string $name
116 * Ex: 'view-individual'
117 * @return array
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.
120 * Ex: [
121 * 'name' => 'view-individual',
122 * 'title' => 'View an individual contact',
123 * 'server_route' => 'civicrm/view-individual',
124 * 'requires' => ['afform'],
125 * ]
126 */
127 public function getMeta($name) {
128 // FIXME error checking
129
130 $defaults = [
131 'name' => $name,
132 'requires' => [],
133 'title' => '',
134 'description' => '',
135 'is_dashlet' => FALSE,
136 'is_public' => FALSE,
137 'is_token' => FALSE,
138 'permission' => 'access CiviCRM',
139 'type' => 'system',
140 ];
141
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'];
148 }
149 return $r;
150 }
151 elseif ($this->findFilePath($name, self::LAYOUT_FILE)) {
152 return $defaults;
153 }
154 else {
155 return NULL;
156 }
157 }
158
159 /**
160 * Adds has_local & has_base to an afform metadata record
161 *
162 * @param array $record
163 */
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);
172
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);
177 }
178 }
179
180 /**
181 * @param string $formName
182 * Ex: 'view-individual'
183 * @return string|NULL
184 * Ex: '<em>Hello world!</em>'
185 * NULL if no layout exists
186 */
187 public function getLayout($formName) {
188 $filePath = $this->findFilePath($formName, self::LAYOUT_FILE);
189 return $filePath === NULL ? NULL : file_get_contents($filePath);
190 }
191
192 /**
193 * Get the effective metadata for all forms.
194 *
195 * @return array
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', ...]]
199 */
200 public function getMetas() {
201 $result = [];
202 foreach (array_keys($this->findFilePaths()) as $name) {
203 $result[$name] = $this->getMeta($name);
204 }
205 return $result;
206 }
207
208 /**
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.
216 */
217 private function appendFilePaths(&$formPaths, $parent, $priority) {
218 $files = preg_grep(self::FILE_REGEXP, (array) glob("$parent/*"));
219
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]);
225 }
226 }
227
228 /**
229 * Get the path where site-local form customizations are stored.
230 *
231 * @return mixed|string
232 * Ex: '/var/www/sites/default/files/civicrm/afform'.
233 */
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');
238 }
239
240 /**
241 * @return string
242 */
243 private function getMarkerRegexp() {
244 static $v;
245 if ($v === NULL) {
246 $v = '/\.(' . preg_quote(self::LAYOUT_FILE, '/') . '|' . preg_quote(self::METADATA_FILE, '/') . ')$/';
247 }
248 return $v;
249 }
250
251 }