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