xref: /plugin/combo/ComboStrap/PluginUtility.php (revision c3437056399326d621a01da73b649707fbb0ae69)
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
1052    public
1053    static function getComponentName($tag)
1054    {
1055        return strtolower(PluginUtility::PLUGIN_BASE_NAME) . "_" . $tag;
1056    }
1057
1058    public
1059    static function addAttributeValue($attribute, $value, array &$attributes)
1060    {
1061        if (array_key_exists($attribute, $attributes) && $attributes[$attribute] !== "") {
1062            $attributes[$attribute] .= " {$value}";
1063        } else {
1064            $attributes[$attribute] = "{$value}";
1065        }
1066    }
1067
1068    /**
1069     * Plugin Utility is available to all plugin,
1070     * this is a convenient way to the the snippet manager
1071     * @return SnippetManager
1072     */
1073    public
1074    static function getSnippetManager(): SnippetManager
1075    {
1076        return SnippetManager::get();
1077    }
1078
1079
1080
1081    /**
1082     * Function used in a render
1083     * @param $data - the data from {@link PluginUtility::handleAndReturnUnmatchedData()}
1084     * @return string
1085     */
1086    public
1087    static function renderUnmatched($data)
1088    {
1089        /**
1090         * Attributes
1091         */
1092        if (isset($data[PluginUtility::ATTRIBUTES])) {
1093            $attributes = $data[PluginUtility::ATTRIBUTES];
1094        } else {
1095            $attributes = [];
1096        }
1097        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
1098        $display = $tagAttributes->getValue(TagAttributes::DISPLAY);
1099        if ($display != "none") {
1100            $payload = $data[self::PAYLOAD];
1101            $previousTagDisplayType = $data[self::CONTEXT];
1102            if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
1103                $payload = ltrim($payload);
1104            }
1105            return PluginUtility::htmlEncode($payload);
1106        } else {
1107            return "";
1108        }
1109    }
1110
1111    public
1112    static function renderUnmatchedXml($data)
1113    {
1114        $payload = $data[self::PAYLOAD];
1115        $previousTagDisplayType = $data[self::CONTEXT];
1116        if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
1117            $payload = ltrim($payload);
1118        }
1119        return PluginUtility::xmlEncode($payload);
1120
1121    }
1122
1123    /**
1124     * Function used in a handle function of a syntax plugin for
1125     * unmatched context
1126     * @param $tagName
1127     * @param $match
1128     * @param \Doku_Handler $handler
1129     * @return array
1130     */
1131    public
1132    static function handleAndReturnUnmatchedData($tagName, $match, \Doku_Handler $handler): array
1133    {
1134        $callStack = CallStack::createFromHandler($handler);
1135        $sibling = $callStack->previous();
1136        $context = null;
1137        if (!empty($sibling)) {
1138            $context = $sibling->getDisplay();
1139        }
1140        return array(
1141            PluginUtility::STATE => DOKU_LEXER_UNMATCHED,
1142            PluginUtility::PAYLOAD => $match,
1143            PluginUtility::CONTEXT => $context
1144        );
1145    }
1146
1147    public
1148    static function setConf($key, $value, $namespace = 'plugin')
1149    {
1150        global $conf;
1151        if ($namespace !== null) {
1152            $conf[$namespace][PluginUtility::PLUGIN_BASE_NAME][$key] = $value;
1153        } else {
1154            $conf[$key] = $value;
1155        }
1156
1157    }
1158
1159    /**
1160     * Utility methodPreprocess a start tag to be able to extract the name
1161     * and the attributes easily
1162     *
1163     * It will delete:
1164     *   * the characters <> and the /> if present
1165     *   * and trim
1166     *
1167     * It will remain the tagname and its attributes
1168     * @param $match
1169     * @return false|string|null
1170     */
1171    private
1172    static function getPreprocessEnterTag($match)
1173    {
1174        // Until the first >
1175        $pos = strpos($match, ">");
1176        if ($pos == false) {
1177            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_WARNING);
1178            return null;
1179        }
1180        $match = substr($match, 0, $pos);
1181
1182
1183        // Trim to start clean
1184        $match = trim($match);
1185
1186        // Suppress the <
1187        if ($match[0] == "<") {
1188            $match = substr($match, 1);
1189        }
1190
1191        // Suppress the / for a leaf tag
1192        if ($match[strlen($match) - 1] == "/") {
1193            $match = substr($match, 0, strlen($match) - 1);
1194        }
1195        return $match;
1196    }
1197
1198    /**
1199     * Retrieve the tag name used in the text document
1200     * @param $match
1201     * @return false|string|null
1202     */
1203    public
1204    static function getSyntaxTagNameFromMatch($match)
1205    {
1206        $preprocessMatch = PluginUtility::getPreprocessEnterTag($match);
1207
1208        // Tag name (ie until the first blank)
1209        $spacePosition = strpos($match, " ");
1210        if (!$spacePosition) {
1211            // No space, meaning this is only the tag name
1212            return $preprocessMatch;
1213        } else {
1214            return trim(substr(0, $spacePosition));
1215        }
1216
1217    }
1218
1219    /**
1220     * @param \Doku_Renderer_xhtml $renderer
1221     * @param $position
1222     * @param $name
1223     */
1224    public
1225    static function startSection($renderer, $position, $name)
1226    {
1227
1228
1229        if (empty($position)) {
1230            LogUtility::msg("The position for a start section should not be empty", LogUtility::LVL_MSG_ERROR, "support");
1231        }
1232        if (empty($name)) {
1233            LogUtility::msg("The name for a start section should not be empty", LogUtility::LVL_MSG_ERROR, "support");
1234        }
1235
1236        /**
1237         * New Dokuwiki Version
1238         * for DokuWiki Greebo and more recent versions
1239         */
1240        if (defined('SEC_EDIT_PATTERN')) {
1241            $renderer->startSectionEdit($position, array('target' => self::EDIT_SECTION_TARGET, 'name' => $name));
1242        } else {
1243            /**
1244             * Old version
1245             */
1246            /** @noinspection PhpParamsInspection */
1247            $renderer->startSectionEdit($position, self::EDIT_SECTION_TARGET, $name);
1248        }
1249    }
1250
1251    /**
1252     * Add an enter call to the stack
1253     * @param \Doku_Handler $handler
1254     * @param $tagName
1255     * @param array $callStackArray
1256     */
1257    public
1258    static function addEnterCall(
1259        \Doku_Handler &$handler,
1260        $tagName,
1261        $callStackArray = array()
1262    )
1263    {
1264        $pluginName = PluginUtility::getComponentName($tagName);
1265        $handler->addPluginCall(
1266            $pluginName,
1267            $callStackArray,
1268            DOKU_LEXER_ENTER,
1269            null,
1270            null
1271        );
1272    }
1273
1274    /**
1275     * Add an end call dynamically
1276     * @param \Doku_Handler $handler
1277     * @param $tagName
1278     * @param array $callStackArray
1279     */
1280    public
1281    static function addEndCall(\Doku_Handler $handler, $tagName, $callStackArray = array())
1282    {
1283        $pluginName = PluginUtility::getComponentName($tagName);
1284        $handler->addPluginCall(
1285            $pluginName,
1286            $callStackArray,
1287            DOKU_LEXER_END,
1288            null,
1289            null
1290        );
1291    }
1292
1293    /**
1294     * General Debug
1295     */
1296    public
1297    static function isDebug()
1298    {
1299        global $conf;
1300        return $conf["allowdebug"] === 1;
1301
1302    }
1303
1304    /**
1305     * @return bool true if loaded, false otherwise
1306     * Strap is loaded only if this is the same version
1307     * to avoid function, class, or members that does not exist
1308     */
1309    public
1310    static function loadStrapUtilityTemplateIfPresentAndSameVersion()
1311    {
1312        $templateUtilityFile = __DIR__ . '/../../../tpl/strap/class/TplUtility.php';
1313        if (file_exists($templateUtilityFile)) {
1314            /**
1315             * Check the version
1316             */
1317            $templateInfo = confToHash(__DIR__ . '/../../../tpl/strap/template.info.txt');
1318            $templateVersion = $templateInfo['version'];
1319            $comboVersion = self::$INFO_PLUGIN['version'];
1320            if ($templateVersion != $comboVersion) {
1321                if ($comboVersion > $templateVersion) {
1322                    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).");
1323                } else {
1324                    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).");
1325                }
1326                return false;
1327            } else {
1328                /** @noinspection PhpIncludeInspection */
1329                require_once($templateUtilityFile);
1330                return true;
1331            }
1332        } else {
1333            $level = LogUtility::LVL_MSG_DEBUG;
1334            if (defined('DOKU_UNITTEST')) {
1335                // fail
1336                $level = LogUtility::LVL_MSG_ERROR;
1337            }
1338            if (Site::getTemplate() != "strap") {
1339                LogUtility::msg("The strap template is not installed", $level);
1340            } else {
1341                LogUtility::msg("The file ($templateUtilityFile) was not found", $level);
1342            }
1343            return false;
1344        }
1345    }
1346
1347
1348    /**
1349     *
1350     * See also dev.md file
1351     */
1352    public static function isDevOrTest()
1353    {
1354        if (self::isDev()) {
1355            return true;
1356        }
1357        return self::isTest();
1358    }
1359
1360    public static function isDev()
1361    {
1362        global $_SERVER;
1363        if ($_SERVER["REMOTE_ADDR"] == "127.0.0.1") {
1364            return true;
1365        }
1366        return false;
1367    }
1368
1369    public static function getInstructions($markiCode)
1370    {
1371        return p_get_instructions($markiCode);
1372    }
1373
1374    public static function getInstructionsWithoutRoot($markiCode)
1375    {
1376        return RenderUtility::getInstructionsAndStripPEventually($markiCode);
1377    }
1378
1379    /**
1380     * Transform a text into a valid HTML id
1381     * @param $string
1382     * @return string
1383     */
1384    public static function toHtmlId($string)
1385    {
1386        /**
1387         * sectionId calls cleanID
1388         * cleanID delete all things before a ':'
1389         * we do then the replace before to not
1390         * lost a minus '-' separator
1391         */
1392        $string = str_replace(array(':', '.'), '', $string);
1393        return sectionID($string, $check);
1394    }
1395
1396    public static function isTest()
1397    {
1398        return defined('DOKU_UNITTEST');
1399    }
1400
1401
1402    public static function getCacheManager(): CacheManager
1403    {
1404        return CacheManager::getOrCreate();
1405    }
1406
1407    public static function getModeFromPluginName($name)
1408    {
1409        return "plugin_$name";
1410    }
1411
1412    public static function isCi(): bool
1413    {
1414        // https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
1415        return getenv("CI") === "true";
1416    }
1417
1418
1419}
1420
1421PluginUtility::init();
1422