Merge pull request #17093 from eileenmcnaughton/cont
[civicrm-core.git] / CRM / Core / Resources.php
1 <?php
2 /*
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 +--------------------------------------------------------------------+
10 */
11 use Civi\Core\Event\GenericHookEvent;
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
25 * @copyright CiviCRM LLC https://civicrm.org/licensing
26 * $Id$
27 *
28 */
29 class 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
37 */
38 private static $_singleton = NULL;
39
40 /**
41 * @var CRM_Extension_Mapper
42 */
43 private $extMapper = NULL;
44
45 /**
46 * @var CRM_Core_Resources_Strings
47 */
48 private $strings = NULL;
49
50 /**
51 * Settings in free-form data tree.
52 *
53 * @var array
54 */
55 protected $settings = [];
56
57 /**
58 * Setting factories.
59 *
60 * @var callable[]
61 */
62 protected $settingsFactories = [];
63
64 /**
65 * Added core resources.
66 *
67 * Format is ($regionName => bool).
68 *
69 * @var array
70 */
71 protected $addedCoreResources = [];
72
73 /**
74 * Added core styles.
75 *
76 * Format is ($regionName => bool).
77 *
78 * @var array
79 */
80 protected $addedCoreStyles = [];
81
82 /**
83 * Added settings.
84 *
85 * Format is ($regionName => bool).
86 *
87 * @var array
88 */
89 protected $addedSettings = [];
90
91 /**
92 * A value to append to JS/CSS URLs to coerce cache resets.
93 *
94 * @var string
95 */
96 protected $cacheCode = NULL;
97
98 /**
99 * The name of a setting which persistently stores the cacheCode.
100 *
101 * @var string
102 */
103 protected $cacheCodeKey = NULL;
104
105 /**
106 * Are ajax popup screens enabled.
107 *
108 * @var bool
109 */
110 public $ajaxPopupsEnabled;
111
112 /**
113 * @var \Civi\Core\Paths
114 */
115 protected $paths;
116
117 /**
118 * Get or set the single instance of CRM_Core_Resources.
119 *
120 * @param CRM_Core_Resources $instance
121 * New copy of the manager.
122 *
123 * @return CRM_Core_Resources
124 */
125 public static function singleton(CRM_Core_Resources $instance = NULL) {
126 if ($instance !== NULL) {
127 self::$_singleton = $instance;
128 }
129 if (self::$_singleton === NULL) {
130 self::$_singleton = Civi::service('resources');
131 }
132 return self::$_singleton;
133 }
134
135 /**
136 * Construct a resource manager.
137 *
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.
142 * @param string|null $cacheCodeKey Random code to append to resource URLs; changing the code forces clients to reload resources
143 */
144 public function __construct($extMapper, $cache, $cacheCodeKey = NULL) {
145 $this->extMapper = $extMapper;
146 $this->strings = new CRM_Core_Resources_Strings($cache);
147 $this->cacheCodeKey = $cacheCodeKey;
148 if ($cacheCodeKey !== NULL) {
149 $this->cacheCode = Civi::settings()->get($cacheCodeKey);
150 }
151 if (!$this->cacheCode) {
152 $this->resetCacheCode();
153 }
154 $this->ajaxPopupsEnabled = (bool) Civi::settings()->get('ajaxPopupsEnabled');
155 $this->paths = Civi::paths();
156 }
157
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;
172 $perms = [];
173 foreach ($permNames as $permName) {
174 $perms[$permName] = CRM_Core_Permission::check($permName);
175 }
176 return $this->addSetting([
177 'permissions' => $perms,
178 ]);
179 }
180
181 /**
182 * Add a JavaScript file to the current page using <SCRIPT SRC>.
183 *
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'.
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.
197 *
198 * @return CRM_Core_Resources
199 *
200 * @throws \CRM_Core_Exception
201 */
202 public function addScriptFile($ext, $file, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION, $translate = TRUE) {
203 if ($translate) {
204 $domain = ($translate === TRUE) ? $ext : $translate;
205 $this->addString($this->strings->get($domain, $this->getPath($ext, $file), 'text/javascript'), $domain);
206 }
207 $url = $this->getUrl($ext, $this->filterMinify($ext, $file), TRUE);
208 return $this->addScriptUrl($url, $weight, $region);
209 }
210
211 /**
212 * Add a JavaScript file to the current page using <SCRIPT SRC>.
213 *
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'.
219 * @return CRM_Core_Resources
220 */
221 public function addScriptUrl($url, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
222 CRM_Core_Region::instance($region)->add([
223 'name' => $url,
224 'type' => 'scriptUrl',
225 'scriptUrl' => $url,
226 'weight' => $weight,
227 'region' => $region,
228 ]);
229 return $this;
230 }
231
232 /**
233 * Add a JavaScript file to the current page using <SCRIPT SRC>.
234 *
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'.
241 * @return CRM_Core_Resources
242 */
243 public function addScript($code, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
244 CRM_Core_Region::instance($region)->add([
245 // 'name' => automatic
246 'type' => 'script',
247 'script' => $code,
248 'weight' => $weight,
249 'region' => $region,
250 ]);
251 return $this;
252 }
253
254 /**
255 * Add JavaScript variables to CRM.vars
256 *
257 * Example:
258 * From the server:
259 * CRM_Core_Resources::singleton()->addVars('myNamespace', array('foo' => 'bar'));
260 * Access var from javascript:
261 * CRM.vars.myNamespace.foo // "bar"
262 *
263 * @see http://wiki.civicrm.org/confluence/display/CRMDOC/Javascript+Reference
264 *
265 * @param string $nameSpace
266 * Usually the name of your extension.
267 * @param array $vars
268 * @param string $region
269 * The region to add settings to (eg. for payment processors usually billing-block)
270 *
271 * @return CRM_Core_Resources
272 */
273 public function addVars($nameSpace, $vars, $region = NULL) {
274 $existing = CRM_Utils_Array::value($nameSpace, CRM_Utils_Array::value('vars', $this->settings), []);
275 $vars = $this->mergeSettings($existing, $vars);
276 $this->addSetting(['vars' => [$nameSpace => $vars]], $region);
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 *
285 * @param array $settings
286 * @param string $region
287 * The region to add settings to (eg. for payment processors usually billing-block)
288 *
289 * @return CRM_Core_Resources
290 */
291 public function addSetting($settings, $region = NULL) {
292 if (!$region) {
293 $region = self::isAjaxMode() ? 'ajax-snippet' : 'html-header';
294 }
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;
308 return $this;
309 }
310
311 /**
312 * Add JavaScript variables to the global CRM object via a callback function.
313 *
314 * @param callable $callable
315 * @return CRM_Core_Resources
316 */
317 public function addSettingsFactory($callable) {
318 // Make sure our callback has been registered
319 $this->addSetting([]);
320 $this->settingsFactories[] = $callable;
321 return $this;
322 }
323
324 /**
325 * Helper fn for addSettingsFactory.
326 */
327 public function getSettings() {
328 $result = $this->settings;
329 foreach ($this->settingsFactories as $callable) {
330 $result = $this->mergeSettings($result, $callable());
331 }
332 CRM_Utils_Hook::alterResourceSettings($result);
333 return $result;
334 }
335
336 /**
337 * @param array $settings
338 * @param array $additions
339 * @return array
340 * combination of $settings and $additions
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 /**
353 * Helper fn for addSetting.
354 * Render JavaScript variables for the global CRM object.
355 *
356 * @return string
357 */
358 public function renderSetting($region = NULL) {
359 // On a standard page request we construct the CRM object from scratch
360 if (($region === 'html-header') || !self::isAjaxMode()) {
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 }
367 return sprintf("<script type=\"text/javascript\">\n%s\n</script>\n", $js);
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
374 *
375 * Note: this function rarely needs to be called directly and is mostly for internal use.
376 * See CRM_Core_Resources::addScriptFile which automatically adds translated strings from js files
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 *
396 * @param string|array $text
397 * @param string|null $domain
398 * @return CRM_Core_Resources
399 */
400 public function addString($text, $domain = 'civicrm') {
401 foreach ((array) $text as $str) {
402 $translated = ts($str, [
403 'domain' => ($domain == 'civicrm') ? NULL : [$domain, NULL],
404 'raw' => TRUE,
405 ]);
406
407 // We only need to push this string to client if the translation
408 // is actually different from the original
409 if ($translated != $str) {
410 $bucket = $domain == 'civicrm' ? 'strings' : 'strings::' . $domain;
411 $this->addSetting([
412 $bucket => [$str => $translated],
413 ]);
414 }
415 }
416 return $this;
417 }
418
419 /**
420 * Add a CSS file to the current page using <LINK HREF>.
421 *
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'.
430 * @return CRM_Core_Resources
431 */
432 public function addStyleFile($ext, $file, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
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;
439 }
440
441 /**
442 * Add a CSS file to the current page using <LINK HREF>.
443 *
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'.
449 * @return CRM_Core_Resources
450 */
451 public function addStyleUrl($url, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
452 CRM_Core_Region::instance($region)->add([
453 'name' => $url,
454 'type' => 'styleUrl',
455 'styleUrl' => $url,
456 'weight' => $weight,
457 'region' => $region,
458 ]);
459 return $this;
460 }
461
462 /**
463 * Add a CSS content to the current page using <STYLE>.
464 *
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'.
471 * @return CRM_Core_Resources
472 */
473 public function addStyle($code, $weight = self::DEFAULT_WEIGHT, $region = self::DEFAULT_REGION) {
474 CRM_Core_Region::instance($region)->add([
475 // 'name' => automatic
476 'type' => 'style',
477 'style' => $code,
478 'weight' => $weight,
479 'region' => $region,
480 ]);
481 return $this;
482 }
483
484 /**
485 * Determine file path of a resource provided by an extension.
486 *
487 * @param string $ext
488 * extension name; use 'civicrm' for core.
489 * @param string|null $file
490 * file path -- relative to the extension base dir.
491 *
492 * @return bool|string
493 * full file path or FALSE if not found
494 */
495 public function getPath($ext, $file = NULL) {
496 // TODO consider caching results
497 $base = $this->paths->hasVariable($ext)
498 ? rtrim($this->paths->getVariable($ext, 'path'), '/')
499 : $this->extMapper->keyToBasePath($ext);
500 if ($file === NULL) {
501 return $base;
502 }
503 $path = $base . '/' . $file;
504 if (is_file($path)) {
505 return $path;
506 }
507 return FALSE;
508 }
509
510 /**
511 * Determine public URL of a resource provided by an extension.
512 *
513 * @param string $ext
514 * extension name; use 'civicrm' for core.
515 * @param string $file
516 * file path -- relative to the extension base dir.
517 * @param bool $addCacheCode
518 *
519 * @return string, URL
520 */
521 public function getUrl($ext, $file = NULL, $addCacheCode = FALSE) {
522 if ($file === NULL) {
523 $file = '';
524 }
525 if ($addCacheCode) {
526 $file = $this->addCacheCode($file);
527 }
528 // TODO consider caching results
529 $base = $this->paths->hasVariable($ext)
530 ? $this->paths->getVariable($ext, 'url')
531 : ($this->extMapper->keyToUrl($ext) . '/');
532 return $base . $file;
533 }
534
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;
551 $files = [];
552 foreach ($patterns as $pattern) {
553 if (preg_match(';^(assetBuilder|ext)://;', $pattern)) {
554 $files[] = $pattern;
555 }
556 if (CRM_Utils_File::isAbsolute($pattern)) {
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 }
565 // Deterministic order.
566 sort($files);
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
573 /**
574 * @return string
575 */
576 public function getCacheCode() {
577 return $this->cacheCode;
578 }
579
580 /**
581 * @param $value
582 * @return CRM_Core_Resources
583 */
584 public function setCacheCode($value) {
585 $this->cacheCode = $value;
586 if ($this->cacheCodeKey) {
587 Civi::settings()->set($this->cacheCodeKey, $value);
588 }
589 return $this;
590 }
591
592 /**
593 * @return CRM_Core_Resources
594 */
595 public function resetCacheCode() {
596 $this->setCacheCode(CRM_Utils_String::createRandom(5, CRM_Utils_String::ALPHANUMERIC));
597 // Also flush cms resource cache if needed
598 CRM_Core_Config::singleton()->userSystem->clearResourceCache();
599 return $this;
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 *
609 * @param string $region
610 * @return CRM_Core_Resources
611 */
612 public function addCoreResources($region = 'html-header') {
613 if (!isset($this->addedCoreResources[$region]) && !self::isAjaxMode()) {
614 $this->addedCoreResources[$region] = TRUE;
615 $config = CRM_Core_Config::singleton();
616
617 // Add resources from coreResourceList
618 $jsWeight = -9999;
619 foreach ($this->coreResourceList($region) as $item) {
620 if (is_array($item)) {
621 $this->addSetting($item);
622 }
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 {
630 // Don't bother looking for ts() calls in packages, there aren't any
631 $translate = (substr($item, 0, 3) == 'js/');
632 $this->addScriptFile('civicrm', $item, $jsWeight++, $region, $translate);
633 }
634 }
635 // Add global settings
636 $settings = [
637 'config' => [
638 'isFrontend' => $config->userFrameworkFrontend,
639 ],
640 ];
641 // Disable profile creation if user lacks permission
642 if (!CRM_Core_Permission::check('edit all contacts') && !CRM_Core_Permission::check('add contacts')) {
643 $settings['config']['entityRef']['contactCreate'] = FALSE;
644 }
645 $this->addSetting($settings);
646
647 // Give control of jQuery and _ back to the CMS - this loads last
648 $this->addScriptFile('civicrm', 'js/noconflict.js', 9999, $region, FALSE);
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)) {
671 $customCSSURL = $this->addCacheCode($config->customCSSURL);
672 $this->addStyleUrl($customCSSURL, 99, $region);
673 }
674 if (!Civi::settings()->get('disable_core_css')) {
675 $this->addStyleFile('civicrm', 'css/civicrm.css', -99, $region);
676 }
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);
679 }
680 return $this;
681 }
682
683 /**
684 * Flushes cached translated strings.
685 * @return CRM_Core_Resources
686 */
687 public function flushStrings() {
688 $this->strings->flush();
689 return $this;
690 }
691
692 /**
693 * @return CRM_Core_Resources_Strings
694 */
695 public function getStrings() {
696 return $this->strings;
697 }
698
699 /**
700 * Create dynamic script for localizing js widgets.
701 */
702 public static function outputLocalizationJS() {
703 CRM_Core_Page_AJAX::setJsHeaders();
704 $config = CRM_Core_Config::singleton();
705 $vars = [
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...')),
709 'entityRef' => self::getEntityRefMetadata(),
710 'ajaxPopupsEnabled' => self::singleton()->ajaxPopupsEnabled,
711 'allowAlertAutodismissal' => (bool) Civi::settings()->get('allow_alert_autodismissal'),
712 'resourceCacheCode' => self::singleton()->getCacheCode(),
713 'locale' => CRM_Core_I18n::getLocale(),
714 'cid' => (int) CRM_Core_Session::getLoggedInContactID(),
715 ];
716 print CRM_Core_Smarty::singleton()->fetchWith('CRM/common/l10n.js.tpl', $vars);
717 CRM_Utils_System::civiExit();
718 }
719
720 /**
721 * List of core resources we add to every CiviCRM page.
722 *
723 * Note: non-compressed versions of .min files will be used in debug mode
724 *
725 * @param string $region
726 * @return array
727 */
728 public function coreResourceList($region) {
729 $config = CRM_Core_Config::singleton();
730
731 // Scripts needed by everyone, everywhere
732 // FIXME: This is too long; list needs finer-grained segmentation
733 $items = [
734 "bower_components/jquery/dist/jquery.min.js",
735 "bower_components/jquery-ui/jquery-ui.min.js",
736 "bower_components/jquery-ui/themes/smoothness/jquery-ui.min.css",
737 "bower_components/lodash-compat/lodash.min.js",
738 "packages/jquery/plugins/jquery.mousewheel.min.js",
739 "bower_components/select2/select2.min.js",
740 "bower_components/select2/select2.min.css",
741 "bower_components/font-awesome/css/font-awesome.min.css",
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",
749 "js/Common.js",
750 "js/crm.datepicker.js",
751 "js/crm.ajax.js",
752 "js/wysiwyg/crm.wysiwyg.js",
753 ];
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
761 // add wysiwyg editor
762 $editor = Civi::settings()->get('editor_id');
763 if ($editor == "CKEditor") {
764 CRM_Admin_Page_CKEditorConfig::setConfigDefault();
765 $items[] = [
766 'config' => [
767 'wysisygScriptLocation' => Civi::paths()->getUrl("[civicrm.root]/js/wysiwyg/crm.ckeditor.js"),
768 'CKEditorCustomConfig' => CRM_Admin_Page_CKEditorConfig::getConfigUrl(),
769 ],
770 ];
771 }
772
773 // These scripts are only needed by back-office users
774 if (CRM_Core_Permission::check('access CiviCRM')) {
775 $items[] = "packages/jquery/plugins/jquery.tableHeader.js";
776 $items[] = "packages/jquery/plugins/jquery.notify.min.js";
777 }
778
779 $contactID = CRM_Core_Session::getLoggedInContactID();
780
781 // Menubar
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') {
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';
795 // @see CRM_Core_Resources::renderMenubarStylesheet
796 $items[] = Civi::service('asset_builder')->getUrl('crm-menubar.css', [
797 'menubarColor' => Civi::settings()->get('menubar_color'),
798 'height' => 40,
799 'breakpoint' => 768,
800 ]);
801 // Variables for crm.menubar.js
802 $items[] = [
803 'menubar' => [
804 'position' => $position,
805 'qfKey' => CRM_Core_Key::get('CRM_Contact_Controller_Search', TRUE),
806 'cacheCode' => CRM_Core_BAO_Navigation::getCacheKey($contactID),
807 ],
808 ];
809 }
810
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
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 }
820
821 $tsLocale = CRM_Core_I18n::getLocale();
822 // Add localized jQuery UI files
823 if ($tsLocale && $tsLocale != 'en_US') {
824 // Search for i18n file in order of specificity (try fr-CA, then fr)
825 list($lang) = explode('_', $tsLocale);
826 $path = "bower_components/jquery-ui/ui/i18n";
827 foreach ([str_replace('_', '-', $tsLocale), $lang] as $language) {
828 $localizationFile = "$path/datepicker-{$language}.js";
829 if ($this->getPath('civicrm', $localizationFile)) {
830 $items[] = $localizationFile;
831 break;
832 }
833 }
834 }
835
836 // Allow hooks to modify this list
837 CRM_Utils_Hook::coreResourceList($items, $region);
838
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
852 return $items;
853 }
854
855 /**
856 * @return bool
857 * is this page request an ajax snippet?
858 */
859 public static function isAjaxMode() {
860 if (in_array(CRM_Utils_Array::value('snippet', $_REQUEST), [
861 CRM_Core_Smarty::PRINT_SNIPPET,
862 CRM_Core_Smarty::PRINT_NOFORM,
863 CRM_Core_Smarty::PRINT_JSON,
864 ])
865 ) {
866 return TRUE;
867 }
868 list($arg0, $arg1) = array_pad(explode('/', CRM_Utils_System::getUrlPath()), 2, '');
869 return ($arg0 === 'civicrm' && in_array($arg1, ['ajax', 'angularprofiles', 'asset']));
870 }
871
872 /**
873 * @param \Civi\Core\Event\GenericHookEvent $e
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';
881 $content = '';
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) {
891 $content .= file_get_contents(self::singleton()->getPath('civicrm', $item));
892 }
893 $params = $e->params;
894 // "color" is deprecated in favor of the more specific "menubarColor"
895 $menubarColor = $params['color'] ?? $params['menubarColor'];
896 $vars = [
897 '$resourceBase' => rtrim($config->resourceBase, '/'),
898 '$menubarHeight' => $params['height'] . 'px',
899 '$breakMin' => $params['breakpoint'] . 'px',
900 '$breakMax' => ($params['breakpoint'] - 1) . 'px',
901 '$menubarColor' => $menubarColor,
902 '$menuItemColor' => $params['menuItemColor'] ?? $menubarColor,
903 '$highlightColor' => $params['highlightColor'] ?? CRM_Utils_Color::getHighlight($menubarColor),
904 '$textColor' => $params['textColor'] ?? CRM_Utils_Color::getContrast($menubarColor, '#333', '#ddd'),
905 ];
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);
908 }
909
910 /**
911 * Provide a list of available entityRef filters.
912 *
913 * @return array
914 */
915 public static function getEntityRefMetadata() {
916 $data = [
917 'filters' => [],
918 'links' => [],
919 ];
920 $config = CRM_Core_Config::singleton();
921
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;
927 }
928 }
929
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)) {
939 $filters = $baoName::getEntityRefFilters();
940 if ($filters) {
941 $data['filters'][$entity] = $filters;
942 }
943 if (is_callable([$baoName, 'getEntityRefCreateLinks'])) {
944 $createLinks = $baoName::getEntityRefCreateLinks();
945 if ($createLinks) {
946 $data['links'][$entity] = $createLinks;
947 }
948 }
949 }
950 }
951
952 CRM_Utils_Hook::entityRefFilters($data['filters'], $data['links']);
953
954 return $data;
955 }
956
957 /**
958 * Determine the minified file name.
959 *
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;
971 }
972 }
973 return $file;
974 }
975
976 /**
977 * @param string $url
978 * @return string
979 */
980 public function addCacheCode($url) {
981 $hasQuery = strpos($url, '?') !== FALSE;
982 $operator = $hasQuery ? '&' : '?';
983
984 return $url . $operator . 'r=' . $this->cacheCode;
985 }
986
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
998 }