Fixes #6505,#6509 fhir,patient helper methods (#6510)
[openemr.git] / src / Core / Header.php
bloba105f3ea1da55df511d3dad85460ab607bd3ef5b
1 <?php
3 /**
4 * OpenEMR <https://open-emr.org>.
6 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
7 */
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;
18 /**
19 * Class Header.
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
24 * `setupHeader()`
26 * @package OpenEMR
27 * @subpackage Core
28 * @author Robert Down <robertdown@live.com>
29 * @copyright Copyright (c) 2017-2022 Robert Down
31 class Header
33 private static $scripts;
34 private static $links;
35 private static $isHeader;
37 /**
38 * Setup various <head> elements.
40 * See root_dir/config/config.yaml for available assets
42 * Example usage in a PHP view script:
43 * ```php
44 * // Top of script with require_once statements
45 * use OpenEMR\Core\Header;
47 * // Inside of <head>
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');
59 * ```
61 * Inside of a twig template (Parameters same as before):
62 * ```html
63 * {{ includeAsset() }}
64 * ```
66 * Inside of a smarty template, use | (pipe) delimited string of key names
67 * ```php
68 * {headerTemplate}
69 * {headerTemplate assets='key-of-asset'} (1 optional assets)
70 * {headerTemplate assets='array|of|keys'} (multiple optional assets. ie. via | delimiter)
71 * ```
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
80 * @return string
82 public static function setupHeader($assets = [], $echoOutput = true)
84 $favicon = self::getFavIcon();
86 // Required tag
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";
90 // BS4 required tag
91 $output .= "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\" />\n";
92 // Favicon
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 -->";
127 if ($echoOutput) {
128 echo $output;
129 } else {
130 return $output;
134 public static function getFavIcon()
136 $logoService = new LogoService();
137 $icon = $logoService->getLogo("core/favicon/", "favicon.ico");
138 return $icon;
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)
153 if ($headerMode) {
154 self::$isHeader = true;
155 } else {
156 self::$isHeader = false;
159 try {
160 if ($echoOutput) {
161 echo self::includeAsset($assets);
162 } else {
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
181 * @return string
183 private static function includeAsset($assets = [])
186 if (is_string($assets)) {
187 $assets = [$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");
192 self::$scripts = [];
193 self::$links = [];
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
213 * @return void
215 private static function parseConfigFile($map, $selectedAssets = array())
217 $foundAssets = [];
218 $excludedCount = 0;
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)) {
229 $excludedCount++;
230 continue;
233 $foundAssets[] = $k;
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
246 } else {
247 foreach ($tmp['links'] as $l) {
248 self::$links[] = $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'])) {
259 } else {
260 foreach ($tmpRtl['links'] as $l) {
261 self::$links[] = $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);
288 $scripts = [];
289 $links = [];
291 if ($script) {
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)) {
297 $script = [$script];
300 foreach ($script as $k) {
301 $k = self::parsePlaceholders($k);
302 if ($alreadyBuilt) {
303 $path = $k;
304 } else {
305 $path = self::createFullPath($basePath, $k);
307 $scripts[] = self::createElement($path, 'script', $alreadyBuilt);
311 if ($link) {
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)) {
317 $link = [$link];
320 foreach ($link as $l) {
321 $l = self::parsePlaceholders($l);
322 if ($alreadyBuilt) {
323 $path = $l;
324 } else {
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)
346 $re = '/%(.*)%/';
347 $matches = [];
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);
356 return $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}";
378 } else {
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)
405 try {
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"], '?'));