Merge pull request #17133 from eileenmcnaughton/dep
[civicrm-core.git] / CRM / Core / Resources.php
CommitLineData
6a488035
TO
1<?php
2/*
bc77d7c0
TO
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
e70a7fc0 10 */
1889d803 11use Civi\Core\Event\GenericHookEvent;
6a488035
TO
12
13/**
14 * This class facilitates the loading of resources
15 * such as JavaScript files and CSS files.
16 *
17 * Any URLs generated for resources may include a 'cache-code'. By resetting the
18 * cache-code, one may force clients to re-download resource files (regardless of
19 * any HTTP caching rules).
20 *
21 * TODO: This is currently a thin wrapper over CRM_Core_Region. We
22 * should incorporte services for aggregation, minimization, etc.
23 *
24 * @package CRM
ca5cec67 25 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
26 * $Id$
27 *
28 */
29class CRM_Core_Resources {
30 const DEFAULT_WEIGHT = 0;
31 const DEFAULT_REGION = 'page-footer';
32
33 /**
34 * We don't have a container or dependency-injection, so use singleton instead
35 *
36 * @var object
6a488035
TO
37 */
38 private static $_singleton = NULL;
39
40 /**
41 * @var CRM_Extension_Mapper
42 */
43 private $extMapper = NULL;
44
45 /**
fd7dc3f3 46 * @var CRM_Core_Resources_Strings
6a488035 47 */
fd7dc3f3 48 private $strings = NULL;
6a488035
TO
49
50 /**
e97c66ff 51 * Settings in free-form data tree.
52 *
53 * @var array
6a488035 54 */
be2fb01f 55 protected $settings = [];
6a488035
TO
56
57 /**
e97c66ff 58 * Setting factories.
59 *
60 * @var callable[]
6a488035 61 */
be2fb01f 62 protected $settingsFactories = [];
6a488035
TO
63
64 /**
e97c66ff 65 * Added core resources.
66 *
67 * Format is ($regionName => bool).
68 *
69 * @var array
6a488035 70 */
be2fb01f 71 protected $addedCoreResources = [];
6a488035
TO
72
73 /**
e97c66ff 74 * Added core styles.
75 *
76 * Format is ($regionName => bool).
77 *
78 * @var array
6a488035 79 */
be2fb01f 80 protected $addedCoreStyles = [];
6a488035 81
2b2878a9
MW
82 /**
83 * Added settings.
84 *
85 * Format is ($regionName => bool).
86 *
87 * @var array
88 */
89 protected $addedSettings = [];
90
6a488035 91 /**
e97c66ff 92 * A value to append to JS/CSS URLs to coerce cache resets.
93 *
94 * @var string
6a488035
TO
95 */
96 protected $cacheCode = NULL;
97
98 /**
e97c66ff 99 * The name of a setting which persistently stores the cacheCode.
100 *
101 * @var string
6a488035
TO
102 */
103 protected $cacheCodeKey = NULL;
104
53f2643c 105 /**
e97c66ff 106 * Are ajax popup screens enabled.
107 *
53f2643c
CW
108 * @var bool
109 */
110 public $ajaxPopupsEnabled;
111
b698e2d5
TO
112 /**
113 * @var \Civi\Core\Paths
114 */
115 protected $paths;
116
6a488035 117 /**
fe482240 118 * Get or set the single instance of CRM_Core_Resources.
6a488035 119 *
5a4f6742
CW
120 * @param CRM_Core_Resources $instance
121 * New copy of the manager.
e97c66ff 122 *
6a488035
TO
123 * @return CRM_Core_Resources
124 */
518fa0ee 125 public static function singleton(CRM_Core_Resources $instance = NULL) {
6a488035
TO
126 if ($instance !== NULL) {
127 self::$_singleton = $instance;
128 }
129 if (self::$_singleton === NULL) {
223ba025 130 self::$_singleton = Civi::service('resources');
6a488035
TO
131 }
132 return self::$_singleton;
133 }
134
135 /**
d09edf64 136 * Construct a resource manager.
6a488035 137 *
6a0b768e
TO
138 * @param CRM_Extension_Mapper $extMapper
139 * Map extension names to their base path or URLs.
140 * @param CRM_Utils_Cache_Interface $cache
141 * JS-localization cache.
3be754d6 142 * @param string|null $cacheCodeKey Random code to append to resource URLs; changing the code forces clients to reload resources
6a488035
TO
143 */
144 public function __construct($extMapper, $cache, $cacheCodeKey = NULL) {
145 $this->extMapper = $extMapper;
fd7dc3f3 146 $this->strings = new CRM_Core_Resources_Strings($cache);
6a488035
TO
147 $this->cacheCodeKey = $cacheCodeKey;
148 if ($cacheCodeKey !== NULL) {
aaffa79f 149 $this->cacheCode = Civi::settings()->get($cacheCodeKey);
6a488035 150 }
150f50c1 151 if (!$this->cacheCode) {
6a488035
TO
152 $this->resetCacheCode();
153 }
84fb7424 154 $this->ajaxPopupsEnabled = (bool) Civi::settings()->get('ajaxPopupsEnabled');
b698e2d5 155 $this->paths = Civi::paths();
6a488035
TO
156 }
157
90efc417
TO
158 /**
159 * Export permission data to the client to enable smarter GUIs.
160 *
161 * Note: Application security stems from the server's enforcement
162 * of the security logic (e.g. in the API permissions). There's no way
163 * the client can use this info to make the app more secure; however,
164 * it can produce a better-tuned (non-broken) UI.
165 *
166 * @param array $permNames
167 * List of permission names to check/export.
168 * @return CRM_Core_Resources
169 */
170 public function addPermissions($permNames) {
171 $permNames = (array) $permNames;
be2fb01f 172 $perms = [];
90efc417
TO
173 foreach ($permNames as $permName) {
174 $perms[$permName] = CRM_Core_Permission::check($permName);
175 }
be2fb01f 176 return $this->addSetting([
90efc417 177 'permissions' => $perms,
be2fb01f 178 ]);
90efc417
TO
179 }
180
6a488035
TO
181 /**
182 * Add a JavaScript file to the current page using <SCRIPT SRC>.
183 *
5a4f6742
CW
184 * @param string $ext
185 * extension name; use 'civicrm' for core.
186 * @param string $file
187 * file path -- relative to the extension base dir.
188 * @param int $weight
189 * relative weight within a given region.
190 * @param string $region
191 * location within the file; 'html-header', 'page-header', 'page-footer'.
3f3bba82
TO
192 * @param bool|string $translate
193 * Whether to load translated strings for this file. Use one of:
194 * - FALSE: Do not load translated strings.
195 * - TRUE: Load translated strings. Use the $ext's default domain.
196 * - string: Load translated strings. Use a specific domain.
6a488035
TO
197 *
198 * @return CRM_Core_Resources
89bfc54a 199 *
200 * @throws \CRM_Core_Exception
6a488035
TO
201 */
202 public function addScriptFile($ext, $file, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION, $translate = TRUE) {
203 if ($translate) {
3f3bba82
TO
204 $domain = ($translate === TRUE) ? $ext : $translate;
205 $this->addString($this->strings->get($domain, $this->getPath($ext, $file), 'text/javascript'), $domain);
6a488035 206 }
d89d2545
TO
207 $url = $this->getUrl($ext, $this->filterMinify($ext, $file), TRUE);
208 return $this->addScriptUrl($url, $weight, $region);
6a488035
TO
209 }
210
211 /**
212 * Add a JavaScript file to the current page using <SCRIPT SRC>.
213 *
5a4f6742
CW
214 * @param string $url
215 * @param int $weight
216 * relative weight within a given region.
217 * @param string $region
218 * location within the file; 'html-header', 'page-header', 'page-footer'.
6a488035
TO
219 * @return CRM_Core_Resources
220 */
221 public function addScriptUrl($url, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
be2fb01f 222 CRM_Core_Region::instance($region)->add([
518fa0ee
SL
223 'name' => $url,
224 'type' => 'scriptUrl',
225 'scriptUrl' => $url,
226 'weight' => $weight,
227 'region' => $region,
228 ]);
6a488035
TO
229 return $this;
230 }
231
232 /**
233 * Add a JavaScript file to the current page using <SCRIPT SRC>.
234 *
5a4f6742
CW
235 * @param string $code
236 * JavaScript source code.
237 * @param int $weight
238 * relative weight within a given region.
239 * @param string $region
240 * location within the file; 'html-header', 'page-header', 'page-footer'.
6a488035
TO
241 * @return CRM_Core_Resources
242 */
243 public function addScript($code, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
be2fb01f 244 CRM_Core_Region::instance($region)->add([
8d7a9d07 245 // 'name' => automatic
518fa0ee
SL
246 'type' => 'script',
247 'script' => $code,
248 'weight' => $weight,
249 'region' => $region,
250 ]);
6a488035
TO
251 return $this;
252 }
253
254 /**
73bcd446 255 * Add JavaScript variables to CRM.vars
6a488035 256 *
132fc68e
CW
257 * Example:
258 * From the server:
73bcd446
CW
259 * CRM_Core_Resources::singleton()->addVars('myNamespace', array('foo' => 'bar'));
260 * Access var from javascript:
261 * CRM.vars.myNamespace.foo // "bar"
132fc68e 262 *
01478033 263 * @see http://wiki.civicrm.org/confluence/display/CRMDOC/Javascript+Reference
132fc68e 264 *
6a0b768e
TO
265 * @param string $nameSpace
266 * Usually the name of your extension.
73bcd446 267 * @param array $vars
2b2878a9
MW
268 * @param string $region
269 * The region to add settings to (eg. for payment processors usually billing-block)
270 *
73bcd446
CW
271 * @return CRM_Core_Resources
272 */
2b2878a9 273 public function addVars($nameSpace, $vars, $region = NULL) {
be2fb01f 274 $existing = CRM_Utils_Array::value($nameSpace, CRM_Utils_Array::value('vars', $this->settings), []);
73bcd446 275 $vars = $this->mergeSettings($existing, $vars);
2b2878a9 276 $this->addSetting(['vars' => [$nameSpace => $vars]], $region);
73bcd446
CW
277 return $this;
278 }
279
280 /**
281 * Add JavaScript variables to the root of the CRM object.
282 * This function is usually reserved for low-level system use.
283 * Extensions and components should generally use addVars instead.
284 *
5a4f6742 285 * @param array $settings
2b2878a9
MW
286 * @param string $region
287 * The region to add settings to (eg. for payment processors usually billing-block)
288 *
6a488035
TO
289 * @return CRM_Core_Resources
290 */
2b2878a9
MW
291 public function addSetting($settings, $region = NULL) {
292 if (!$region) {
88cd8875 293 $region = self::isAjaxMode() ? 'ajax-snippet' : 'html-header';
6a488035 294 }
2b2878a9
MW
295 $this->settings = $this->mergeSettings($this->settings, $settings);
296 if (isset($this->addedSettings[$region])) {
297 return $this;
298 }
299 $resources = $this;
300 $settingsResource = [
301 'callback' => function (&$snippet, &$html) use ($resources, $region) {
302 $html .= "\n" . $resources->renderSetting($region);
303 },
304 'weight' => -100000,
305 ];
306 CRM_Core_Region::instance($region)->add($settingsResource);
307 $this->addedSettings[$region] = TRUE;
6a488035
TO
308 return $this;
309 }
310
311 /**
69847402 312 * Add JavaScript variables to the global CRM object via a callback function.
6a488035 313 *
3be754d6 314 * @param callable $callable
6a488035
TO
315 * @return CRM_Core_Resources
316 */
317 public function addSettingsFactory($callable) {
d937dbcd 318 // Make sure our callback has been registered
be2fb01f 319 $this->addSetting([]);
6a488035
TO
320 $this->settingsFactories[] = $callable;
321 return $this;
322 }
323
69847402 324 /**
d09edf64 325 * Helper fn for addSettingsFactory.
69847402 326 */
6a488035
TO
327 public function getSettings() {
328 $result = $this->settings;
329 foreach ($this->settingsFactories as $callable) {
330 $result = $this->mergeSettings($result, $callable());
331 }
898951c6 332 CRM_Utils_Hook::alterResourceSettings($result);
6a488035
TO
333 return $result;
334 }
335
336 /**
337 * @param array $settings
338 * @param array $additions
a6c01b45
CW
339 * @return array
340 * combination of $settings and $additions
6a488035
TO
341 */
342 protected function mergeSettings($settings, $additions) {
343 foreach ($additions as $k => $v) {
344 if (isset($settings[$k]) && is_array($settings[$k]) && is_array($v)) {
345 $v += $settings[$k];
346 }
347 $settings[$k] = $v;
348 }
349 return $settings;
350 }
351
352 /**
d09edf64 353 * Helper fn for addSetting.
6a488035
TO
354 * Render JavaScript variables for the global CRM object.
355 *
6a488035
TO
356 * @return string
357 */
2b2878a9 358 public function renderSetting($region = NULL) {
156fd9b9 359 // On a standard page request we construct the CRM object from scratch
2b2878a9 360 if (($region === 'html-header') || !self::isAjaxMode()) {
156fd9b9
CW
361 $js = 'var CRM = ' . json_encode($this->getSettings()) . ';';
362 }
363 // For an ajax request we append to it
364 else {
365 $js = 'CRM.$.extend(true, CRM, ' . json_encode($this->getSettings()) . ');';
366 }
d759bd10 367 return sprintf("<script type=\"text/javascript\">\n%s\n</script>\n", $js);
6a488035
TO
368 }
369
370 /**
371 * Add translated string to the js CRM object.
372 * It can then be retrived from the client-side ts() function
373 * Variable substitutions can happen from client-side
8ef12e64 374 *
6a488035 375 * Note: this function rarely needs to be called directly and is mostly for internal use.
d3e86119 376 * See CRM_Core_Resources::addScriptFile which automatically adds translated strings from js files
6a488035
TO
377 *
378 * Simple example:
379 * // From php:
380 * CRM_Core_Resources::singleton()->addString('Hello');
381 * // The string is now available to javascript code i.e.
382 * ts('Hello');
383 *
384 * Example with client-side substitutions:
385 * // From php:
386 * CRM_Core_Resources::singleton()->addString('Your %1 has been %2');
387 * // ts() in javascript works the same as in php, for example:
388 * ts('Your %1 has been %2', {1: objectName, 2: actionTaken});
389 *
390 * NOTE: This function does not work with server-side substitutions
391 * (as this might result in collisions and unwanted variable injections)
392 * Instead, use code like:
393 * CRM_Core_Resources::singleton()->addSetting(array('myNamespace' => array('myString' => ts('Your %1 has been %2', array(subs)))));
394 * And from javascript access it at CRM.myNamespace.myString
395 *
5a4f6742 396 * @param string|array $text
e97c66ff 397 * @param string|null $domain
6a488035
TO
398 * @return CRM_Core_Resources
399 */
3f3bba82 400 public function addString($text, $domain = 'civicrm') {
6a488035 401 foreach ((array) $text as $str) {
be2fb01f
CW
402 $translated = ts($str, [
403 'domain' => ($domain == 'civicrm') ? NULL : [$domain, NULL],
1b4710da 404 'raw' => TRUE,
be2fb01f 405 ]);
3f3bba82 406
6a488035
TO
407 // We only need to push this string to client if the translation
408 // is actually different from the original
409 if ($translated != $str) {
3f3bba82 410 $bucket = $domain == 'civicrm' ? 'strings' : 'strings::' . $domain;
be2fb01f
CW
411 $this->addSetting([
412 $bucket => [$str => $translated],
413 ]);
6a488035
TO
414 }
415 }
416 return $this;
417 }
418
419 /**
420 * Add a CSS file to the current page using <LINK HREF>.
421 *
5a4f6742
CW
422 * @param string $ext
423 * extension name; use 'civicrm' for core.
424 * @param string $file
425 * file path -- relative to the extension base dir.
426 * @param int $weight
427 * relative weight within a given region.
428 * @param string $region
429 * location within the file; 'html-header', 'page-header', 'page-footer'.
6a488035
TO
430 * @return CRM_Core_Resources
431 */
432 public function addStyleFile($ext, $file, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
d89d2545
TO
433 /** @var Civi\Core\Themes $theme */
434 $theme = Civi::service('themes');
435 foreach ($theme->resolveUrls($theme->getActiveThemeKey(), $ext, $file) as $url) {
436 $this->addStyleUrl($url, $weight, $region);
437 }
438 return $this;
6a488035
TO
439 }
440
441 /**
442 * Add a CSS file to the current page using <LINK HREF>.
443 *
5a4f6742
CW
444 * @param string $url
445 * @param int $weight
446 * relative weight within a given region.
447 * @param string $region
448 * location within the file; 'html-header', 'page-header', 'page-footer'.
6a488035
TO
449 * @return CRM_Core_Resources
450 */
451 public function addStyleUrl($url, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
be2fb01f 452 CRM_Core_Region::instance($region)->add([
518fa0ee
SL
453 'name' => $url,
454 'type' => 'styleUrl',
455 'styleUrl' => $url,
456 'weight' => $weight,
457 'region' => $region,
458 ]);
6a488035
TO
459 return $this;
460 }
461
462 /**
463 * Add a CSS content to the current page using <STYLE>.
464 *
5a4f6742
CW
465 * @param string $code
466 * CSS source code.
467 * @param int $weight
468 * relative weight within a given region.
469 * @param string $region
470 * location within the file; 'html-header', 'page-header', 'page-footer'.
6a488035
TO
471 * @return CRM_Core_Resources
472 */
473 public function addStyle($code, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
be2fb01f 474 CRM_Core_Region::instance($region)->add([
8d7a9d07 475 // 'name' => automatic
518fa0ee
SL
476 'type' => 'style',
477 'style' => $code,
478 'weight' => $weight,
479 'region' => $region,
480 ]);
6a488035
TO
481 return $this;
482 }
483
484 /**
d09edf64 485 * Determine file path of a resource provided by an extension.
6a488035 486 *
5a4f6742
CW
487 * @param string $ext
488 * extension name; use 'civicrm' for core.
e97c66ff 489 * @param string|null $file
5a4f6742 490 * file path -- relative to the extension base dir.
6a488035 491 *
72b3a70c
CW
492 * @return bool|string
493 * full file path or FALSE if not found
6a488035 494 */
16cd1eca 495 public function getPath($ext, $file = NULL) {
6a488035 496 // TODO consider caching results
b698e2d5
TO
497 $base = $this->paths->hasVariable($ext)
498 ? rtrim($this->paths->getVariable($ext, 'path'), '/')
499 : $this->extMapper->keyToBasePath($ext);
16cd1eca 500 if ($file === NULL) {
b698e2d5 501 return $base;
16cd1eca 502 }
b698e2d5 503 $path = $base . '/' . $file;
6a488035
TO
504 if (is_file($path)) {
505 return $path;
506 }
507 return FALSE;
508 }
509
510 /**
d09edf64 511 * Determine public URL of a resource provided by an extension.
6a488035 512 *
5a4f6742
CW
513 * @param string $ext
514 * extension name; use 'civicrm' for core.
515 * @param string $file
516 * file path -- relative to the extension base dir.
2a6da8d7
EM
517 * @param bool $addCacheCode
518 *
6a488035
TO
519 * @return string, URL
520 */
521 public function getUrl($ext, $file = NULL, $addCacheCode = FALSE) {
522 if ($file === NULL) {
523 $file = '';
524 }
525 if ($addCacheCode) {
6f12c6eb 526 $file = $this->addCacheCode($file);
6a488035
TO
527 }
528 // TODO consider caching results
b698e2d5
TO
529 $base = $this->paths->hasVariable($ext)
530 ? $this->paths->getVariable($ext, 'url')
531 : ($this->extMapper->keyToUrl($ext) . '/');
532 return $base . $file;
6a488035
TO
533 }
534
16cd1eca
TO
535 /**
536 * Evaluate a glob pattern in the context of a particular extension.
537 *
538 * @param string $ext
539 * Extension name; use 'civicrm' for core.
540 * @param string|array $patterns
541 * Glob pattern; e.g. "*.html".
542 * @param null|int $flags
543 * See glob().
544 * @return array
545 * List of matching files, relative to the extension base dir.
546 * @see glob()
547 */
548 public function glob($ext, $patterns, $flags = NULL) {
549 $path = $this->getPath($ext);
550 $patterns = (array) $patterns;
be2fb01f 551 $files = [];
16cd1eca 552 foreach ($patterns as $pattern) {
e5c376e7
TO
553 if (preg_match(';^(assetBuilder|ext)://;', $pattern)) {
554 $files[] = $pattern;
555 }
9f87b14b 556 if (CRM_Utils_File::isAbsolute($pattern)) {
16cd1eca
TO
557 // Absolute path.
558 $files = array_merge($files, (array) glob($pattern, $flags));
559 }
560 else {
561 // Relative path.
562 $files = array_merge($files, (array) glob("$path/$pattern", $flags));
563 }
564 }
518fa0ee
SL
565 // Deterministic order.
566 sort($files);
16cd1eca
TO
567 $files = array_unique($files);
568 return array_map(function ($file) use ($path) {
569 return CRM_Utils_File::relativize($file, "$path/");
570 }, $files);
571 }
572
a0ee3941
EM
573 /**
574 * @return string
575 */
6a488035
TO
576 public function getCacheCode() {
577 return $this->cacheCode;
578 }
579
a0ee3941
EM
580 /**
581 * @param $value
5badddc3 582 * @return CRM_Core_Resources
a0ee3941 583 */
6a488035
TO
584 public function setCacheCode($value) {
585 $this->cacheCode = $value;
586 if ($this->cacheCodeKey) {
08ef4ddd 587 Civi::settings()->set($this->cacheCodeKey, $value);
6a488035 588 }
9762f6ff 589 return $this;
6a488035
TO
590 }
591
5badddc3
CW
592 /**
593 * @return CRM_Core_Resources
594 */
6a488035
TO
595 public function resetCacheCode() {
596 $this->setCacheCode(CRM_Utils_String::createRandom(5, CRM_Utils_String::ALPHANUMERIC));
f091327b
CW
597 // Also flush cms resource cache if needed
598 CRM_Core_Config::singleton()->userSystem->clearResourceCache();
9762f6ff 599 return $this;
6a488035
TO
600 }
601
602 /**
603 * This adds CiviCRM's standard css and js to the specified region of the document.
604 * It will only run once.
605 *
606 * TODO: Separate the functional code (like addStyle/addScript) from the policy code
607 * (like addCoreResources/addCoreStyles).
608 *
2a6da8d7 609 * @param string $region
6a488035 610 * @return CRM_Core_Resources
6a488035
TO
611 */
612 public function addCoreResources($region = 'html-header') {
156fd9b9 613 if (!isset($this->addedCoreResources[$region]) && !self::isAjaxMode()) {
6a488035
TO
614 $this->addedCoreResources[$region] = TRUE;
615 $config = CRM_Core_Config::singleton();
616
a43b1583 617 // Add resources from coreResourceList
6a488035 618 $jsWeight = -9999;
dbb12634 619 foreach ($this->coreResourceList($region) as $item) {
7266e09b
CW
620 if (is_array($item)) {
621 $this->addSetting($item);
622 }
adcd4bf7
CW
623 elseif (strpos($item, '.css')) {
624 $this->isFullyFormedUrl($item) ? $this->addStyleUrl($item, -100, $region) : $this->addStyleFile('civicrm', $item, -100, $region);
625 }
626 elseif ($this->isFullyFormedUrl($item)) {
627 $this->addScriptUrl($item, $jsWeight++, $region);
628 }
629 else {
74227f77 630 // Don't bother looking for ts() calls in packages, there aren't any
7266e09b
CW
631 $translate = (substr($item, 0, 3) == 'js/');
632 $this->addScriptFile('civicrm', $item, $jsWeight++, $region, $translate);
6a488035 633 }
6a488035 634 }
d759bd10 635 // Add global settings
be2fb01f
CW
636 $settings = [
637 'config' => [
353ffa53 638 'isFrontend' => $config->userFrameworkFrontend,
be2fb01f
CW
639 ],
640 ];
4e56e743
CW
641 // Disable profile creation if user lacks permission
642 if (!CRM_Core_Permission::check('edit all contacts') && !CRM_Core_Permission::check('add contacts')) {
b7ceb253 643 $settings['config']['entityRef']['contactCreate'] = FALSE;
3d527838 644 }
4e56e743 645 $this->addSetting($settings);
3d527838
CW
646
647 // Give control of jQuery and _ back to the CMS - this loads last
c2777586 648 $this->addScriptFile('civicrm', 'js/noconflict.js', 9999, $region, FALSE);
6a488035
TO
649
650 $this->addCoreStyles($region);
651 }
652 return $this;
653 }
654
655 /**
656 * This will add CiviCRM's standard CSS
657 *
658 * TODO: Separate the functional code (like addStyle/addScript) from the policy code
659 * (like addCoreResources/addCoreStyles).
660 *
661 * @param string $region
662 * @return CRM_Core_Resources
663 */
664 public function addCoreStyles($region = 'html-header') {
665 if (!isset($this->addedCoreStyles[$region])) {
666 $this->addedCoreStyles[$region] = TRUE;
667
668 // Load custom or core css
669 $config = CRM_Core_Config::singleton();
670 if (!empty($config->customCSSURL)) {
6f12c6eb 671 $customCSSURL = $this->addCacheCode($config->customCSSURL);
672 $this->addStyleUrl($customCSSURL, 99, $region);
6a488035 673 }
aaffa79f 674 if (!Civi::settings()->get('disable_core_css')) {
6a488035 675 $this->addStyleFile('civicrm', 'css/civicrm.css', -99, $region);
6a488035 676 }
a2c70872
AH
677 // crm-i.css added ahead of other styles so it can be overridden by FA.
678 $this->addStyleFile('civicrm', 'css/crm-i.css', -101, $region);
6a488035
TO
679 }
680 return $this;
681 }
682
627668e8 683 /**
d09edf64 684 * Flushes cached translated strings.
5badddc3 685 * @return CRM_Core_Resources
627668e8
CW
686 */
687 public function flushStrings() {
fd7dc3f3 688 $this->strings->flush();
9762f6ff
CW
689 return $this;
690 }
691
6a488035 692 /**
fd7dc3f3
TO
693 * @return CRM_Core_Resources_Strings
694 */
695 public function getStrings() {
696 return $this->strings;
6a488035
TO
697 }
698
19f7e35e 699 /**
8d7a9d07 700 * Create dynamic script for localizing js widgets.
19f7e35e 701 */
00be9182 702 public static function outputLocalizationJS() {
4cc9b813 703 CRM_Core_Page_AJAX::setJsHeaders();
a88cf11a 704 $config = CRM_Core_Config::singleton();
be2fb01f 705 $vars = [
3d527838
CW
706 'moneyFormat' => json_encode(CRM_Utils_Money::format(1234.56)),
707 'contactSearch' => json_encode($config->includeEmailInName ? ts('Start typing a name or email...') : ts('Start typing a name...')),
708 'otherSearch' => json_encode(ts('Enter search term...')),
e695ee7c 709 'entityRef' => self::getEntityRefMetadata(),
7b83e312 710 'ajaxPopupsEnabled' => self::singleton()->ajaxPopupsEnabled,
a9fb6123 711 'allowAlertAutodismissal' => (bool) Civi::settings()->get('allow_alert_autodismissal'),
c7e39a79 712 'resourceCacheCode' => self::singleton()->getCacheCode(),
b30809e4
CW
713 'locale' => CRM_Core_I18n::getLocale(),
714 'cid' => (int) CRM_Core_Session::getLoggedInContactID(),
be2fb01f 715 ];
3d4fb0ed 716 print CRM_Core_Smarty::singleton()->fetchWith('CRM/common/l10n.js.tpl', $vars);
4cc9b813 717 CRM_Utils_System::civiExit();
c66581f5
CW
718 }
719
6a488035 720 /**
d09edf64 721 * List of core resources we add to every CiviCRM page.
6a488035 722 *
e8038a7b
CW
723 * Note: non-compressed versions of .min files will be used in debug mode
724 *
e3c1e85b 725 * @param string $region
a43b1583 726 * @return array
6a488035 727 */
e3c1e85b 728 public function coreResourceList($region) {
dbe0bbc6 729 $config = CRM_Core_Config::singleton();
a43b1583 730
4968e732
CW
731 // Scripts needed by everyone, everywhere
732 // FIXME: This is too long; list needs finer-grained segmentation
be2fb01f 733 $items = [
3c61fc8f
CW
734 "bower_components/jquery/dist/jquery.min.js",
735 "bower_components/jquery-ui/jquery-ui.min.js",
e8038a7b 736 "bower_components/jquery-ui/themes/smoothness/jquery-ui.min.css",
1891a856 737 "bower_components/lodash-compat/lodash.min.js",
e8038a7b
CW
738 "packages/jquery/plugins/jquery.mousewheel.min.js",
739 "bower_components/select2/select2.min.js",
740 "bower_components/select2/select2.min.css",
90000c30 741 "bower_components/font-awesome/css/font-awesome.min.css",
e8038a7b
CW
742 "packages/jquery/plugins/jquery.form.min.js",
743 "packages/jquery/plugins/jquery.timeentry.min.js",
744 "packages/jquery/plugins/jquery.blockUI.min.js",
745 "bower_components/datatables/media/js/jquery.dataTables.min.js",
746 "bower_components/datatables/media/css/jquery.dataTables.min.css",
747 "bower_components/jquery-validation/dist/jquery.validate.min.js",
748 "packages/jquery/plugins/jquery.ui.datepicker.validation.min.js",
a0c8b50e 749 "js/Common.js",
ff2eb9e8 750 "js/crm.datepicker.js",
53f2643c 751 "js/crm.ajax.js",
7060d177 752 "js/wysiwyg/crm.wysiwyg.js",
be2fb01f 753 ];
adcd4bf7
CW
754
755 // Dynamic localization script
756 $items[] = $this->addCacheCode(
757 CRM_Utils_System::url('civicrm/ajax/l10n-js/' . CRM_Core_I18n::getLocale(),
758 ['cid' => CRM_Core_Session::getLoggedInContactID()], FALSE, NULL, FALSE)
759 );
760
7c523661 761 // add wysiwyg editor
aaffa79f 762 $editor = Civi::settings()->get('editor_id');
b608cfb1 763 if ($editor == "CKEditor") {
5e0b4b77 764 CRM_Admin_Page_CKEditorConfig::setConfigDefault();
be2fb01f
CW
765 $items[] = [
766 'config' => [
286a7e5a
CW
767 'wysisygScriptLocation' => Civi::paths()->getUrl("[civicrm.root]/js/wysiwyg/crm.ckeditor.js"),
768 'CKEditorCustomConfig' => CRM_Admin_Page_CKEditorConfig::getConfigUrl(),
be2fb01f
CW
769 ],
770 ];
b608cfb1 771 }
dbe0bbc6 772
4968e732
CW
773 // These scripts are only needed by back-office users
774 if (CRM_Core_Permission::check('access CiviCRM')) {
642d43fa 775 $items[] = "packages/jquery/plugins/jquery.tableHeader.js";
e8038a7b 776 $items[] = "packages/jquery/plugins/jquery.notify.min.js";
4968e732
CW
777 }
778
b30809e4
CW
779 $contactID = CRM_Core_Session::getLoggedInContactID();
780
781 // Menubar
c931044d
CW
782 $position = 'none';
783 if (
784 $contactID && !$config->userFrameworkFrontend
785 && CRM_Core_Permission::check('access CiviCRM')
786 && !@constant('CIVICRM_DISABLE_DEFAULT_MENU')
787 && !CRM_Core_Config::isUpgradeMode()
788 ) {
789 $position = Civi::settings()->get('menubar_position') ?: 'over-cms-menu';
790 }
791 if ($position !== 'none') {
b30809e4
CW
792 $items[] = 'bower_components/smartmenus/dist/jquery.smartmenus.min.js';
793 $items[] = 'bower_components/smartmenus/dist/addons/keyboard/jquery.smartmenus.keyboard.min.js';
794 $items[] = 'js/crm.menubar.js';
dcaf410f 795 // @see CRM_Core_Resources::renderMenubarStylesheet
08a3a519 796 $items[] = Civi::service('asset_builder')->getUrl('crm-menubar.css', [
dcaf410f 797 'menubarColor' => Civi::settings()->get('menubar_color'),
c4560ed2
CW
798 'height' => 40,
799 'breakpoint' => 768,
08a3a519 800 ]);
dcaf410f 801 // Variables for crm.menubar.js
b30809e4
CW
802 $items[] = [
803 'menubar' => [
c931044d 804 'position' => $position,
b30809e4
CW
805 'qfKey' => CRM_Core_Key::get('CRM_Contact_Controller_Search', TRUE),
806 'cacheCode' => CRM_Core_BAO_Navigation::getCacheKey($contactID),
807 ],
808 ];
809 }
810
d606fff7
CW
811 // JS for multilingual installations
812 if (!empty($config->languageLimit) && count($config->languageLimit) > 1 && CRM_Core_Permission::check('translate CiviCRM')) {
813 $items[] = "js/crm.multilingual.js";
814 }
815
53f2643c
CW
816 // Enable administrators to edit option lists in a dialog
817 if (CRM_Core_Permission::check('administer CiviCRM') && $this->ajaxPopupsEnabled) {
818 $items[] = "js/crm.optionEdit.js";
819 }
9efeb642 820
98466ff9 821 $tsLocale = CRM_Core_I18n::getLocale();
dbe0bbc6 822 // Add localized jQuery UI files
5d26edf0 823 if ($tsLocale && $tsLocale != 'en_US') {
dbe0bbc6 824 // Search for i18n file in order of specificity (try fr-CA, then fr)
5d26edf0 825 list($lang) = explode('_', $tsLocale);
3c61fc8f 826 $path = "bower_components/jquery-ui/ui/i18n";
be2fb01f 827 foreach ([str_replace('_', '-', $tsLocale), $lang] as $language) {
f2f191fe 828 $localizationFile = "$path/datepicker-{$language}.js";
dbe0bbc6
CW
829 if ($this->getPath('civicrm', $localizationFile)) {
830 $items[] = $localizationFile;
831 break;
832 }
833 }
834 }
f9f361d0 835
72e86d7d 836 // Allow hooks to modify this list
e3c1e85b 837 CRM_Utils_Hook::coreResourceList($items, $region);
f9f361d0 838
1143a781
TO
839 // Oof, existing listeners would expect $items to typically begin with 'bower_components/' or 'packages/'
840 // (using an implicit base of `[civicrm.root]`). We preserve the hook contract and cleanup $items post-hook.
841 $map = [
842 'bower_components' => rtrim(Civi::paths()->getUrl('[civicrm.bower]/.', 'absolute'), '/'),
843 'packages' => rtrim(Civi::paths()->getUrl('[civicrm.packages]/.', 'absolute'), '/'),
844 ];
845 $filter = function($m) use ($map) {
846 return $map[$m[1]] . $m[2];
847 };
848 $items = array_map(function($item) use ($filter) {
849 return is_array($item) ? $item : preg_replace_callback(';^(bower_components|packages)(/.*);', $filter, $item);
850 }, $items);
851
6a488035
TO
852 return $items;
853 }
156fd9b9
CW
854
855 /**
a6c01b45
CW
856 * @return bool
857 * is this page request an ajax snippet?
156fd9b9 858 */
00be9182 859 public static function isAjaxMode() {
be2fb01f 860 if (in_array(CRM_Utils_Array::value('snippet', $_REQUEST), [
518fa0ee
SL
861 CRM_Core_Smarty::PRINT_SNIPPET,
862 CRM_Core_Smarty::PRINT_NOFORM,
863 CRM_Core_Smarty::PRINT_JSON,
864 ])
42a40a1c 865 ) {
866 return TRUE;
867 }
60c3b6e9
CW
868 list($arg0, $arg1) = array_pad(explode('/', CRM_Utils_System::getUrlPath()), 2, '');
869 return ($arg0 === 'civicrm' && in_array($arg1, ['ajax', 'angularprofiles', 'asset']));
156fd9b9 870 }
b7ceb253 871
1889d803 872 /**
518fa0ee 873 * @param \Civi\Core\Event\GenericHookEvent $e
1889d803
CW
874 * @see \CRM_Utils_Hook::buildAsset()
875 */
876 public static function renderMenubarStylesheet(GenericHookEvent $e) {
877 if ($e->asset !== 'crm-menubar.css') {
878 return;
879 }
880 $e->mimeType = 'text/css';
dcaf410f 881 $content = '';
1889d803
CW
882 $config = CRM_Core_Config::singleton();
883 $cms = strtolower($config->userFramework);
884 $cms = $cms === 'drupal' ? 'drupal7' : $cms;
885 $items = [
886 'bower_components/smartmenus/dist/css/sm-core-css.css',
887 'css/crm-menubar.css',
888 "css/menubar-$cms.css",
889 ];
890 foreach ($items as $item) {
dcaf410f 891 $content .= file_get_contents(self::singleton()->getPath('civicrm', $item));
8a52ae34 892 }
dcaf410f
CW
893 $params = $e->params;
894 // "color" is deprecated in favor of the more specific "menubarColor"
895 $menubarColor = $params['color'] ?? $params['menubarColor'];
1889d803 896 $vars = [
dcaf410f
CW
897 '$resourceBase' => rtrim($config->resourceBase, '/'),
898 '$menubarHeight' => $params['height'] . 'px',
899 '$breakMin' => $params['breakpoint'] . 'px',
900 '$breakMax' => ($params['breakpoint'] - 1) . 'px',
901 '$menubarColor' => $menubarColor,
63c2508b 902 '$menuItemColor' => $params['menuItemColor'] ?? $menubarColor,
dcaf410f
CW
903 '$highlightColor' => $params['highlightColor'] ?? CRM_Utils_Color::getHighlight($menubarColor),
904 '$textColor' => $params['textColor'] ?? CRM_Utils_Color::getContrast($menubarColor, '#333', '#ddd'),
1889d803 905 ];
dcaf410f
CW
906 $vars['$highlightTextColor'] = $params['highlightTextColor'] ?? CRM_Utils_Color::getContrast($vars['$highlightColor'], '#333', '#ddd');
907 $e->content = str_replace(array_keys($vars), array_values($vars), $content);
1889d803
CW
908 }
909
b7ceb253 910 /**
f9e31d7f 911 * Provide a list of available entityRef filters.
fd7c068f 912 *
b7ceb253
CW
913 * @return array
914 */
e695ee7c
CW
915 public static function getEntityRefMetadata() {
916 $data = [
917 'filters' => [],
918 'links' => [],
919 ];
06606cd1 920 $config = CRM_Core_Config::singleton();
b7ceb253 921
1d6f94ab
CW
922 $disabledComponents = [];
923 $dao = CRM_Core_DAO::executeQuery("SELECT name, namespace FROM civicrm_component");
924 while ($dao->fetch()) {
925 if (!in_array($dao->name, $config->enableComponents)) {
926 $disabledComponents[$dao->name] = $dao->namespace;
06606cd1
CW
927 }
928 }
929
1d6f94ab
CW
930 foreach (CRM_Core_DAO_AllCoreTables::daoToClass() as $entity => $daoName) {
931 // Skip DAOs of disabled components
932 foreach ($disabledComponents as $nameSpace) {
933 if (strpos($daoName, $nameSpace) === 0) {
934 continue 2;
935 }
936 }
937 $baoName = str_replace('_DAO_', '_BAO_', $daoName);
938 if (class_exists($baoName)) {
e695ee7c
CW
939 $filters = $baoName::getEntityRefFilters();
940 if ($filters) {
2229cf4f 941 $data['filters'][$entity] = $filters;
e695ee7c
CW
942 }
943 if (is_callable([$baoName, 'getEntityRefCreateLinks'])) {
944 $createLinks = $baoName::getEntityRefCreateLinks();
945 if ($createLinks) {
2229cf4f 946 $data['links'][$entity] = $createLinks;
e695ee7c 947 }
1d6f94ab
CW
948 }
949 }
b7b528bc
CW
950 }
951
77d0bf4e 952 CRM_Utils_Hook::entityRefFilters($data['filters'], $data['links']);
fd7c068f 953
e695ee7c 954 return $data;
b7ceb253 955 }
96025800 956
09a4dcd5 957 /**
d89d2545 958 * Determine the minified file name.
09a4dcd5 959 *
d89d2545
TO
960 * @param string $ext
961 * @param string $file
962 * @return string
963 * An updated $fileName. If a minified version exists and is supported by
964 * system policy, the minified version will be returned. Otherwise, the original.
965 */
966 public function filterMinify($ext, $file) {
967 if (CRM_Core_Config::singleton()->debug && strpos($file, '.min.') !== FALSE) {
968 $nonMiniFile = str_replace('.min.', '.', $file);
969 if ($this->getPath($ext, $nonMiniFile)) {
970 $file = $nonMiniFile;
09a4dcd5
CW
971 }
972 }
d89d2545 973 return $file;
09a4dcd5
CW
974 }
975
6f12c6eb 976 /**
977 * @param string $url
978 * @return string
979 */
980 public function addCacheCode($url) {
33603e1d 981 $hasQuery = strpos($url, '?') !== FALSE;
03449a5b 982 $operator = $hasQuery ? '&' : '?';
6f12c6eb 983
03449a5b 984 return $url . $operator . 'r=' . $this->cacheCode;
6f12c6eb 985 }
33603e1d 986
adcd4bf7
CW
987 /**
988 * Checks if the given URL is fully-formed
989 *
990 * @param string $url
991 *
992 * @return bool
993 */
994 public static function isFullyFormedUrl($url) {
995 return (substr($url, 0, 4) === 'http') || (substr($url, 0, 1) === '/');
996 }
997
6a488035 998}