4 * OpenEMR <https://open-emr.org>.
6 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
9 namespace OpenEMR\Core
;
11 use OpenEMR\Common\Logging\SystemLogger
;
12 use OpenEMR\Events\Core\ScriptFilterEvent
;
13 use OpenEMR\Events\Core\StyleFilterEvent
;
14 use OpenEMR\Services\LogoService
;
15 use Symfony\Component\Yaml\Yaml
;
16 use Symfony\Component\Yaml\Exception\ParseException
;
21 * Helper class to generate some `<script>` and `<link>` elements based on a
22 * configuration file. This file would be a good place to include other helpers
23 * for creating a `<head>` element, but for now it sufficently handles the
28 * @author Robert Down <robertdown@live.com>
29 * @copyright Copyright (c) 2017-2022 Robert Down
33 private static $scripts;
34 private static $links;
35 private static $isHeader;
38 * Setup various <head> elements.
40 * See root_dir/config/config.yaml for available assets
42 * Example usage in a PHP view script:
44 * // Top of script with require_once statements
45 * use OpenEMR\Core\Header;
48 * // If no special assets are needed:
49 * Header::setupHeader();
51 * // If 1 special asset is needed:
52 * Header::setupHeader('key-of-asset');
54 * // If 2 or more assets are needed:
55 * Header::setupHeader(['array', 'of', 'keys']);
57 * // If wish to not include a normally autoloaded asset
58 * Header::setupHeader('no_main-theme');
61 * Inside of a twig template (Parameters same as before):
63 * {{ includeAsset() }}
66 * Inside of a smarty template, use | (pipe) delimited string of key names
69 * {headerTemplate assets='key-of-asset'} (1 optional assets)
70 * {headerTemplate assets='array|of|keys'} (multiple optional assets. ie. via | delimiter)
73 * The above example will render `<script>` tags and `<link>` tag which
74 * bring in the requested assets from config.yaml
76 * @param array|string $assets Asset(s) to include
77 * @param boolean $echoOutput - if true then echo
78 * if false then return string
79 * @throws ParseException If unable to parse the config file
82 public static function setupHeader($assets = [], $echoOutput = true)
84 $favicon = self
::getFavIcon();
87 $output = "\n<meta charset=\"utf-8\" />\n";
88 // Makes only compatible with MS Edge
89 $output .= "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n";
91 $output .= "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\" />\n";
93 $output .= "<link rel=\"shortcut icon\" href=\"$favicon\" />\n";
94 $output .= self
::setupAssets($assets, true, false);
96 // we need to grab the script
97 $scriptName = $_SERVER['SCRIPT_NAME'];
99 // we fire off events to grab any additional module scripts or css files that desire to adjust the currently executed script
100 $scriptFilterEvent = new ScriptFilterEvent(basename($scriptName));
101 $scriptFilterEvent->setContextArgument(ScriptFilterEvent
::CONTEXT_ARGUMENT_SCRIPT_NAME
, $scriptName);
102 $apptScripts = $GLOBALS['kernel']->getEventDispatcher()->dispatch($scriptFilterEvent, ScriptFilterEvent
::EVENT_NAME
);
104 $styleFilterEvent = new StyleFilterEvent($scriptName);
105 $styleFilterEvent->setContextArgument(StyleFilterEvent
::CONTEXT_ARGUMENT_SCRIPT_NAME
, $scriptName);
106 $apptStyles = $GLOBALS['kernel']->getEventDispatcher()->dispatch($styleFilterEvent, StyleFilterEvent
::EVENT_NAME
);
107 // note these scripts have been filtered to be in the same origin as the current site in pnadmin.php & pnuserapi.php {
109 if (!empty($apptScripts->getScripts())) {
110 $output .= "<!-- Module Scripts Started -->";
111 foreach ($apptScripts->getScripts() as $script) {
112 // we want versions appended
113 $output .= Header
::createElement($script, 'script', false);
115 $output .= "<!-- Module Scripts Ended -->";
118 if (!empty($apptStyles->getStyles())) {
119 $output .= "<!-- Module Styles Started -->";
120 foreach ($apptStyles->getStyles() as $cssSrc) {
121 // we want version appended
122 $output .= Header
::createElement($cssSrc, 'style', false);
124 $output .= "<!-- Module Styles Ended -->";
134 public static function getFavIcon()
136 $logoService = new LogoService();
137 $icon = $logoService->getLogo("core/favicon/", "favicon.ico");
142 * Can call this function directly rather than using above setupHeader function
143 * if do not want to include the autoloaded assets.
145 * @param array $assets Asset(s) to include
146 * @param boolean $headerMode - if true, then include autoloaded assets
147 * if false, then do not include autoloaded assets
148 * @param boolean $echoOutput - if true then echo
149 * if false then return string
151 public static function setupAssets($assets = [], $headerMode = false, $echoOutput = true)
154 self
::$isHeader = true;
156 self
::$isHeader = false;
161 echo self
::includeAsset($assets);
163 return self
::includeAsset($assets);
165 } catch (\InvalidArgumentException
$e) {
166 error_log(errorLogEscape($e->getMessage()));
171 * Include an asset from a config file.
173 * Static function to read in a YAML file into an array, check if the
174 * $assets keys are in the config file, and from the config file generate
175 * the HTML for a `<script>` or `<link>` tag.
177 * This is a private function, use Header::setupHeader() instead
179 * @param array|string $assets Asset(s) to include
180 * @throws ParseException If unable to parse the config file
183 private static function includeAsset($assets = [])
186 if (is_string($assets)) {
190 // @TODO Hard coded the path to the config file, not good RD 2017-05-27
191 $map = self
::readConfigFile("{$GLOBALS['fileroot']}/config/config.yaml");
195 self
::parseConfigFile($map, $assets);
197 /* adding custom assets in addition */
198 if (is_file("{$GLOBALS['fileroot']}/custom/assets/custom.yaml")) {
199 $customMap = self
::readConfigFile("{$GLOBALS['fileroot']}/custom/assets/custom.yaml");
200 self
::parseConfigFile($customMap, $assets);
203 $linksStr = implode("", self
::$links);
204 $scriptsStr = implode("", self
::$scripts);
205 return "\n{$linksStr}\n{$scriptsStr}\n";
209 * Parse assets from config file
211 * @param array $map Assets to parse into self::$scripts and self::$links
212 * @param array $selectedAssets
215 private static function parseConfigFile($map, $selectedAssets = array())
219 foreach ($map as $k => $opts) {
220 $autoload = (isset($opts['autoload'])) ?
$opts['autoload'] : false;
221 $allowNoLoad = (isset($opts['allowNoLoad'])) ?
$opts['allowNoLoad'] : false;
222 $alreadyBuilt = (isset($opts['alreadyBuilt'])) ?
$opts['alreadyBuilt'] : false;
223 $loadInFile = (isset($opts['loadInFile'])) ?
$opts['loadInFile'] : false;
224 $rtl = (isset($opts['rtl'])) ?
$opts['rtl'] : false;
226 if ((self
::$isHeader === true && $autoload === true) ||
in_array($k, $selectedAssets) ||
($loadInFile && $loadInFile === self
::getCurrentFile())) {
227 if ($allowNoLoad === true) {
228 if (in_array("no_" . $k, $selectedAssets)) {
235 $tmp = self
::buildAsset($opts, $alreadyBuilt);
237 foreach ($tmp['scripts'] as $s) {
238 self
::$scripts[] = $s;
241 if (($k == "bootstrap") && ((!in_array("no_main-theme", $selectedAssets)) ||
(in_array("patientportal-style", $selectedAssets)))) {
242 // Above comparison is to skip bootstrap theme loading when using a main theme or using the patient portal theme
243 // since bootstrap theme is already including in main themes and portal theme via SASS.
244 } else if ($k == "compact-theme" && (in_array("no_main-theme", $selectedAssets) ||
empty($GLOBALS['enable_compact_mode']))) {
245 // Do not display compact theme if it is turned off
247 foreach ($tmp['links'] as $l) {
252 if ($rtl && !empty($_SESSION['language_direction']) && $_SESSION['language_direction'] == 'rtl') {
253 $tmpRtl = self
::buildAsset($rtl, $alreadyBuilt);
254 foreach ($tmpRtl['scripts'] as $s) {
255 self
::$scripts[] = $s;
258 if ($k == "compact-theme" && (in_array("no_main-theme", $selectedAssets) ||
!$GLOBALS['enable_compact_mode'])) {
260 foreach ($tmpRtl['links'] as $l) {
268 if (($thisCnt = count(array_diff($selectedAssets, $foundAssets))) > 0) {
269 if ($thisCnt !== $excludedCount) {
270 (new SystemLogger())->error("Not all selected assets were included in header", ['selectedAssets' => $selectedAssets, 'foundAssets' => $foundAssets]);
275 * Build an html element from config options.
277 * @var array $opts Options
278 * @var boolean $alreadyBuilt - This means the path with cache busting segment has already been built
279 * @return array Array with `scripts` and `links` keys which contain arrays of elements
281 private static function buildAsset($opts = array(), $alreadyBuilt = false)
283 $script = (isset($opts['script'])) ?
$opts['script'] : false;
284 $link = (isset($opts['link'])) ?
$opts['link'] : false;
285 $path = (isset($opts['basePath'])) ?
$opts['basePath'] : '';
286 $basePath = self
::parsePlaceholders($path);
292 if (!is_string($script) && !is_array($script)) {
293 throw new \
InvalidArgumentException("Script must be of type string or array");
296 if (is_string($script)) {
300 foreach ($script as $k) {
301 $k = self
::parsePlaceholders($k);
305 $path = self
::createFullPath($basePath, $k);
307 $scripts[] = self
::createElement($path, 'script', $alreadyBuilt);
312 if (!is_string($link) && !is_array($link)) {
313 throw new \
InvalidArgumentException("Link must be of type string or array");
316 if (is_string($link)) {
320 foreach ($link as $l) {
321 $l = self
::parsePlaceholders($l);
325 $path = self
::createFullPath($basePath, $l);
327 $links[] = self
::createElement($path, 'link', $alreadyBuilt);
331 return ['scripts' => $scripts, 'links' => $links];
335 * Parse a string for $GLOBAL key placeholders %key-name%.
337 * Perform a regex match all in the given subject for anything wrapped in
338 * percent signs `%some-key%` and if that string exists in the $GLOBALS
339 * array, will replace the occurence with the value of that key.
341 * @param string $subject String containing placeholders (%key-name%)
342 * @return string The new string with properly replaced keys
344 public static function parsePlaceholders($subject)
348 preg_match_all($re, $subject, $matches, PREG_SET_ORDER
, 0);
350 foreach ($matches as $match) {
351 if (array_key_exists($match[1], $GLOBALS)) {
352 $subject = str_replace($match[0], $GLOBALS["{$match[1]}"], $subject);
360 * Create the actual HTML element.
362 * @param string $path File path to load
363 * @param string $type Must be `script` or `link`
364 * @return string mixed HTML element
366 private static function createElement($path, $type, $alreadyBuilt)
369 $script = "<script src=\"%path%\"></script>\n";
370 $link = "<link rel=\"stylesheet\" href=\"%path%\" />\n";
372 $template = ($type == 'script') ?
$script : $link;
373 if (!$alreadyBuilt) {
374 $v = $GLOBALS['v_js_includes'];
375 // need to handle header elements that may already have a ? in the parameter.
376 if (strrpos($path, "?") !== false) {
377 $path = $path . "&v={$v}";
379 $path = $path . "?v={$v}";
382 return str_replace("%path%", $path, $template);
386 * Create a full path from given parts.
388 * @param string $base Base path
389 * @param string $path specific path / filename
390 * @return string The full path
392 private static function createFullPath($base, $path)
394 return $base . $path;
398 * Read a config file and turn it into an array.
400 * @param string $file Full path to filename
401 * @return array Array of assets
403 private static function readConfigFile($file)
406 $config = Yaml
::parse(file_get_contents($file));
407 return $config['assets'];
408 } catch (ParseException
$e) {
409 error_log(errorLogEscape($e->getMessage()));
410 // @TODO need to handle this better. RD 2017-05-24
415 * Return relative path to current file
417 * @return string The current file
419 private static function getCurrentFile()
421 //remove web root and query string
422 return str_replace($GLOBALS['webroot'] . '/', '', strtok($_SERVER["REQUEST_URI"], '?'));