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