Commit | Line | Data |
---|---|---|
66aa0f5e TO |
1 | <?php |
2 | ||
3 | require_once 'afform.civix.php'; | |
4 | use CRM_Afform_ExtensionUtil as E; | |
fb388832 | 5 | use Civi\Api4\Action\Afform\Submit; |
66aa0f5e | 6 | |
bc3b7c5b | 7 | function _afform_fields() { |
d1ec770c | 8 | return ['name', 'title', 'description', 'requires', 'layout', 'server_route', 'client_route', 'is_public']; |
bc3b7c5b TO |
9 | } |
10 | ||
11 | /** | |
12 | * Filter the content of $params to only have supported afform fields. | |
13 | * | |
14 | * @param array $params | |
15 | * @return array | |
16 | */ | |
17 | function _afform_fields_filter($params) { | |
5591cfbf | 18 | $result = []; |
bc3b7c5b TO |
19 | foreach (_afform_fields() as $field) { |
20 | if (isset($params[$field])) { | |
21 | $result[$field] = $params[$field]; | |
22 | } | |
d1ec770c TO |
23 | |
24 | if (isset($result[$field])) { | |
25 | switch ($field) { | |
26 | case 'is_public': | |
27 | $result[$field] = CRM_Utils_String::strtobool($result[$field]); | |
28 | break; | |
29 | ||
30 | } | |
31 | } | |
bc3b7c5b TO |
32 | } |
33 | return $result; | |
34 | } | |
35 | ||
8f4a0ee9 | 36 | /** |
5591cfbf | 37 | * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container |
8f4a0ee9 TO |
38 | */ |
39 | function afform_civicrm_container($container) { | |
77dccccb | 40 | $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__)); |
8f4a0ee9 TO |
41 | $container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\Definition( |
42 | 'CRM_Afform_AfformScanner', | |
5591cfbf | 43 | [] |
8f4a0ee9 TO |
44 | )); |
45 | } | |
46 | ||
66aa0f5e TO |
47 | /** |
48 | * Implements hook_civicrm_config(). | |
49 | * | |
50 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config | |
51 | */ | |
52 | function afform_civicrm_config(&$config) { | |
53 | _afform_civix_civicrm_config($config); | |
77dccccb TO |
54 | |
55 | if (isset(Civi::$statics[__FUNCTION__])) { | |
56 | return; | |
57 | } | |
58 | Civi::$statics[__FUNCTION__] = 1; | |
59 | ||
fb388832 TO |
60 | // Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processContacts'], -500); |
61 | Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processGenericEntity'], -1000); | |
77dccccb | 62 | Civi::dispatcher()->addListener('hook_civicrm_angularModules', '_afform_civicrm_angularModules_autoReq', -1000); |
66aa0f5e TO |
63 | } |
64 | ||
65 | /** | |
66 | * Implements hook_civicrm_xmlMenu(). | |
67 | * | |
68 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu | |
69 | */ | |
70 | function afform_civicrm_xmlMenu(&$files) { | |
71 | _afform_civix_civicrm_xmlMenu($files); | |
72 | } | |
73 | ||
74 | /** | |
75 | * Implements hook_civicrm_install(). | |
76 | * | |
77 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install | |
78 | */ | |
79 | function afform_civicrm_install() { | |
80 | _afform_civix_civicrm_install(); | |
81 | } | |
82 | ||
83 | /** | |
84 | * Implements hook_civicrm_postInstall(). | |
85 | * | |
86 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall | |
87 | */ | |
88 | function afform_civicrm_postInstall() { | |
89 | _afform_civix_civicrm_postInstall(); | |
90 | } | |
91 | ||
92 | /** | |
93 | * Implements hook_civicrm_uninstall(). | |
94 | * | |
95 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall | |
96 | */ | |
97 | function afform_civicrm_uninstall() { | |
98 | _afform_civix_civicrm_uninstall(); | |
99 | } | |
100 | ||
101 | /** | |
102 | * Implements hook_civicrm_enable(). | |
103 | * | |
104 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable | |
105 | */ | |
106 | function afform_civicrm_enable() { | |
107 | _afform_civix_civicrm_enable(); | |
108 | } | |
109 | ||
110 | /** | |
111 | * Implements hook_civicrm_disable(). | |
112 | * | |
113 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable | |
114 | */ | |
115 | function afform_civicrm_disable() { | |
116 | _afform_civix_civicrm_disable(); | |
117 | } | |
118 | ||
119 | /** | |
120 | * Implements hook_civicrm_upgrade(). | |
121 | * | |
122 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade | |
123 | */ | |
124 | function afform_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { | |
125 | return _afform_civix_civicrm_upgrade($op, $queue); | |
126 | } | |
127 | ||
128 | /** | |
129 | * Implements hook_civicrm_managed(). | |
130 | * | |
131 | * Generate a list of entities to create/deactivate/delete when this module | |
132 | * is installed, disabled, uninstalled. | |
133 | * | |
134 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed | |
135 | */ | |
136 | function afform_civicrm_managed(&$entities) { | |
137 | _afform_civix_civicrm_managed($entities); | |
138 | } | |
139 | ||
140 | /** | |
141 | * Implements hook_civicrm_caseTypes(). | |
142 | * | |
143 | * Generate a list of case-types. | |
144 | * | |
145 | * Note: This hook only runs in CiviCRM 4.4+. | |
146 | * | |
147 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes | |
148 | */ | |
149 | function afform_civicrm_caseTypes(&$caseTypes) { | |
150 | _afform_civix_civicrm_caseTypes($caseTypes); | |
151 | } | |
152 | ||
153 | /** | |
154 | * Implements hook_civicrm_angularModules(). | |
155 | * | |
156 | * Generate a list of Angular modules. | |
157 | * | |
158 | * Note: This hook only runs in CiviCRM 4.5+. It may | |
159 | * use features only available in v4.6+. | |
160 | * | |
161 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules | |
162 | */ | |
163 | function afform_civicrm_angularModules(&$angularModules) { | |
164 | _afform_civix_civicrm_angularModules($angularModules); | |
bb56ac78 | 165 | |
77dccccb | 166 | /** @var CRM_Afform_AfformScanner $scanner */ |
8f4a0ee9 | 167 | $scanner = Civi::service('afform_scanner'); |
bb56ac78 TO |
168 | $names = array_keys($scanner->findFilePaths()); |
169 | foreach ($names as $name) { | |
170 | $meta = $scanner->getMeta($name); | |
87dde5eb | 171 | $angularModules[_afform_angular_module_name($name, 'camel')] = [ |
bb56ac78 TO |
172 | 'ext' => E::LONG_NAME, |
173 | 'js' => ['assetBuilder://afform.js?name=' . urlencode($name)], | |
174 | 'requires' => $meta['requires'], | |
175 | 'basePages' => [], | |
77dccccb | 176 | 'exports' => [ |
23315411 | 177 | _afform_angular_module_name($name, 'dash') => 'AE', |
77dccccb | 178 | ], |
bb56ac78 TO |
179 | ]; |
180 | ||
181 | // FIXME: The HTML layout template is embedded in the JS asset. | |
182 | // This works at runtime for basic usage, but it bypasses | |
7d2f28fe | 183 | // the normal workflow for templates (e.g. translation). |
bb56ac78 TO |
184 | // We should update core so that 'partials' can be specified more dynamically. |
185 | } | |
66aa0f5e TO |
186 | } |
187 | ||
77dccccb TO |
188 | /** |
189 | * Scan the list of Angular modules and inject automatic-requirements. | |
190 | * | |
191 | * TLDR: if an afform uses element "<other-el/>", and if another module defines | |
192 | * `$angularModules['otherMod']['exports']['el'][0] === 'other-el'`, then | |
193 | * the 'otherMod' is automatically required. | |
194 | * | |
195 | * @param \Civi\Core\Event\GenericHookEvent $e | |
196 | * @see CRM_Utils_Hook::angularModules() | |
197 | */ | |
198 | function _afform_civicrm_angularModules_autoReq($e) { | |
199 | /** @var CRM_Afform_AfformScanner $scanner */ | |
200 | $scanner = Civi::service('afform_scanner'); | |
201 | $moduleEnvId = md5(\CRM_Core_Config_Runtime::getId() . implode(',', array_keys($e->angularModules))); | |
202 | $depCache = CRM_Utils_Cache::create([ | |
203 | 'name' => 'afdep_' . substr($moduleEnvId, 0, 32 - 6), | |
204 | 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'], | |
205 | 'withArray' => 'fast', | |
206 | 'prefetch' => TRUE, | |
207 | ]); | |
208 | $depCacheTtl = 2 * 60 * 60; | |
209 | ||
210 | $revMap = _afform_reverse_deps($e->angularModules); | |
211 | ||
212 | $formNames = array_keys($scanner->findFilePaths()); | |
213 | foreach ($formNames as $formName) { | |
214 | $angModule = _afform_angular_module_name($formName, 'camel'); | |
215 | $cacheLine = $depCache->get($formName, NULL); | |
216 | ||
217 | $jFile = $scanner->findFilePath($formName, 'aff.json'); | |
218 | $hFile = $scanner->findFilePath($formName, 'aff.html'); | |
219 | ||
220 | $jStat = stat($jFile); | |
221 | $hStat = stat($hFile); | |
222 | ||
223 | if ($cacheLine === NULL) { | |
224 | $needsUpdate = TRUE; | |
225 | } | |
226 | elseif ($jStat !== FALSE && $jStat['size'] !== $cacheLine['js']) { | |
227 | $needsUpdate = TRUE; | |
228 | } | |
229 | elseif ($jStat !== FALSE && $jStat['mtime'] > $cacheLine['jm']) { | |
230 | $needsUpdate = TRUE; | |
231 | } | |
232 | elseif ($hStat !== FALSE && $hStat['size'] !== $cacheLine['hs']) { | |
233 | $needsUpdate = TRUE; | |
234 | } | |
235 | elseif ($hStat !== FALSE && $hStat['mtime'] > $cacheLine['hm']) { | |
236 | $needsUpdate = TRUE; | |
237 | } | |
238 | else { | |
239 | $needsUpdate = FALSE; | |
240 | } | |
241 | ||
242 | if ($needsUpdate) { | |
243 | $cacheLine = [ | |
244 | 'js' => $jStat['size'] ?? NULL, | |
245 | 'jm' => $jStat['mtime'] ?? NULL, | |
246 | 'hs' => $hStat['size'] ?? NULL, | |
247 | 'hm' => $hStat['mtime'] ?? NULL, | |
248 | 'r' => array_values(array_unique(array_merge( | |
249 | [CRM_Afform_AfformScanner::DEFAULT_REQUIRES], | |
250 | $e->angularModules[$angModule]['requires'] ?? [], | |
251 | _afform_reverse_deps_find($formName, file_get_contents($hFile), $revMap) | |
252 | ))), | |
253 | ]; | |
254 | // print_r(['cache update:' . $formName => $cacheLine]); | |
255 | $depCache->set($formName, $cacheLine, $depCacheTtl); | |
256 | } | |
257 | ||
258 | $e->angularModules[$angModule]['requires'] = $cacheLine['r']; | |
259 | } | |
260 | } | |
261 | ||
262 | /** | |
263 | * @param $angularModules | |
264 | * @return array | |
265 | * 'attr': array(string $attrName => string $angModuleName) | |
266 | * 'el': array(string $elementName => string $angModuleName) | |
267 | */ | |
268 | function _afform_reverse_deps($angularModules) { | |
23315411 TO |
269 | $revMap = ['attr' => [], 'el' => []]; |
270 | foreach (array_keys($angularModules) as $module) { | |
271 | if (!isset($angularModules[$module]['exports'])) { | |
272 | continue; | |
273 | } | |
274 | foreach ($angularModules[$module]['exports'] as $symbolName => $symbolTypes) { | |
275 | if (strpos($symbolTypes, 'A') !== FALSE) { | |
276 | $revMap['attr'][$symbolName] = $module; | |
277 | } | |
278 | if (strpos($symbolTypes, 'E') !== FALSE) { | |
279 | $revMap['el'][$symbolName] = $module; | |
77dccccb TO |
280 | } |
281 | } | |
282 | } | |
283 | return $revMap; | |
284 | } | |
285 | ||
23315411 TO |
286 | /** |
287 | * @param string $formName | |
288 | * @param string $html | |
289 | * @param array $revMap | |
290 | * The reverse-dependencies map from _afform_reverse_deps(). | |
291 | * @return array | |
292 | * @see _afform_reverse_deps() | |
293 | */ | |
77dccccb TO |
294 | function _afform_reverse_deps_find($formName, $html, $revMap) { |
295 | $symbols = \Civi\Afform\Symbols::scan($html); | |
296 | $elems = array_intersect_key($revMap['el'], $symbols->elements); | |
297 | $attrs = array_intersect_key($revMap['attr'], $symbols->attributes); | |
298 | return array_values(array_unique(array_merge($elems, $attrs))); | |
299 | } | |
300 | ||
9384980c TO |
301 | /** |
302 | * @param \Civi\Angular\Manager $angular | |
303 | * @see CRM_Utils_Hook::alterAngular() | |
304 | */ | |
305 | function afform_civicrm_alterAngular($angular) { | |
306 | $fieldMetadata = \Civi\Angular\ChangeSet::create('fieldMetadata') | |
3cd5c38b CW |
307 | ->alterHtml(';^~afform/;', function($doc, $path) { |
308 | $entities = _afform_getMetadata($doc); | |
9384980c TO |
309 | |
310 | foreach (pq('af-field', $doc) as $afField) { | |
311 | /** @var DOMElement $afField */ | |
0eaa6e27 | 312 | $fieldName = $afField->getAttribute('name'); |
56b9319d | 313 | $entityName = pq($afField)->parents('[af-fieldset]')->attr('af-fieldset'); |
9384980c TO |
314 | if (!preg_match(';^[a-zA-Z0-9\_\-\. ]+$;', $entityName)) { |
315 | throw new \CRM_Core_Exception("Cannot process $path: malformed entity name ($entityName)"); | |
316 | } | |
317 | $entityType = $entities[$entityName]['type']; | |
318 | $getFields = civicrm_api4($entityType, 'getFields', [ | |
65c9e7ae | 319 | 'action' => 'create', |
9384980c | 320 | 'where' => [['name', '=', $fieldName]], |
cef2e16a | 321 | 'select' => ['title', 'input_type', 'input_attrs', 'options'], |
c9920ea7 | 322 | 'loadOptions' => TRUE, |
9384980c | 323 | ]); |
cef2e16a | 324 | // Merge field definition data with whatever's already in the markup |
9384980c | 325 | foreach ($getFields as $field) { |
a715f6d4 | 326 | $existingFieldDefn = trim(pq($afField)->attr('defn') ?: ''); |
cef2e16a CW |
327 | if ($existingFieldDefn && $existingFieldDefn[0] != '{') { |
328 | // If it's not an object, don't mess with it. | |
329 | continue; | |
330 | } | |
331 | foreach ($field as &$prop) { | |
332 | $prop = json_encode($prop, JSON_UNESCAPED_SLASHES); | |
333 | } | |
334 | if ($existingFieldDefn) { | |
335 | $field = array_merge($field, CRM_Utils_JS::getRawProps($existingFieldDefn)); | |
336 | } | |
a715f6d4 | 337 | pq($afField)->attr('defn', CRM_Utils_JS::writeObject($field)); |
9384980c TO |
338 | } |
339 | } | |
340 | }); | |
341 | $angular->add($fieldMetadata); | |
342 | } | |
343 | ||
344 | function _afform_getMetadata(phpQueryObject $doc) { | |
345 | $entities = []; | |
c37151d2 | 346 | foreach ($doc->find('af-entity') as $afmModelProp) { |
8410442c | 347 | $entities[$afmModelProp->getAttribute('name')] = [ |
bbd6df21 | 348 | 'type' => $afmModelProp->getAttribute('type'), |
9384980c TO |
349 | ]; |
350 | } | |
351 | return $entities; | |
352 | } | |
353 | ||
66aa0f5e TO |
354 | /** |
355 | * Implements hook_civicrm_alterSettingsFolders(). | |
356 | * | |
357 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders | |
358 | */ | |
359 | function afform_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { | |
360 | _afform_civix_civicrm_alterSettingsFolders($metaDataFolders); | |
361 | } | |
362 | ||
363 | /** | |
364 | * Implements hook_civicrm_entityTypes(). | |
365 | * | |
366 | * Declare entity types provided by this module. | |
367 | * | |
368 | * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes | |
369 | */ | |
370 | function afform_civicrm_entityTypes(&$entityTypes) { | |
371 | _afform_civix_civicrm_entityTypes($entityTypes); | |
372 | } | |
373 | ||
98f4a7cb TO |
374 | /** |
375 | * Implements hook_civicrm_themes(). | |
376 | */ | |
377 | function afform_civicrm_themes(&$themes) { | |
378 | _afform_civix_civicrm_themes($themes); | |
379 | } | |
380 | ||
66aa0f5e TO |
381 | // --- Functions below this ship commented out. Uncomment as required. --- |
382 | ||
bb56ac78 TO |
383 | /** |
384 | * Implements hook_civicrm_buildAsset(). | |
385 | */ | |
386 | function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) { | |
387 | if ($asset !== 'afform.js') { | |
388 | return; | |
389 | } | |
390 | ||
391 | if (empty($params['name'])) { | |
392 | throw new RuntimeException("Missing required parameter: afform.js?name=NAME"); | |
393 | } | |
394 | ||
395 | $name = $params['name']; | |
854558d3 | 396 | /** @var \CRM_Afform_AfformScanner $scanner */ |
8f4a0ee9 | 397 | $scanner = Civi::service('afform_scanner'); |
f4017340 | 398 | $meta = $scanner->getMeta($name); |
7d2f28fe | 399 | |
bb56ac78 TO |
400 | $smarty = CRM_Core_Smarty::singleton(); |
401 | $smarty->assign('afform', [ | |
87dde5eb | 402 | 'camel' => _afform_angular_module_name($name, 'camel'), |
bb56ac78 | 403 | 'meta' => $meta, |
f4017340 | 404 | 'metaJson' => json_encode($meta), |
8bed1c84 | 405 | 'layout' => _afform_html_filter($name, $scanner->getLayout($name)), |
bb56ac78 TO |
406 | ]); |
407 | $mimeType = 'text/javascript'; | |
9ec944f2 | 408 | $content = $smarty->fetch('afform/AfformAngularModule.tpl'); |
bb56ac78 TO |
409 | } |
410 | ||
8bed1c84 TO |
411 | /** |
412 | * Apply any filters to an HTML partial. | |
413 | * | |
414 | * @param string $formName | |
415 | * @param string $html | |
416 | * Original HTML. | |
417 | * @return string | |
418 | * Modified HTML. | |
419 | */ | |
420 | function _afform_html_filter($formName, $html) { | |
421 | $fileName = '~afform/' . _afform_angular_module_name($formName, 'camel'); | |
422 | $htmls = [$fileName => $html]; | |
423 | $htmls = \Civi\Angular\ChangeSet::applyResourceFilters(Civi::service('angular')->getChangeSets(), 'partials', $htmls); | |
424 | return $htmls[$fileName]; | |
425 | } | |
426 | ||
8775c48a TO |
427 | /** |
428 | * Implements hook_civicrm_alterMenu(). | |
429 | */ | |
430 | function afform_civicrm_alterMenu(&$items) { | |
8f4a0ee9 TO |
431 | if (Civi::container()->has('afform_scanner')) { |
432 | $scanner = Civi::service('afform_scanner'); | |
433 | } | |
434 | else { | |
435 | // During installation... | |
436 | $scanner = new CRM_Afform_AfformScanner(); | |
437 | } | |
8775c48a TO |
438 | foreach ($scanner->getMetas() as $name => $meta) { |
439 | if (!empty($meta['server_route'])) { | |
440 | $items[$meta['server_route']] = [ | |
441 | 'page_callback' => 'CRM_Afform_Page_AfformBase', | |
442 | 'page_arguments' => 'afform=' . urlencode($name), | |
13bdd6d2 | 443 | 'title' => $meta['title'] ?? '', |
254f01f0 ML |
444 | 'access_arguments' => [['access CiviCRM'], 'and'], // FIXME |
445 | 'is_public' => $meta['is_public'], | |
8775c48a TO |
446 | ]; |
447 | } | |
448 | } | |
8775c48a TO |
449 | } |
450 | ||
74f862e4 TO |
451 | /** |
452 | * Clear any local/in-memory caches based on afform data. | |
453 | */ | |
454 | function _afform_clear() { | |
455 | $container = \Civi::container(); | |
456 | $container->get('afform_scanner')->clear(); | |
457 | ||
458 | // Civi\Angular\Manager doesn't currently have a way to clear its in-memory | |
459 | // data, so we just reset the whole object. | |
460 | $container->set('angular', NULL); | |
461 | } | |
462 | ||
bb56ac78 | 463 | /** |
87dde5eb TO |
464 | * @param string $fileBaseName |
465 | * Ex: foo-bar | |
841850b1 TO |
466 | * @param string $format |
467 | * 'camel' or 'dash'. | |
bb56ac78 | 468 | * @return string |
841850b1 | 469 | * Ex: 'FooBar' or 'foo-bar'. |
3cd5c38b | 470 | * @throws \Exception |
bb56ac78 | 471 | */ |
87dde5eb | 472 | function _afform_angular_module_name($fileBaseName, $format = 'camel') { |
841850b1 TO |
473 | switch ($format) { |
474 | case 'camel': | |
87dde5eb TO |
475 | $camelCase = ''; |
476 | foreach (explode('-', $fileBaseName) as $shortNamePart) { | |
477 | $camelCase .= ucfirst($shortNamePart); | |
478 | } | |
479 | return strtolower($camelCase{0}) . substr($camelCase, 1); | |
841850b1 TO |
480 | |
481 | case 'dash': | |
87dde5eb | 482 | return strtolower(implode('-', array_filter(preg_split('/(?=[A-Z])/', $fileBaseName)))); |
841850b1 TO |
483 | |
484 | default: | |
485 | throw new \Exception("Unrecognized format"); | |
486 | } | |
bb56ac78 | 487 | } |