Commit | Line | Data |
---|---|---|
16072ce1 TO |
1 | <?php |
2 | namespace Civi\Angular; | |
3 | ||
4 | /** | |
5 | * Manage Angular resources. | |
6 | * | |
7 | * @package Civi\Angular | |
8 | */ | |
9 | class Manager { | |
10 | ||
11 | /** | |
12 | * @var \CRM_Core_Resources | |
13 | */ | |
14 | protected $res = NULL; | |
15 | ||
16 | /** | |
cc101011 | 17 | * Modules. |
18 | * | |
19 | * @var array|null | |
16072ce1 TO |
20 | * Each item has some combination of these keys: |
21 | * - ext: string | |
8671b4f2 | 22 | * The Civi extension which defines the Angular module. |
16072ce1 | 23 | * - js: array(string $relativeFilePath) |
8671b4f2 | 24 | * List of JS files (relative to the extension). |
16072ce1 | 25 | * - css: array(string $relativeFilePath) |
8671b4f2 | 26 | * List of CSS files (relative to the extension). |
16072ce1 | 27 | * - partials: array(string $relativeFilePath) |
8671b4f2 TO |
28 | * A list of partial-HTML folders (relative to the extension). |
29 | * This will be mapped to "~/moduleName" by crmResource. | |
1da632e0 TO |
30 | * - settings: array(string $key => mixed $value) |
31 | * List of settings to preload. | |
aa882f9b CW |
32 | * - settingsFactory: callable |
33 | * Callback function to fetch settings. | |
66c46618 CW |
34 | * - permissions: array |
35 | * List of permissions to make available client-side | |
aa882f9b CW |
36 | * - requires: array |
37 | * List of other modules required | |
16072ce1 TO |
38 | */ |
39 | protected $modules = NULL; | |
40 | ||
53d00841 TO |
41 | /** |
42 | * @var \CRM_Utils_Cache_Interface | |
43 | */ | |
44 | protected $cache; | |
45 | ||
6dc348de TO |
46 | /** |
47 | * @var array | |
48 | * Array(string $name => ChangeSet $change). | |
49 | */ | |
50 | protected $changeSets = NULL; | |
51 | ||
16072ce1 TO |
52 | /** |
53 | * @param \CRM_Core_Resources $res | |
54 | * The resource manager. | |
34f3bbd9 | 55 | * @param $cache |
16072ce1 | 56 | */ |
53d00841 | 57 | public function __construct($res, \CRM_Utils_Cache_Interface $cache = NULL) { |
16072ce1 | 58 | $this->res = $res; |
c33f1df1 | 59 | $this->cache = $cache ? $cache : new \CRM_Utils_Cache_ArrayCache([]); |
16072ce1 TO |
60 | } |
61 | ||
76b9562a TO |
62 | /** |
63 | * Clear out any runtime-cached metadata. | |
64 | * | |
65 | * This is useful if, eg, you have recently added or destroyed Angular modules. | |
66 | * | |
67 | * @return static | |
68 | */ | |
69 | public function clear() { | |
70 | $this->cache->clear(); | |
71 | $this->modules = NULL; | |
72 | $this->changeSets = NULL; | |
a19c781a CW |
73 | // Force-refresh assetBuilder files |
74 | \Civi::container()->get('asset_builder')->clear(FALSE); | |
76b9562a TO |
75 | return $this; |
76 | } | |
77 | ||
16072ce1 | 78 | /** |
fe482240 | 79 | * Get a list of AngularJS modules which should be autoloaded. |
16072ce1 TO |
80 | * |
81 | * @return array | |
8671b4f2 TO |
82 | * Each item has some combination of these keys: |
83 | * - ext: string | |
84 | * The Civi extension which defines the Angular module. | |
85 | * - js: array(string $relativeFilePath) | |
86 | * List of JS files (relative to the extension). | |
87 | * - css: array(string $relativeFilePath) | |
88 | * List of CSS files (relative to the extension). | |
89 | * - partials: array(string $relativeFilePath) | |
90 | * A list of partial-HTML folders (relative to the extension). | |
91 | * This will be mapped to "~/moduleName" by crmResource. | |
1da632e0 TO |
92 | * - settings: array(string $key => mixed $value) |
93 | * List of settings to preload. | |
16072ce1 TO |
94 | */ |
95 | public function getModules() { | |
96 | if ($this->modules === NULL) { | |
1da632e0 | 97 | $config = \CRM_Core_Config::singleton(); |
8456e727 | 98 | global $civicrm_root; |
16072ce1 | 99 | |
8456e727 TO |
100 | // Note: It would be nice to just glob("$civicrm_root/ang/*.ang.php"), but at time |
101 | // of writing CiviMail and CiviCase have special conditionals. | |
16072ce1 | 102 | |
c64f69d9 | 103 | $angularModules = []; |
8456e727 | 104 | $angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.ang.php"; |
b9a4dfbd | 105 | $angularModules['checklist-model'] = include "$civicrm_root/ang/checklist-model.ang.php"; |
8456e727 TO |
106 | $angularModules['crmApp'] = include "$civicrm_root/ang/crmApp.ang.php"; |
107 | $angularModules['crmAttachment'] = include "$civicrm_root/ang/crmAttachment.ang.php"; | |
108 | $angularModules['crmAutosave'] = include "$civicrm_root/ang/crmAutosave.ang.php"; | |
109 | $angularModules['crmCxn'] = include "$civicrm_root/ang/crmCxn.ang.php"; | |
138b5733 | 110 | $angularModules['crmDialog'] = include "$civicrm_root/ang/crmDialog.ang.php"; |
9a78af90 | 111 | $angularModules['crmMonaco'] = include "$civicrm_root/ang/crmMonaco.ang.php"; |
8456e727 | 112 | $angularModules['crmResource'] = include "$civicrm_root/ang/crmResource.ang.php"; |
67341f3c | 113 | $angularModules['crmRouteBinder'] = include "$civicrm_root/ang/crmRouteBinder.ang.php"; |
8456e727 TO |
114 | $angularModules['crmUi'] = include "$civicrm_root/ang/crmUi.ang.php"; |
115 | $angularModules['crmUtil'] = include "$civicrm_root/ang/crmUtil.ang.php"; | |
116 | $angularModules['dialogService'] = include "$civicrm_root/ang/dialogService.ang.php"; | |
10d00207 | 117 | $angularModules['jsonFormatter'] = include "$civicrm_root/ang/jsonFormatter.ang.php"; |
8456e727 TO |
118 | $angularModules['ngRoute'] = include "$civicrm_root/ang/ngRoute.ang.php"; |
119 | $angularModules['ngSanitize'] = include "$civicrm_root/ang/ngSanitize.ang.php"; | |
120 | $angularModules['ui.utils'] = include "$civicrm_root/ang/ui.utils.ang.php"; | |
e88c001e | 121 | $angularModules['ui.bootstrap'] = include "$civicrm_root/ang/ui.bootstrap.ang.php"; |
8456e727 TO |
122 | $angularModules['ui.sortable'] = include "$civicrm_root/ang/ui.sortable.ang.php"; |
123 | $angularModules['unsavedChanges'] = include "$civicrm_root/ang/unsavedChanges.ang.php"; | |
cd6afc75 | 124 | $angularModules['crmStatusPage'] = include "$civicrm_root/ang/crmStatusPage.ang.php"; |
33044927 | 125 | $angularModules['exportui'] = include "$civicrm_root/ang/exportui.ang.php"; |
0b873c9a CW |
126 | $angularModules['api4Explorer'] = include "$civicrm_root/ang/api4Explorer.ang.php"; |
127 | $angularModules['api4'] = include "$civicrm_root/ang/api4.ang.php"; | |
f263929f | 128 | $angularModules['crmDashboard'] = include "$civicrm_root/ang/crmDashboard.ang.php"; |
bca4106f | 129 | $angularModules['crmD3'] = include "$civicrm_root/ang/crmD3.ang.php"; |
c0f7f681 | 130 | |
16072ce1 TO |
131 | foreach (\CRM_Core_Component::getEnabledComponents() as $component) { |
132 | $angularModules = array_merge($angularModules, $component->getAngularModules()); | |
133 | } | |
134 | \CRM_Utils_Hook::angularModules($angularModules); | |
aa882f9b CW |
135 | foreach ($angularModules as $module => $info) { |
136 | // Merge in defaults | |
137 | $angularModules[$module] += ['basePages' => ['civicrm/a']]; | |
138 | // Validate settingsFactory callables | |
139 | if (isset($info['settingsFactory'])) { | |
140 | // To keep the cache small, we want `settingsFactory` to contain the string names of class & function, not an object | |
141 | if (!is_array($info['settingsFactory']) && !is_string($info['settingsFactory'])) { | |
142 | throw new \CRM_Core_Exception($module . ' settingsFactory must be a callable array or string'); | |
143 | } | |
144 | // To keep the cache small, convert full object to just the class name | |
145 | if (is_array($info['settingsFactory']) && is_object($info['settingsFactory'][0])) { | |
146 | $angularModules[$module]['settingsFactory'][0] = get_class($info['settingsFactory'][0]); | |
147 | } | |
8da6c9b8 TO |
148 | } |
149 | } | |
16072ce1 TO |
150 | $this->modules = $this->resolvePatterns($angularModules); |
151 | } | |
152 | ||
153 | return $this->modules; | |
154 | } | |
155 | ||
156 | /** | |
157 | * Get the descriptor for an Angular module. | |
158 | * | |
159 | * @param string $name | |
160 | * Module name. | |
161 | * @return array | |
162 | * Details about the module: | |
163 | * - ext: string, the name of the Civi extension which defines the module | |
164 | * - js: array(string $relativeFilePath). | |
165 | * - css: array(string $relativeFilePath). | |
166 | * - partials: array(string $relativeFilePath). | |
167 | * @throws \Exception | |
168 | */ | |
169 | public function getModule($name) { | |
170 | $modules = $this->getModules(); | |
171 | if (!isset($modules[$name])) { | |
172 | throw new \Exception("Unrecognized Angular module"); | |
173 | } | |
174 | return $modules[$name]; | |
175 | } | |
176 | ||
8da6c9b8 TO |
177 | /** |
178 | * Resolve a full list of Angular dependencies. | |
179 | * | |
180 | * @param array $names | |
181 | * List of Angular modules. | |
182 | * Ex: array('crmMailing'). | |
183 | * @return array | |
184 | * List of Angular modules, include all dependencies. | |
185 | * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute'). | |
51feb18e | 186 | * @throws \CRM_Core_Exception |
8da6c9b8 TO |
187 | */ |
188 | public function resolveDependencies($names) { | |
189 | $allModules = $this->getModules(); | |
c64f69d9 | 190 | $visited = []; |
8da6c9b8 TO |
191 | $result = $names; |
192 | while (($missingModules = array_diff($result, array_keys($visited))) && !empty($missingModules)) { | |
193 | foreach ($missingModules as $module) { | |
194 | $visited[$module] = 1; | |
195 | if (!isset($allModules[$module])) { | |
51feb18e | 196 | throw new \CRM_Core_Exception("Unrecognized Angular module {$module}. Please ensure that all Angular modules are declared."); |
8da6c9b8 TO |
197 | } |
198 | elseif (isset($allModules[$module]['requires'])) { | |
199 | $result = array_unique(array_merge($result, $allModules[$module]['requires'])); | |
200 | } | |
201 | } | |
202 | } | |
203 | sort($result); | |
204 | return $result; | |
205 | } | |
206 | ||
207 | /** | |
208 | * Get a list of Angular modules that should be loaded on the given | |
209 | * base-page. | |
210 | * | |
211 | * @param string $basePage | |
212 | * The name of the base-page for which we want a list of moudles. | |
213 | * @return array | |
214 | * List of Angular modules. | |
215 | * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute'). | |
216 | */ | |
217 | public function resolveDefaultModules($basePage) { | |
218 | $modules = $this->getModules(); | |
c64f69d9 | 219 | $result = []; |
8da6c9b8 TO |
220 | foreach ($modules as $moduleName => $module) { |
221 | if (in_array($basePage, $module['basePages']) || in_array('*', $module['basePages'])) { | |
222 | $result[] = $moduleName; | |
223 | } | |
224 | } | |
225 | return $result; | |
226 | } | |
227 | ||
16072ce1 TO |
228 | /** |
229 | * Convert any globs in an Angular module to file names. | |
230 | * | |
231 | * @param array $modules | |
232 | * List of Angular modules. | |
233 | * @return array | |
234 | * Updated list of Angular modules | |
235 | */ | |
236 | protected function resolvePatterns($modules) { | |
c64f69d9 | 237 | $newModules = []; |
16072ce1 TO |
238 | |
239 | foreach ($modules as $moduleKey => $module) { | |
c64f69d9 | 240 | foreach (['js', 'css', 'partials'] as $fileset) { |
16072ce1 TO |
241 | if (!isset($module[$fileset])) { |
242 | continue; | |
243 | } | |
244 | $module[$fileset] = $this->res->glob($module['ext'], $module[$fileset]); | |
245 | } | |
246 | $newModules[$moduleKey] = $module; | |
247 | } | |
248 | ||
249 | return $newModules; | |
250 | } | |
251 | ||
252 | /** | |
6dc348de | 253 | * Get the partial HTML documents for a module (unfiltered). |
16072ce1 TO |
254 | * |
255 | * @param string $name | |
256 | * Angular module name. | |
257 | * @return array | |
258 | * Array(string $extFilePath => string $html) | |
a2dc0f82 TO |
259 | * @throws \Exception |
260 | * Invalid partials configuration. | |
16072ce1 | 261 | */ |
6dc348de | 262 | public function getRawPartials($name) { |
16072ce1 | 263 | $module = $this->getModule($name); |
90c62ad3 TO |
264 | $result = !empty($module['partialsCallback']) |
265 | ? \Civi\Core\Resolver::singleton()->call($module['partialsCallback'], [$name, $module]) | |
266 | : []; | |
16072ce1 | 267 | if (isset($module['partials'])) { |
a2dc0f82 TO |
268 | foreach ($module['partials'] as $partialDir) { |
269 | $partialDir = $this->res->getPath($module['ext']) . '/' . $partialDir; | |
270 | $files = \CRM_Utils_File::findFiles($partialDir, '*.html', TRUE); | |
271 | foreach ($files as $file) { | |
272 | $filename = '~/' . $name . '/' . $file; | |
273 | $result[$filename] = file_get_contents($partialDir . '/' . $file); | |
274 | } | |
16072ce1 | 275 | } |
6dc348de | 276 | return $result; |
16072ce1 TO |
277 | } |
278 | return $result; | |
279 | } | |
280 | ||
6dc348de TO |
281 | /** |
282 | * Get the partial HTML documents for a module. | |
283 | * | |
284 | * @param string $name | |
285 | * Angular module name. | |
286 | * @return array | |
287 | * Array(string $extFilePath => string $html) | |
288 | * @throws \Exception | |
289 | * Invalid partials configuration. | |
290 | */ | |
291 | public function getPartials($name) { | |
8b25b1fe | 292 | $cacheKey = "angular-partials_$name"; |
6dc348de TO |
293 | $cacheValue = $this->cache->get($cacheKey); |
294 | if ($cacheValue === NULL) { | |
295 | $cacheValue = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name)); | |
296 | $this->cache->set($cacheKey, $cacheValue); | |
297 | } | |
298 | return $cacheValue; | |
299 | } | |
300 | ||
16072ce1 TO |
301 | /** |
302 | * Get list of translated strings for a module. | |
303 | * | |
304 | * @param string $name | |
305 | * Angular module name. | |
306 | * @return array | |
307 | * Translated strings: array(string $orig => string $translated). | |
308 | */ | |
309 | public function getTranslatedStrings($name) { | |
e3d90d6c | 310 | $module = $this->getModule($name); |
c64f69d9 | 311 | $result = []; |
16072ce1 TO |
312 | $strings = $this->getStrings($name); |
313 | foreach ($strings as $string) { | |
314 | // TODO: should we pass translation domain based on $module[ext] or $module[tsDomain]? | |
315 | // It doesn't look like client side really supports the domain right now... | |
c64f69d9 CW |
316 | $translated = ts($string, [ |
317 | 'domain' => [$module['ext'], NULL], | |
318 | ]); | |
16072ce1 TO |
319 | if ($translated != $string) { |
320 | $result[$string] = $translated; | |
321 | } | |
322 | } | |
323 | return $result; | |
324 | } | |
325 | ||
326 | /** | |
327 | * Get list of translatable strings for a module. | |
328 | * | |
329 | * @param string $name | |
330 | * Angular module name. | |
331 | * @return array | |
332 | * Translatable strings. | |
333 | */ | |
334 | public function getStrings($name) { | |
335 | $module = $this->getModule($name); | |
c64f69d9 | 336 | $result = []; |
16072ce1 TO |
337 | if (isset($module['js'])) { |
338 | foreach ($module['js'] as $file) { | |
339 | $strings = $this->res->getStrings()->get( | |
340 | $module['ext'], | |
341 | $this->res->getPath($module['ext'], $file), | |
342 | 'text/javascript' | |
343 | ); | |
344 | $result = array_unique(array_merge($result, $strings)); | |
345 | } | |
346 | } | |
53d00841 TO |
347 | $partials = $this->getPartials($name); |
348 | foreach ($partials as $partial) { | |
349 | $result = array_unique(array_merge($result, \CRM_Utils_JS::parseStrings($partial))); | |
16072ce1 TO |
350 | } |
351 | return $result; | |
352 | } | |
353 | ||
354 | /** | |
27a90ef6 TO |
355 | * Get resources for one or more modules. |
356 | * | |
357 | * @param string|array $moduleNames | |
358 | * List of module names. | |
359 | * @param string $resType | |
1da632e0 | 360 | * Type of resource ('js', 'css', 'settings'). |
27a90ef6 | 361 | * @param string $refType |
1da632e0 | 362 | * Type of reference to the resource ('cacheUrl', 'rawUrl', 'path', 'settings'). |
16072ce1 | 363 | * @return array |
27a90ef6 TO |
364 | * List of URLs or paths. |
365 | * @throws \CRM_Core_Exception | |
16072ce1 | 366 | */ |
27a90ef6 | 367 | public function getResources($moduleNames, $resType, $refType) { |
c64f69d9 | 368 | $result = []; |
27a90ef6 TO |
369 | $moduleNames = (array) $moduleNames; |
370 | foreach ($moduleNames as $moduleName) { | |
371 | $module = $this->getModule($moduleName); | |
372 | if (isset($module[$resType])) { | |
373 | foreach ($module[$resType] as $file) { | |
e5c376e7 TO |
374 | $refTypeSuffix = ''; |
375 | if (is_string($file) && preg_match(';^(assetBuilder|ext)://;', $file)) { | |
376 | $refTypeSuffix = '-' . parse_url($file, PHP_URL_SCHEME); | |
377 | } | |
378 | ||
379 | switch ($refType . $refTypeSuffix) { | |
27a90ef6 TO |
380 | case 'path': |
381 | $result[] = $this->res->getPath($module['ext'], $file); | |
382 | break; | |
16072ce1 | 383 | |
27a90ef6 TO |
384 | case 'rawUrl': |
385 | $result[] = $this->res->getUrl($module['ext'], $file); | |
386 | break; | |
387 | ||
388 | case 'cacheUrl': | |
389 | $result[] = $this->res->getUrl($module['ext'], $file, TRUE); | |
390 | break; | |
391 | ||
e5c376e7 TO |
392 | case 'path-assetBuilder': |
393 | $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH); | |
c64f69d9 | 394 | $assetParams = []; |
e5c376e7 TO |
395 | parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams); |
396 | $result[] = \Civi::service('asset_builder')->getPath($assetName, $assetParams); | |
397 | break; | |
398 | ||
399 | case 'rawUrl-assetBuilder': | |
400 | case 'cacheUrl-assetBuilder': | |
401 | $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH); | |
c64f69d9 | 402 | $assetParams = []; |
e5c376e7 TO |
403 | parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams); |
404 | $result[] = \Civi::service('asset_builder')->getUrl($assetName, $assetParams); | |
405 | break; | |
406 | ||
407 | case 'path-ext': | |
408 | $result[] = $this->res->getPath(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/')); | |
409 | break; | |
410 | ||
411 | case 'rawUrl-ext': | |
412 | $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/')); | |
413 | break; | |
414 | ||
415 | case 'cacheUrl-ext': | |
416 | $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'), TRUE); | |
417 | break; | |
418 | ||
1da632e0 | 419 | case 'settings': |
aa882f9b | 420 | case 'settingsFactory': |
5438399c | 421 | case 'requires': |
66c46618 | 422 | case 'permissions': |
d67ff852 | 423 | case 'bundles': |
1da632e0 TO |
424 | if (!empty($module[$resType])) { |
425 | $result[$moduleName] = $module[$resType]; | |
426 | } | |
427 | break; | |
428 | ||
27a90ef6 TO |
429 | default: |
430 | throw new \CRM_Core_Exception("Unrecognized resource format"); | |
431 | } | |
432 | } | |
16072ce1 TO |
433 | } |
434 | } | |
6dc348de TO |
435 | |
436 | return ChangeSet::applyResourceFilters($this->getChangeSets(), $resType, $result); | |
437 | } | |
438 | ||
439 | /** | |
440 | * @return array | |
441 | * Array(string $name => ChangeSet $changeSet). | |
442 | */ | |
443 | public function getChangeSets() { | |
444 | if ($this->changeSets === NULL) { | |
c64f69d9 | 445 | $this->changeSets = []; |
6dc348de TO |
446 | \CRM_Utils_Hook::alterAngular($this); |
447 | } | |
448 | return $this->changeSets; | |
449 | } | |
450 | ||
451 | /** | |
452 | * @param ChangeSet $changeSet | |
453 | * @return \Civi\Angular\Manager | |
454 | */ | |
455 | public function add($changeSet) { | |
456 | $this->changeSets[$changeSet->getName()] = $changeSet; | |
457 | return $this; | |
16072ce1 | 458 | } |
8671b4f2 | 459 | |
16072ce1 | 460 | } |