1<?php
2
3
4namespace ComboStrap;
5
6
7use dokuwiki\Extension\Plugin;
8use dokuwiki\Extension\SyntaxPlugin;
9
10require_once(__DIR__ . '/../vendor/autoload.php');
11
12/**
13 * Parent in th hierarchy should be first
14 * Ie before {@link ImageLink, SvgImageLink, RasterImageLink)
15 */
16require_once(__DIR__ . '/DokuPath.php');
17require_once(__DIR__ . '/Media.php');
18require_once(__DIR__ . '/MediaLink.php');
19
20/**
21 * Plugin Utility is added in all Dokuwiki extension
22 * and
23 * all classes are added in plugin utility
24 *
25 * This is an utility master and the class loader
26 *
27 * If the load is relative, the load path is used
28 * and the bad php file may be loaded
29 * Furthermore, the absolute path helps
30 * the IDE when refactoring
31 */
32require_once(__DIR__ . '/AdsUtility.php');
33require_once(__DIR__ . '/Align.php');
34require_once(__DIR__ . '/Analytics.php');
35require_once(__DIR__ . '/AnalyticsMenuItem.php');
36require_once(__DIR__ . '/Animation.php');
37require_once(__DIR__ . '/ArrayCaseInsensitive.php');
38require_once(__DIR__ . '/ArrayUtility.php');
39require_once(__DIR__ . '/Background.php');
40require_once(__DIR__ . '/Boldness.php');
41require_once(__DIR__ . '/Bootstrap.php');
42require_once(__DIR__ . '/BreadcrumbHierarchical.php');
43require_once(__DIR__ . '/CacheByLogicalKey.php');
44require_once(__DIR__ . '/CacheInstructionsByLogicalKey.php');
45require_once(__DIR__ . '/CacheManager.php');
46require_once(__DIR__ . '/CacheMedia.php');
47require_once(__DIR__ . '/Call.php');
48require_once(__DIR__ . '/CallStack.php');
49require_once(__DIR__ . '/ColorUtility.php');
50require_once(__DIR__ . '/ConditionalValue.php');
51require_once(__DIR__ . '/ConfUtility.php');
52require_once(__DIR__ . '/Dimension.php');
53require_once(__DIR__ . '/DokuwikiUrl.php');
54require_once(__DIR__ . '/ExitException.php');
55require_once(__DIR__ . '/File.php');
56require_once(__DIR__ . '/FloatAttribute.php');
57require_once(__DIR__ . '/FontSize.php');
58require_once(__DIR__ . '/FsWikiUtility.php');
59require_once(__DIR__ . '/HeaderUtility.php');
60require_once(__DIR__ . '/HistoricalBreadcrumbMenuItem.php');
61require_once(__DIR__ . '/Hover.php');
62require_once(__DIR__ . '/Http.php');
63require_once(__DIR__ . '/Icon.php');
64require_once(__DIR__ . '/Identity.php');
65require_once(__DIR__ . '/Image.php');
66require_once(__DIR__ . '/ImageLink.php');
67require_once(__DIR__ . '/ImageRaster.php');
68require_once(__DIR__ . '/ImageSvg.php');
69require_once(__DIR__ . '/Iso8601Date.php');
70require_once(__DIR__ . '/Json.php');
71require_once(__DIR__ . '/Lang.php');
72require_once(__DIR__ . '/LineSpacing.php');
73require_once(__DIR__ . '/LogException.php');
74require_once(__DIR__ . '/LogUtility.php');
75require_once(__DIR__ . '/LowQualityPage.php');
76require_once(__DIR__ . '/MetadataUtility.php');
77require_once(__DIR__ . '/Message.php');
78require_once(__DIR__ . '/Mermaid.php');
79require_once(__DIR__ . '/NavBarUtility.php');
80require_once(__DIR__ . '/Opacity.php');
81require_once(__DIR__ . '/Os.php');
82require_once(__DIR__ . '/Page.php');
83require_once(__DIR__ . '/PageProtection.php');
84require_once(__DIR__ . '/PageRules.php');
85require_once(__DIR__ . '/PageSql.php');
86require_once(__DIR__ . '/PageSqlParser/PageSqlLexer.php');
87require_once(__DIR__ . '/PageSqlParser/PageSqlParser.php');
88require_once(__DIR__ . '/PageSqlTreeListener.php');
89require_once(__DIR__ . '/PagesIndex.php');
90require_once(__DIR__ . '/PipelineUtility.php');
91require_once(__DIR__ . '/Position.php');
92require_once(__DIR__ . '/Prism.php');
93require_once(__DIR__ . '/Publication.php');
94require_once(__DIR__ . '/RasterImageLink.php');
95require_once(__DIR__ . '/RenderUtility.php');
96require_once(__DIR__ . '/Resources.php');
97require_once(__DIR__ . '/Sanitizer.php');
98require_once(__DIR__ . '/Shadow.php');
99require_once(__DIR__ . '/Site.php');
100require_once(__DIR__ . '/Skin.php');
101require_once(__DIR__ . '/Snippet.php');
102require_once(__DIR__ . '/SnippetManager.php');
103require_once(__DIR__ . '/Spacing.php');
104require_once(__DIR__ . '/Sqlite.php');
105require_once(__DIR__ . '/StringUtility.php');
106require_once(__DIR__ . '/StyleUtility.php');
107require_once(__DIR__ . '/SvgDocument.php');
108require_once(__DIR__ . '/SvgImageLink.php');
109require_once(__DIR__ . '/Syntax.php');
110require_once(__DIR__ . '/TableUtility.php');
111require_once(__DIR__ . '/Tag.php');
112require_once(__DIR__ . '/TagAttributes.php');
113require_once(__DIR__ . '/Template.php');
114require_once(__DIR__ . '/TemplateUtility.php');
115require_once(__DIR__ . '/TextAlign.php');
116require_once(__DIR__ . '/TextColor.php');
117require_once(__DIR__ . '/ThirdMediaLink.php');
118require_once(__DIR__ . '/ThirdPartyPlugins.php');
119require_once(__DIR__ . '/TocUtility.php');
120require_once(__DIR__ . '/Toggle.php');
121require_once(__DIR__ . '/Underline.php');
122require_once(__DIR__ . '/Unit.php');
123require_once(__DIR__ . '/UrlManagerBestEndPage.php');
124require_once(__DIR__ . '/UrlUtility.php');
125require_once(__DIR__ . '/XhtmlUtility.php');
126require_once(__DIR__ . '/XmlDocument.php');
127require_once(__DIR__ . '/XmlUtility.php');
128
129
130/**
131 * Class url static
132 * List of static utilities
133 */
134class PluginUtility
135{
136
137    const DOKU_DATA_DIR = '/dokudata/pages';
138    const DOKU_CACHE_DIR = '/dokudata/cache';
139
140    /**
141     * Key in the data array between the handle and render function
142     */
143    const STATE = "state";
144    const PAYLOAD = "payload"; // The html or text
145    const ATTRIBUTES = "attributes";
146    // The context is generally the parent tag but it may be also the grandfather.
147    // It permits to determine the HTML that is outputted
148    const CONTEXT = 'context';
149    const TAG = "tag";
150
151    /**
152     * The name of the hidden/private namespace
153     * where the icon and other artifactory are stored
154     */
155    const COMBOSTRAP_NAMESPACE_NAME = "combostrap";
156
157    const PARENT = "parent";
158    const POSITION = "position";
159
160    /**
161     * Class to center an element
162     */
163    const CENTER_CLASS = "mx-auto";
164
165
166    const EDIT_SECTION_TARGET = 'section';
167    const ERROR_MESSAGE = "errorAtt";
168    const ERROR_LEVEL = "errorLevel";
169    const DISPLAY = "display";
170
171    /**
172     * The URL base of the documentation
173     */
174    static $URL_BASE;
175
176
177    /**
178     * @var string - the plugin base name (ie the directory)
179     * ie $INFO_PLUGIN['base'];
180     * This is a constant because it permits code analytics
181     * such as verification of a path
182     */
183    const PLUGIN_BASE_NAME = "combo";
184
185    /**
186     * The name of the template plugin
187     */
188    const TEMPLATE_STRAP_NAME = "strap";
189
190    /**
191     * @var array
192     */
193    static $INFO_PLUGIN;
194
195    static $PLUGIN_LANG;
196
197    /**
198     * The plugin name
199     * (not the same than the base as it's not related to the directory
200     * @var string
201     */
202    public static $PLUGIN_NAME;
203    /**
204     * @var mixed the version
205     */
206    private static $VERSION;
207
208
209    /**
210     * Initiate the static variable
211     * See the call after this class
212     */
213    static function init()
214    {
215
216        $pluginInfoFile = __DIR__ . '/../plugin.info.txt';
217        self::$INFO_PLUGIN = confToHash($pluginInfoFile);
218        self::$PLUGIN_NAME = 'ComboStrap';
219        global $lang;
220        self::$PLUGIN_LANG = $lang[self::PLUGIN_BASE_NAME];
221        self::$URL_BASE = "https://" . parse_url(self::$INFO_PLUGIN['url'], PHP_URL_HOST);
222        self::$VERSION = self::$INFO_PLUGIN['version'];
223
224        PluginUtility::initStaticManager();
225
226    }
227
228    /**
229     * @param $inputExpression
230     * @return false|int 1|0
231     * returns:
232     *    - 1 if the input expression is a pattern,
233     *    - 0 if not,
234     *    - FALSE if an error occurred.
235     */
236    static function isRegularExpression($inputExpression)
237    {
238
239        $regularExpressionPattern = "/(\\/.*\\/[gmixXsuUAJ]?)/";
240        return preg_match($regularExpressionPattern, $inputExpression);
241
242    }
243
244    /**
245     * Return a mode from a tag (ie from a {@link Plugin::getPluginComponent()}
246     * @param $tag
247     * @return string
248     *
249     * A mode is just a name for a class
250     * Example: $Parser->addMode('listblock',new Doku_Parser_Mode_ListBlock());
251     */
252    public static function getModeFromTag($tag)
253    {
254        return "plugin_" . self::getComponentName($tag);
255    }
256
257    /**
258     * @param $tag
259     * @return string
260     *
261     * Create a lookahead pattern for a container tag used to enter in a mode
262     */
263    public static function getContainerTagPattern($tag)
264    {
265        // this pattern ensure that the tag
266        // `accordion` will not intercept also the tag `accordionitem`
267        // where:
268        // ?: means non capturing group (to not capture the last >)
269        // (\s.*?): is a capturing group that starts with a space
270        $pattern = "(?:\s.*?>|>)";
271        return '<' . $tag . $pattern . '(?=.*?<\/' . $tag . '>)';
272    }
273
274    /**
275     * This pattern allows space after the tag name
276     * for an end tag
277     * As XHTML (https://www.w3.org/TR/REC-xml/#dt-etag)
278     * @param $tag
279     * @return string
280     */
281    public static function getEndTagPattern($tag)
282    {
283        return "</$tag\s*>";
284    }
285
286    /**
287     * @param $tag
288     * @return string
289     *
290     * Create a open tag pattern without lookahead.
291     * Used for https://dev.w3.org/html5/html-author/#void-elements-0
292     */
293    public static function getVoidElementTagPattern($tag)
294    {
295        return '<' . $tag . '.*?>';
296    }
297
298
299    /**
300     * Take an array  where the key is the attribute name
301     * and return a HTML tag string
302     *
303     * The attribute name and value are escaped
304     *
305     * @param $attributes - combo attributes
306     * @return string
307     * @deprecated to allowed background and other metadata, use {@link TagAttributes::toHtmlEnterTag()}
308     */
309    public static function array2HTMLAttributesAsString($attributes)
310    {
311
312        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
313        return $tagAttributes->toHTMLAttributeString();
314
315    }
316
317    /**
318     *
319     * Parse the attributes part of a match
320     *
321     * Example:
322     *   line-numbers="value"
323     *   line-numbers='value'
324     *
325     * This value may be in:
326     *   * configuration value
327     *   * as well as in the match of a {@link SyntaxPlugin}
328     *
329     * @param $string
330     * @return array
331     *
332     * To parse a match, use {@link PluginUtility::getTagAttributes()}
333     *
334     *
335     */
336    public static function parseAttributes($string)
337    {
338
339        $parameters = array();
340
341        // Rules
342        //  * name may be alone (ie true boolean attribute)
343        //  * a name may get a `-`
344        //  * there may be space every everywhere when the value is enclosed with a quote
345        //  * there may be no space in the value and between the equal sign when the value is not enclosed
346        //
347        // /i not case sensitive
348        $attributePattern = '\s*([-\w]+)\s*(?:=(\s*[\'"]([^`"]*)[\'"]\s*|[^\s]*))?';
349        $result = preg_match_all('/' . $attributePattern . '/i', $string, $matches);
350        if ($result != 0) {
351            foreach ($matches[1] as $key => $parameterKey) {
352
353                // group 3 (ie the value between quotes)
354                $value = $matches[3][$key];
355                if ($value == "") {
356                    // check the value without quotes
357                    $value = $matches[2][$key];
358                }
359                // if there is no value, this is a boolean
360                if ($value == "") {
361                    $value = true;
362                } else {
363                    $value = hsc($value);
364                }
365                $parameters[hsc(strtolower($parameterKey))] = $value;
366            }
367        }
368        return $parameters;
369
370    }
371
372    public static function getTagAttributes($match)
373    {
374        return self::getQualifiedTagAttributes($match, false, "");
375    }
376
377    /**
378     * Return the attribute of a tag
379     * Because they are users input, they are all escaped
380     * @param $match
381     * @param $hasThirdValue - if true, the third parameter is treated as value, not a property and returned in the `third` key
382     * use for the code/file/console where they accept a name as third value
383     * @param $keyThirdArgument - if a third argument is found, return it with this key
384     * @return array
385     */
386    public static function getQualifiedTagAttributes($match, $hasThirdValue, $keyThirdArgument)
387    {
388
389        $match = PluginUtility::getPreprocessEnterTag($match);
390
391        // Suppress the tag name (ie until the first blank)
392        $spacePosition = strpos($match, " ");
393        if (!$spacePosition) {
394            // No space, meaning this is only the tag name
395            return array();
396        }
397        $match = trim(substr($match, $spacePosition));
398        if ($match == "") {
399            return array();
400        }
401
402        // Do we have a type as first argument ?
403        $attributes = array();
404        $spacePosition = strpos($match, " ");
405        if ($spacePosition) {
406            $nextArgument = substr($match, 0, $spacePosition);
407        } else {
408            $nextArgument = $match;
409        }
410        if (!strpos($nextArgument, "=")) {
411            $attributes["type"] = $nextArgument;
412            // Suppress the type
413            $match = substr($match, strlen($nextArgument));
414            $match = trim($match);
415
416            // Do we have a value as first argument ?
417            if (!empty($hasThirdValue)) {
418                $spacePosition = strpos($match, " ");
419                if ($spacePosition) {
420                    $nextArgument = substr($match, 0, $spacePosition);
421                } else {
422                    $nextArgument = $match;
423                }
424                if (!strpos($nextArgument, "=") && !empty($nextArgument)) {
425                    $attributes[$keyThirdArgument] = $nextArgument;
426                    // Suppress the third argument
427                    $match = substr($match, strlen($nextArgument));
428                    $match = trim($match);
429                }
430            }
431        }
432
433        // Parse the remaining attributes
434        $parsedAttributes = self::parseAttributes($match);
435
436        // Merge
437        $attributes = array_merge($attributes, $parsedAttributes);;
438
439        return $attributes;
440
441    }
442
443    /**
444     * @param array $styleProperties - an array of CSS properties with key, value
445     * @return string - the value for the style attribute (ie all rules where joined with the comma)
446     */
447    public static function array2InlineStyle(array $styleProperties)
448    {
449        $inlineCss = "";
450        foreach ($styleProperties as $key => $value) {
451            $inlineCss .= "$key:$value;";
452        }
453        // Suppress the last ;
454        if ($inlineCss[strlen($inlineCss) - 1] == ";") {
455            $inlineCss = substr($inlineCss, 0, -1);
456        }
457        return $inlineCss;
458    }
459
460    /**
461     * @param $tag
462     * @return string
463     * Create a pattern used where the tag is not a container.
464     * ie
465     * <br/>
466     * <icon/>
467     * This is generally used with a subtition plugin
468     * and a {@link Lexer::addSpecialPattern} state
469     * where the tag is just replaced
470     */
471    public static function getEmptyTagPattern($tag): string
472    {
473
474        return '<' . $tag . '[^>]*/>';
475    }
476
477    /**
478     * Just call this function from a class like that
479     *     getTageName(get_called_class())
480     * to get the tag name (ie the component plugin)
481     * of a syntax plugin
482     *
483     * @param $get_called_class
484     * @return string
485     */
486    public static function getTagName($get_called_class)
487    {
488        list(/* $t */, /* $p */, /* $n */, $c) = explode('_', $get_called_class, 4);
489        return (isset($c) ? $c : '');
490    }
491
492    /**
493     * Just call this function from a class like that
494     *     getAdminPageName(get_called_class())
495     * to get the page name of a admin plugin
496     *
497     * @param $get_called_class
498     * @return string - the admin page name
499     */
500    public static function getAdminPageName($get_called_class)
501    {
502        $names = explode('_', $get_called_class);
503        $names = array_slice($names, -2);
504        return implode('_', $names);
505    }
506
507    public static function getNameSpace()
508    {
509        // No : at the begin of the namespace please
510        return self::PLUGIN_BASE_NAME . ':';
511    }
512
513    /**
514     * @param $get_called_class - the plugin class
515     * @return array
516     */
517    public static function getTags($get_called_class)
518    {
519        $elements = array();
520        $elementName = PluginUtility::getTagName($get_called_class);
521        $elements[] = $elementName;
522        $elements[] = strtoupper($elementName);
523        return $elements;
524    }
525
526    /**
527     * Render a text
528     * @param $pageContent
529     * @return string|null
530     */
531    public static function render($pageContent)
532    {
533        return RenderUtility::renderText2XhtmlAndStripPEventually($pageContent, false);
534    }
535
536
537    /**
538     * This method will takes attributes
539     * and process the plugin styling attribute such as width and height
540     * to put them in a style HTML attribute
541     * @param TagAttributes $attributes
542     */
543    public static function processStyle(&$attributes)
544    {
545        // Style
546        $styleAttributeName = "style";
547        if ($attributes->hasComponentAttribute($styleAttributeName)) {
548            $properties = explode(";", $attributes->getValueAndRemove($styleAttributeName));
549            foreach ($properties as $property) {
550                list($key, $value) = explode(":", $property);
551                if ($key != "") {
552                    $attributes->addStyleDeclaration($key, $value);
553                }
554            }
555        }
556
557
558        /**
559         * Border Color
560         * For background color, see {@link TagAttributes::processBackground()}
561         * For text color, see {@link TextColor}
562         */
563
564        if ($attributes->hasComponentAttribute(ColorUtility::BORDER_COLOR)) {
565            $colorValue = $attributes->getValueAndRemove(ColorUtility::BORDER_COLOR);
566            $attributes->addStyleDeclaration(ColorUtility::BORDER_COLOR, ColorUtility::getColorValue($colorValue));
567            self::checkDefaultBorderColorAttributes($attributes);
568        }
569
570
571    }
572
573    /**
574     * Return the name of the requested script
575     */
576    public
577    static function getRequestScript()
578    {
579        $scriptPath = null;
580        $testPropertyValue = self::getPropertyValue("SCRIPT_NAME");
581        if (defined('DOKU_UNITTEST') && $testPropertyValue != null) {
582            return $testPropertyValue;
583        }
584        if (array_key_exists("DOCUMENT_URI", $_SERVER)) {
585            $scriptPath = $_SERVER["DOCUMENT_URI"];
586        }
587        if ($scriptPath == null && array_key_exists("SCRIPT_NAME", $_SERVER)) {
588            $scriptPath = $_SERVER["SCRIPT_NAME"];
589        }
590        if ($scriptPath == null) {
591            msg("Unable to find the main script", LogUtility::LVL_MSG_ERROR);
592        }
593        $path_parts = pathinfo($scriptPath);
594        return $path_parts['basename'];
595    }
596
597    /**
598     *
599     * @param $name
600     * @param $default
601     * @return string - the value of a query string property or if in test mode, the value of a test variable
602     * set with {@link self::setTestProperty}
603     * This is used to test script that are not supported by the dokuwiki test framework
604     * such as css.php
605     */
606    public
607    static function getPropertyValue($name, $default = null)
608    {
609        global $INPUT;
610        $value = $INPUT->str($name);
611        if ($value == null && defined('DOKU_UNITTEST')) {
612            global $COMBO;
613            $value = $COMBO[$name];
614        }
615        if ($value == null) {
616            return $default;
617        } else {
618            return $value;
619        }
620
621    }
622
623    /**
624     * Create an URL to the documentation website
625     * @param $canonical - canonical id or slug
626     * @param $text -  the text of the link
627     * @param bool $withIcon - used to break the recursion with the message in the {@link Icon}
628     * @return string - an url
629     */
630    public
631    static function getUrl($canonical, $text, $withIcon = true)
632    {
633        /** @noinspection SpellCheckingInspection */
634
635        $xhtmlIcon = "";
636        if ($withIcon) {
637
638            /**
639             * We don't include it as an external resource via url
640             * because it then make a http request for every logo
641             * in the configuration page and makes it really slow
642             */
643            $path = File::createFromPath(Resources::getImagesDirectory() . "/logo.svg");
644            $tagAttributes = TagAttributes::createEmpty(SvgImageLink::CANONICAL);
645            $tagAttributes->addComponentAttributeValue(TagAttributes::TYPE_KEY, SvgDocument::ICON_TYPE);
646            $tagAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, "20");
647            $cache = new CacheMedia($path, $tagAttributes);
648            if (!$cache->isCacheUsable()) {
649                $xhtmlIcon = SvgDocument::createFromPath($path)
650                    ->setShouldBeOptimized(true)
651                    ->getXmlText($tagAttributes);
652                $cache->storeCache($xhtmlIcon);
653            }
654            $xhtmlIcon = file_get_contents($cache->getFile()->getFileSystemPath());
655
656
657        }
658        return $xhtmlIcon . ' <a href="' . self::$URL_BASE . '/' . str_replace(":", "/", $canonical) . '" title="' . $text . '">' . $text . '</a>';
659    }
660
661    /**
662     * An utility function to not search every time which array should be first
663     * @param array $inlineAttributes - the component inline attributes
664     * @param array $defaultAttributes - the default configuration attributes
665     * @return array - a merged array
666     */
667    public
668    static function mergeAttributes(array $inlineAttributes, array $defaultAttributes = array())
669    {
670        return array_merge($defaultAttributes, $inlineAttributes);
671    }
672
673    /**
674     * A pattern for a container tag
675     * that needs to catch the content
676     *
677     * Use as a special pattern (substition)
678     *
679     * The {@link \syntax_plugin_combo_math} use it
680     * @param $tag
681     * @return string - a pattern
682     */
683    public
684    static function getLeafContainerTagPattern($tag)
685    {
686        return '<' . $tag . '.*?>.*?<\/' . $tag . '>';
687    }
688
689    /**
690     * Return the content of a tag
691     * <math>Content</math>
692     * @param $match
693     * @return string the content
694     */
695    public
696    static function getTagContent($match)
697    {
698        // From the first >
699        $start = strpos($match, ">");
700        if ($start == false) {
701            LogUtility::msg("The match does not contain any opening tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
702            return "";
703        }
704        $match = substr($match, $start + 1);
705        // If this is the last character, we get a false
706        if ($match == false) {
707            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
708            return "";
709        }
710
711        $end = strrpos($match, "</");
712        if ($end == false) {
713            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
714            return "";
715        }
716
717        return substr($match, 0, $end);
718    }
719
720    /**
721     *
722     * Check if a HTML tag was already added for a request
723     * The request id is just the timestamp
724     * An indicator array should be provided
725     * @return string
726     */
727    public
728    static function getRequestId()
729    {
730
731        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
732            // since php 5.4
733            $requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
734        } else {
735            // DokuWiki test framework use this
736            $requestTime = $_SERVER['REQUEST_TIME'];
737        }
738        $keyPrefix = 'combo_';
739
740        global $ID;
741        return $keyPrefix . hash('crc32b', $_SERVER['REMOTE_ADDR'] . $_SERVER['REMOTE_PORT'] . $requestTime . $ID);
742
743    }
744
745    /**
746     * Get the page id
747     * If the page is a sidebar, it will not return the id of the sidebar
748     * but the one of the page
749     * @return string
750     */
751    public
752    static function getPageId()
753    {
754        return FsWikiUtility::getMainPageId();
755    }
756
757    /**
758     * Transform special HTML characters to entity
759     * Example:
760     * <hello>world</hello>
761     * to
762     * "&lt;hello&gt;world&lt;/hello&gt;"
763     *
764     * @param $text
765     * @return string
766     */
767    public
768    static function htmlEncode($text)
769    {
770        /**
771         * See https://stackoverflow.com/questions/46483/htmlentities-vs-htmlspecialchars/3614344
772         * {@link htmlentities }
773         */
774        //return htmlspecialchars($text, ENT_QUOTES);
775        return htmlentities($text);
776    }
777
778
779    /**
780     * Add a class
781     * @param $classValue
782     * @param array $attributes
783     */
784    public
785    static function addClass2Attributes($classValue, array &$attributes)
786    {
787        self::addAttributeValue("class", $classValue, $attributes);
788    }
789
790    /**
791     * Add a style property to the attributes
792     * @param $property
793     * @param $value
794     * @param array $attributes
795     * @deprecated use {@link TagAttributes::addStyleDeclaration()} instead
796     */
797    public
798    static function addStyleProperty($property, $value, array &$attributes)
799    {
800        if (isset($attributes["style"])) {
801            $attributes["style"] .= ";$property:$value";
802        } else {
803            $attributes["style"] = "$property:$value";
804        }
805
806    }
807
808    /**
809     * Add default border attributes
810     * to see a border
811     * Doc
812     * https://combostrap.com/styling/color#border_color
813     * @param TagAttributes $tagAttributes
814     */
815    private
816    static function checkDefaultBorderColorAttributes(&$tagAttributes)
817    {
818        /**
819         * border color was set without the width
820         * setting the width
821         */
822        if (!(
823            $tagAttributes->hasStyleDeclaration("border")
824            ||
825            $tagAttributes->hasStyleDeclaration("border-width")
826        )
827        ) {
828            $tagAttributes->addStyleDeclaration("border-width", "1px");
829        }
830        /**
831         * border color was set without the style
832         * setting the style
833         */
834        if (!
835        (
836            $tagAttributes->hasStyleDeclaration("border")
837            ||
838            $tagAttributes->hasStyleDeclaration("border-style")
839        )
840        ) {
841            $tagAttributes->addStyleDeclaration("border-style", "solid");
842
843        }
844        if (!$tagAttributes->hasStyleDeclaration("border-radius")) {
845            $tagAttributes->addStyleDeclaration("border-radius", ".25rem");
846        }
847
848    }
849
850    public
851    static function getConfValue($confName, $defaultValue = null)
852    {
853        global $conf;
854        if (isset($conf['plugin'][PluginUtility::PLUGIN_BASE_NAME][$confName])) {
855            return $conf['plugin'][PluginUtility::PLUGIN_BASE_NAME][$confName];
856        } else {
857            return $defaultValue;
858        }
859    }
860
861    /**
862     * @param $match
863     * @return null|string - return the tag name or null if not found
864     */
865    public
866    static function getTag($match)
867    {
868
869        // Trim to start clean
870        $match = trim($match);
871
872        // Until the first >
873        $pos = strpos($match, ">");
874        if ($pos == false) {
875            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
876            return null;
877        }
878        $match = substr($match, 0, $pos);
879
880        // Suppress the <
881        if ($match[0] == "<") {
882            $match = substr($match, 1);
883        } else {
884            LogUtility::msg("This is not a text tag because it does not start with the character `>`");
885        }
886
887        // Suppress the tag name (ie until the first blank)
888        $spacePosition = strpos($match, " ");
889        if (!$spacePosition) {
890            // No space, meaning this is only the tag name
891            return $match;
892        } else {
893            return substr($match, 0, $spacePosition);
894        }
895
896    }
897
898
899    /**
900     * @param string $string add a command into HTML
901     */
902    public
903    static function addAsHtmlComment($string)
904    {
905        print_r('<!-- ' . self::htmlEncode($string) . '-->');
906    }
907
908    public
909    static function getResourceBaseUrl()
910    {
911        return DOKU_URL . 'lib/plugins/' . PluginUtility::PLUGIN_BASE_NAME . '/resources';
912    }
913
914    /**
915     * @param $TAG - the name of the tag that should correspond to the name of the css file in the style directory
916     * @return string - a inline style element to inject in the page or blank if no file exists
917     */
918    public
919    static function getTagStyle($TAG)
920    {
921        $script = self::getCssRules($TAG);
922        if (!empty($script)) {
923            return "<style>" . $script . "</style>";
924        } else {
925            return "";
926        }
927
928    }
929
930
931    public
932    static function getComponentName($tag)
933    {
934        return strtolower(PluginUtility::PLUGIN_BASE_NAME) . "_" . $tag;
935    }
936
937    public
938    static function addAttributeValue($attribute, $value, array &$attributes)
939    {
940        if (array_key_exists($attribute, $attributes) && $attributes[$attribute] !== "") {
941            $attributes[$attribute] .= " {$value}";
942        } else {
943            $attributes[$attribute] = "{$value}";
944        }
945    }
946
947    /**
948     * Plugin Utility is available to all plugin,
949     * this is a convenient way to the the snippet manager
950     * @return SnippetManager
951     */
952    public
953    static function getSnippetManager()
954    {
955        return SnippetManager::get();
956    }
957
958    public
959    static function initStaticManager()
960    {
961        CacheManager::init();
962        SnippetManager::init();
963    }
964
965    /**
966     * Function used in a render
967     * @param $data - the data from {@link PluginUtility::handleAndReturnUnmatchedData()}
968     * @return string
969     */
970    public
971    static function renderUnmatched($data)
972    {
973        /**
974         * Attributes
975         */
976        if (isset($data[PluginUtility::ATTRIBUTES])) {
977            $attributes = $data[PluginUtility::ATTRIBUTES];
978        } else {
979            $attributes = [];
980        }
981        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
982        $display = $tagAttributes->getValue(TagAttributes::DISPLAY);
983        if ($display != "none") {
984            $payload = $data[self::PAYLOAD];
985            $previousTagDisplayType = $data[self::CONTEXT];
986            if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
987                $payload = ltrim($payload);
988            }
989            return PluginUtility::htmlEncode($payload);
990        } else {
991            return "";
992        }
993    }
994
995    /**
996     * Function used in a handle function of a syntax plugin for
997     * unmatched context
998     * @param $tagName
999     * @param $match
1000     * @param \Doku_Handler $handler
1001     * @return array
1002     */
1003    public
1004    static function handleAndReturnUnmatchedData($tagName, $match, \Doku_Handler $handler): array
1005    {
1006        $callStack = CallStack::createFromHandler($handler);
1007        $sibling = $callStack->previous();
1008        $context = null;
1009        if (!empty($sibling)) {
1010            $context = $sibling->getDisplay();
1011        }
1012        return array(
1013            PluginUtility::STATE => DOKU_LEXER_UNMATCHED,
1014            PluginUtility::PAYLOAD => $match,
1015            PluginUtility::CONTEXT => $context
1016        );
1017    }
1018
1019    public
1020    static function setConf($key, $value, $namespace = 'plugin')
1021    {
1022        global $conf;
1023        if ($namespace != null) {
1024            $conf[$namespace][PluginUtility::PLUGIN_BASE_NAME][$key] = $value;
1025        } else {
1026            $conf[$key] = $value;
1027        }
1028
1029    }
1030
1031    /**
1032     * Utility methodPreprocess a start tag to be able to extract the name
1033     * and the attributes easily
1034     *
1035     * It will delete:
1036     *   * the characters <> and the /> if present
1037     *   * and trim
1038     *
1039     * It will remain the tagname and its attributes
1040     * @param $match
1041     * @return false|string|null
1042     */
1043    private
1044    static function getPreprocessEnterTag($match)
1045    {
1046        // Until the first >
1047        $pos = strpos($match, ">");
1048        if ($pos == false) {
1049            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_WARNING);
1050            return null;
1051        }
1052        $match = substr($match, 0, $pos);
1053
1054
1055        // Trim to start clean
1056        $match = trim($match);
1057
1058        // Suppress the <
1059        if ($match[0] == "<") {
1060            $match = substr($match, 1);
1061        }
1062
1063        // Suppress the / for a leaf tag
1064        if ($match[strlen($match) - 1] == "/") {
1065            $match = substr($match, 0, strlen($match) - 1);
1066        }
1067        return $match;
1068    }
1069
1070    /**
1071     * Retrieve the tag name used in the text document
1072     * @param $match
1073     * @return false|string|null
1074     */
1075    public
1076    static function getSyntaxTagNameFromMatch($match)
1077    {
1078        $preprocessMatch = PluginUtility::getPreprocessEnterTag($match);
1079
1080        // Tag name (ie until the first blank)
1081        $spacePosition = strpos($match, " ");
1082        if (!$spacePosition) {
1083            // No space, meaning this is only the tag name
1084            return $preprocessMatch;
1085        } else {
1086            return trim(substr(0, $spacePosition));
1087        }
1088
1089    }
1090
1091    /**
1092     * @param \Doku_Renderer_xhtml $renderer
1093     * @param $position
1094     * @param $name
1095     */
1096    public
1097    static function startSection($renderer, $position, $name)
1098    {
1099
1100
1101        if (empty($position)) {
1102            LogUtility::msg("The position for a start section should not be empty", LogUtility::LVL_MSG_ERROR, "support");
1103        }
1104        if (empty($name)) {
1105            LogUtility::msg("The name for a start section should not be empty", LogUtility::LVL_MSG_ERROR, "support");
1106        }
1107
1108        /**
1109         * New Dokuwiki Version
1110         * for DokuWiki Greebo and more recent versions
1111         */
1112        if (defined('SEC_EDIT_PATTERN')) {
1113            $renderer->startSectionEdit($position, array('target' => self::EDIT_SECTION_TARGET, 'name' => $name));
1114        } else {
1115            /**
1116             * Old version
1117             */
1118            /** @noinspection PhpParamsInspection */
1119            $renderer->startSectionEdit($position, self::EDIT_SECTION_TARGET, $name);
1120        }
1121    }
1122
1123    /**
1124     * Add an enter call to the stack
1125     * @param \Doku_Handler $handler
1126     * @param $tagName
1127     * @param array $callStackArray
1128     */
1129    public
1130    static function addEnterCall(
1131        \Doku_Handler &$handler,
1132        $tagName,
1133        $callStackArray = array()
1134    )
1135    {
1136        $pluginName = PluginUtility::getComponentName($tagName);
1137        $handler->addPluginCall(
1138            $pluginName,
1139            $callStackArray,
1140            DOKU_LEXER_ENTER,
1141            null,
1142            null
1143        );
1144    }
1145
1146    /**
1147     * Add an end call dynamically
1148     * @param \Doku_Handler $handler
1149     * @param $tagName
1150     * @param array $callStackArray
1151     */
1152    public
1153    static function addEndCall(\Doku_Handler $handler, $tagName, $callStackArray = array())
1154    {
1155        $pluginName = PluginUtility::getComponentName($tagName);
1156        $handler->addPluginCall(
1157            $pluginName,
1158            $callStackArray,
1159            DOKU_LEXER_END,
1160            null,
1161            null
1162        );
1163    }
1164
1165    /**
1166     * General Debug
1167     */
1168    public
1169    static function isDebug()
1170    {
1171        global $conf;
1172        return $conf["allowdebug"] === 1;
1173
1174    }
1175
1176    /**
1177     * @return bool true if loaded, false otherwise
1178     * Strap is loaded only if this is the same version
1179     * to avoid function, class, or members that does not exist
1180     */
1181    public
1182    static function loadStrapUtilityTemplateIfPresentAndSameVersion()
1183    {
1184        $templateUtilityFile = __DIR__ . '/../../../tpl/strap/class/TplUtility.php';
1185        if (file_exists($templateUtilityFile)) {
1186            /**
1187             * Check the version
1188             */
1189            $templateInfo = confToHash(__DIR__ . '/../../../tpl/strap/template.info.txt');
1190            $templateVersion = $templateInfo['version'];
1191            $comboVersion = self::$INFO_PLUGIN['version'];
1192            if ($templateVersion != $comboVersion) {
1193                if ($comboVersion > $templateVersion) {
1194                    LogUtility::msg("You should upgrade <a href=\"https://www.dokuwiki.org/template:strap\">strap</a> to the latest version to get a fully functional experience. The version of Combo is ($comboVersion) while the version of Strap is ($templateVersion).");
1195                } else {
1196                    LogUtility::msg("You should upgrade <a href=\"https://www.dokuwiki.org/plugin:combo\">combo</a>  to the latest version to get a fully functional experience. The version of Combo is ($comboVersion) while the version of Strap is ($templateVersion).");
1197                }
1198                return false;
1199            } else {
1200                /** @noinspection PhpIncludeInspection */
1201                require_once($templateUtilityFile);
1202                return true;
1203            }
1204        } else {
1205            $level = LogUtility::LVL_MSG_DEBUG;
1206            if (defined('DOKU_UNITTEST')) {
1207                // fail
1208                $level = LogUtility::LVL_MSG_ERROR;
1209            }
1210            if (Site::getTemplate() != "strap") {
1211                LogUtility::msg("The strap template is not installed", $level);
1212            } else {
1213                LogUtility::msg("The file ($templateUtilityFile) was not found", $level);
1214            }
1215            return false;
1216        }
1217    }
1218
1219
1220    /**
1221     *
1222     * See also dev.md file
1223     */
1224    public static function isDevOrTest()
1225    {
1226        if (self::isDev()) {
1227            return true;
1228        }
1229        return self::isTest();
1230    }
1231
1232    public static function isDev()
1233    {
1234        global $_SERVER;
1235        if ($_SERVER["REMOTE_ADDR"] == "127.0.0.1") {
1236            return true;
1237        }
1238        return false;
1239    }
1240
1241    public static function getInstructions($markiCode)
1242    {
1243        return p_get_instructions($markiCode);
1244    }
1245
1246    public static function getInstructionsWithoutRoot($markiCode)
1247    {
1248        return RenderUtility::getInstructionsAndStripPEventually($markiCode);
1249    }
1250
1251    /**
1252     * Transform a text into a valid HTML id
1253     * @param $string
1254     * @return string
1255     */
1256    public static function toHtmlId($string)
1257    {
1258        /**
1259         * sectionId calls cleanID
1260         * cleanID delete all things before a ':'
1261         * we do then the replace before to not
1262         * lost a minus '-' separator
1263         */
1264        $string = str_replace(array(':', '.'), '', $string);
1265        return sectionID($string, $check);
1266    }
1267
1268    public static function isTest()
1269    {
1270        return defined('DOKU_UNITTEST');
1271    }
1272
1273
1274    public static function getCacheManager()
1275    {
1276        return CacheManager::get();
1277    }
1278
1279    public static function getModeFromPluginName($name)
1280    {
1281        return "plugin_$name";
1282    }
1283
1284    public static function isCi(): bool
1285    {
1286        // https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
1287        return getenv("CI") === "true";
1288    }
1289
1290    /**
1291     * An helper function to not exit when it's a test environment
1292     * @param string $message
1293     */
1294    public static function softExit($message = null)
1295    {
1296
1297        if (!PluginUtility::isTest()) {
1298            exit;
1299        } else {
1300            throw new ExitException($message);
1301        }
1302
1303    }
1304
1305
1306}
1307
1308PluginUtility::init();
1309