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