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