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