1<?php
2
3namespace ComboStrap;
4
5
6use ComboStrap\Meta\Field\PageTemplateName;
7use dokuwiki\Extension\PluginTrait;
8
9class SiteConfig
10{
11    const LOG_EXCEPTION_LEVEL = 'log-exception-level';
12
13    /**
14     * A configuration to enable the theme/template system
15     */
16    public const CONF_ENABLE_THEME_SYSTEM = "combo-conf-001";
17    public const CONF_ENABLE_THEME_SYSTEM_DEFAULT = 1;
18
19    /**
20     * The default font-size for the pages
21     */
22    const REM_CONF = "combo-conf-002";
23    const REM_CANONICAL = "rfs";
24
25    /**
26     * The maximum size to be embedded
27     * Above this size limit they are fetched
28     *
29     * 2kb is too small for icon.
30     * For instance, the et:twitter is 2,600b
31     */
32    public const HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT = "combo-conf-003";
33    public const HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT_DEFAULT = 4;
34    /**
35     * Private configuration used in test
36     * When set to true, all javascript snippet will be inlined
37     */
38    public const HTML_ALWAYS_INLINE_LOCAL_JAVASCRIPT = "combo-conf-004";
39    const CANONICAL = "site-config";
40    const GLOBAL_SCOPE = null;
41    /**
42     * The default name
43     */
44    public const CONF_DEFAULT_INDEX_NAME = "start";
45
46
47    /**
48     * @var WikiPath the {@link self::getContextPath()} when no context could be determined
49     */
50    private WikiPath $defaultContextPath;
51
52    private array $authorizedUrlSchemes;
53
54    /**
55     * @var array - the configuration value to restore
56     *
57     * Note we can't capture the whole global $conf
58     * because the configuration are loaded at runtime via {@link PluginTrait::loadConfig()}
59     *
60     * Meaning that the configuration environment at the start is not fully loaded
61     * and does not represent the environment totally
62     *
63     * We capture then the change and restore them at the end
64     */
65    private array $configurationValuesToRestore = [];
66    private ExecutionContext $executionContext;
67    private array $interWikis;
68
69    /**
70     * @param ExecutionContext $executionContext
71     */
72    public function __construct(ExecutionContext $executionContext)
73    {
74        $this->executionContext = $executionContext;
75    }
76
77    /**
78     * TODO: Default: Note that the config of plugin are loaded
79     *   via {@link PluginTrait::loadConfig()}
80     *   when {@link PluginTrait::getConf()} is used
81     *   Therefore whenever possible, for now {@link PluginTrait::getConf()}
82     *   should be used otherwise, there is no default
83     *   Or best, the default should be also in the code
84     *
85     */
86    public static function getConfValue($confName, $defaultValue = null, ?string $namespace = PluginUtility::PLUGIN_BASE_NAME)
87    {
88        global $conf;
89        if ($namespace !== null) {
90
91            $namespace = $conf['plugin'][$namespace] ?? null;
92            if ($namespace === null) {
93                return $defaultValue;
94            }
95            $value = $namespace[$confName] ?? null;
96
97        } else {
98
99            $value = $conf[$confName] ?? null;
100
101        }
102        if (DataType::isBoolean($value)) {
103            /**
104             * Because the next line
105             * `trim($value) === ""`
106             * is true for a false value
107             */
108            return $value;
109        }
110        if ($value === null || trim($value) === "") {
111            return $defaultValue;
112        }
113        return $value;
114    }
115
116
117    /**
118     * @param string $key
119     * @param $value
120     * @param string|null $pluginNamespace - null for the global namespace
121     * @return $this
122     */
123    public function setConf(string $key, $value, ?string $pluginNamespace = PluginUtility::PLUGIN_BASE_NAME): SiteConfig
124    {
125        /**
126         * Environment within dokuwiki is a global variable
127         *
128         * We set it the global variable
129         *
130         * but we capture it {@link ExecutionContext::$capturedConf}
131         * to restore it when the execution context os {@link ExecutionContext::close()}
132         */
133        $globalKey = "$pluginNamespace:$key";
134        if (!isset($this->configurationValuesToRestore[$globalKey])) {
135            $oldValue = self::getConfValue($key, $value, $pluginNamespace);
136            $this->configurationValuesToRestore[$globalKey] = $oldValue;
137        }
138        Site::setConf($key, $value, $pluginNamespace);
139        return $this;
140    }
141
142    /**
143     * Restore the configuration
144     * as it was when php started
145     * @return void
146     */
147    public function restoreConfigState()
148    {
149
150        foreach ($this->configurationValuesToRestore as $guid => $value) {
151            [$plugin, $confKey] = explode(":", $guid);
152            Site::setConf($confKey, $value, $plugin);
153        }
154    }
155
156    public function setDisableThemeSystem(): SiteConfig
157    {
158        $this->setConf(self::CONF_ENABLE_THEME_SYSTEM, 0);
159        return $this;
160    }
161
162    public function isThemeSystemEnabled(): bool
163    {
164        return $this->getBooleanValue(self::CONF_ENABLE_THEME_SYSTEM, self::CONF_ENABLE_THEME_SYSTEM_DEFAULT);
165    }
166
167    public function getValue(string $key, ?string $default = null, ?string $scope = PluginUtility::PLUGIN_BASE_NAME)
168    {
169        return self::getConfValue($key, $default, $scope);
170    }
171
172    /**
173     * @param string $key
174     * @param int $default - the default value (1=true,0=false in the dokuwiki config system)
175     * @return bool
176     */
177    public function getBooleanValue(string $key, int $default): bool
178    {
179        $value = $this->getValue($key, $default);
180        /**
181         * Boolean in config is normally the value 1
182         */
183        return DataType::toBoolean($value);
184    }
185
186    public function setCacheXhtmlOn()
187    {
188        // ensure the value is not -1, which disables caching
189        // https://www.dokuwiki.org/config:cachetime
190
191        $this->setConf('cachetime', 60 * 60, null);
192        return $this;
193    }
194
195    public function setConsoleOn(): SiteConfig
196    {
197        $this->setConf('console', 1);
198        return $this;
199    }
200
201    public function isConsoleOn(): bool
202    {
203        return $this->getBooleanValue('console', 0);
204    }
205
206    public function getExecutionContext(): ExecutionContext
207    {
208        return $this->executionContext;
209    }
210
211    public function setConsoleOff(): SiteConfig
212    {
213        $this->setConf('console', 0);
214        return $this;
215    }
216
217    public function setLogExceptionToError(): SiteConfig
218    {
219        $this->setLogExceptionLevel(LogUtility::LVL_MSG_ERROR);
220        return $this;
221    }
222
223    public function setDisableLogException(): SiteConfig
224    {
225        $this->setLogExceptionLevel(LogUtility::LVL_MSG_ABOVE_ERROR);
226        return $this;
227    }
228
229    public function setLogExceptionLevel(int $level): SiteConfig
230    {
231        $this->setConf(self::LOG_EXCEPTION_LEVEL, $level);
232        return $this;
233    }
234
235    public function getLogExceptionLevel(): int
236    {
237        return $this->getValue(self::LOG_EXCEPTION_LEVEL, LogUtility::DEFAULT_THROW_LEVEL);
238    }
239
240    /**
241     * @throws ExceptionNotFound
242     */
243    public function getRemFontSize(): int
244    {
245
246        $value = $this->getValue(self::REM_CONF);
247        if ($value === null) {
248            throw new ExceptionNotFound("No rem sized defined");
249        }
250        try {
251            return DataType::toInteger($value);
252        } catch (ExceptionCompile $e) {
253            $message = "The rem configuration value ($value) is not a integer. Error: {$e->getMessage()}";
254            LogUtility::msg($message);
255            throw new ExceptionNotFound($message);
256        }
257
258    }
259
260    public function setDefaultContextPath(WikiPath $contextPath)
261    {
262        $this->defaultContextPath = $contextPath;
263        if (FileSystems::isDirectory($this->defaultContextPath)) {
264            /**
265             * Not a directory.
266             *
267             * If the link or path is the empty path, the path is not the directory
268             * but the actual markup
269             */
270            throw new ExceptionRuntimeInternal("The path ($contextPath) should not be a namespace path");
271        }
272        return $this;
273    }
274
275    /**
276     * @return WikiPath - the default context path is if not set the root page
277     */
278    public function getDefaultContextPath(): WikiPath
279    {
280        if (isset($this->defaultContextPath)) {
281            return $this->defaultContextPath;
282        }
283        // in a admin or dynamic rendering
284        // dokuwiki may have set a $ID
285        global $ID;
286        if (isset($ID)) {
287            return WikiPath::createMarkupPathFromId($ID);
288        }
289        return WikiPath::createRootNamespacePathOnMarkupDrive()->resolve(Site::getIndexPageName() . "." . WikiPath::MARKUP_DEFAULT_TXT_EXTENSION);
290    }
291
292    public function getHtmlMaxInlineResourceSize()
293    {
294        try {
295            return DataType::toInteger($this->getValue(SiteConfig::HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT, self::HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT_DEFAULT)) * 1024;
296        } catch (ExceptionBadArgument $e) {
297            LogUtility::internalError("Max in line size error.", self::CANONICAL, $e);
298            return self::HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT_DEFAULT * 1024;
299        }
300    }
301
302    public function setHtmlMaxInlineResourceSize(int $kbSize): SiteConfig
303    {
304        $this->setConf(SiteConfig::HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT, $kbSize);
305        return $this;
306    }
307
308    public function setDisableHeadingSectionEditing(): SiteConfig
309    {
310        $this->setConf('maxseclevel', 0, null);
311        return $this;
312    }
313
314    public function setHtmlEnableAlwaysInlineLocalJavascript(): SiteConfig
315    {
316        $this->setConf(self::HTML_ALWAYS_INLINE_LOCAL_JAVASCRIPT, 1);
317        return $this;
318    }
319
320    public function setHtmlDisableAlwaysInlineLocalJavascript(): SiteConfig
321    {
322        $this->setConf(self::HTML_ALWAYS_INLINE_LOCAL_JAVASCRIPT, 0);
323        return $this;
324    }
325
326    public function isLocalJavascriptAlwaysInlined(): bool
327    {
328        return $this->getBooleanValue(self::HTML_ALWAYS_INLINE_LOCAL_JAVASCRIPT, 0);
329    }
330
331
332    public function disableLazyLoad(): SiteConfig
333    {
334        return $this->setConf(SvgImageLink::CONF_LAZY_LOAD_ENABLE, 0)
335            ->setConf(LazyLoad::CONF_RASTER_ENABLE, 0);
336
337    }
338
339    public function setUseHeadingAsTitle(): SiteConfig
340    {
341        return $this->setConf('useheading', 1, self::GLOBAL_SCOPE);
342    }
343
344    public function setEnableSectionEditing(): SiteConfig
345    {
346        return $this->setConf('maxseclevel', 999, self::GLOBAL_SCOPE);
347    }
348
349    public function isSectionEditingEnabled(): bool
350    {
351        return $this->getTocMaxLevel() > 0;
352    }
353
354    public function getTocMaxLevel(): int
355    {
356        $value = $this->getValue('maxseclevel', null, self::GLOBAL_SCOPE);
357        try {
358            return DataType::toInteger($value);
359        } catch (ExceptionBadArgument $e) {
360            LogUtility::internalError("Unable to the the maxseclevel as integer. Error: {$e->getMessage()}", Toc::CANONICAL);
361            return 0;
362        }
363    }
364
365    public function setTocMinHeading(int $int): SiteConfig
366    {
367        return $this->setConf('tocminheads', $int, self::GLOBAL_SCOPE);
368    }
369
370    public function getIndexPageName()
371    {
372        return $this->getValue("start", self::CONF_DEFAULT_INDEX_NAME, self::GLOBAL_SCOPE);
373    }
374
375    public function getAuthorizedUrlSchemes(): ?array
376    {
377        if (isset($this->authorizedUrlSchemes)) {
378            return $this->authorizedUrlSchemes;
379        }
380        $this->authorizedUrlSchemes = getSchemes();
381        $this->authorizedUrlSchemes[] = "whatsapp";
382        $this->authorizedUrlSchemes[] = "mailto";
383        return $this->authorizedUrlSchemes;
384    }
385
386    public function getInterWikis(): array
387    {
388        $this->loadInterWikiIfNeeded();
389        return $this->interWikis;
390    }
391
392    public function addInterWiki(string $name, string $value): SiteConfig
393    {
394        $this->loadInterWikiIfNeeded();
395        $this->interWikis[$name] = $value;
396        return $this;
397    }
398
399    private function loadInterWikiIfNeeded(): void
400    {
401        if (isset($this->interWikis)) {
402            return;
403        }
404        $this->interWikis = getInterwiki();
405    }
406
407    public function setTocTopLevel(int $int): SiteConfig
408    {
409        return $this->setConf('toptoclevel', $int, self::GLOBAL_SCOPE);
410    }
411
412    public function getMetaDataDirectory(): LocalPath
413    {
414        $metadataDirectory = $this->getValue('metadir', null, self::GLOBAL_SCOPE);
415        if ($metadataDirectory === null) {
416            throw new ExceptionRuntime("The meta directory configuration value ('metadir') is null");
417        }
418        return LocalPath::createFromPathString($metadataDirectory);
419    }
420
421    public function setCanonicalUrlType(string $value): SiteConfig
422    {
423        return $this->setConf(PageUrlType::CONF_CANONICAL_URL_TYPE, $value);
424    }
425
426    public function setEnableTheming(): SiteConfig
427    {
428        $this->setConf(SiteConfig::CONF_ENABLE_THEME_SYSTEM, 1);
429        return $this;
430    }
431
432    public function getTheme(): string
433    {
434        return $this->getValue(TemplateEngine::CONF_THEME, TemplateEngine::CONF_THEME_DEFAULT);
435    }
436
437    /**
438     * Note: in test to speed the test execution,
439     * the default is set to {@link PageTemplateName::BLANK_TEMPLATE_VALUE}
440     */
441    public function getDefaultLayoutName()
442    {
443        return $this->getValue(PageTemplateName::CONF_DEFAULT_NAME, PageTemplateName::HOLY_TEMPLATE_VALUE);
444    }
445
446    public function setEnableThemeSystem(): SiteConfig
447    {
448        // this is the default but yeah
449        $this->setConf(self::CONF_ENABLE_THEME_SYSTEM, 1);
450        return $this;
451    }
452
453    /**
454     * DokuRewrite
455     * `doku.php/id/...`
456     * https://www.dokuwiki.org/config:userewrite
457     * @return $this
458     */
459    public function setUrlRewriteToDoku(): SiteConfig
460    {
461        $this->setConf('userewrite', '2', self::GLOBAL_SCOPE);
462        return $this;
463    }
464
465    /**
466     * Web server rewrite (Apache rewrite (htaccess), Nginx)
467     * https://www.dokuwiki.org/config:userewrite
468     * @return $this
469     */
470    public function setUrlRewriteToWebServer(): SiteConfig
471    {
472        $this->setConf('userewrite', '1', self::GLOBAL_SCOPE);
473        return $this;
474    }
475
476    public function getRemFontSizeOrDefault(): int
477    {
478        try {
479            return $this->getRemFontSize();
480        } catch (ExceptionNotFound $e) {
481            return 16;
482        }
483    }
484
485    public function getDataDirectory(): LocalPath
486    {
487        global $conf;
488        $dataDirectory = $conf['savedir'];
489        if ($dataDirectory === null) {
490            throw new ExceptionRuntime("The data directory ($dataDirectory) is null");
491        }
492        return LocalPath::createFromPathString($dataDirectory);
493    }
494
495    public function setTheme(string $themeName): SiteConfig
496    {
497        $this->setConf(TemplateEngine::CONF_THEME, $themeName);
498        return $this;
499    }
500
501    public function getPageHeaderSlotName()
502    {
503        return $this->getValue(TemplateSlot::CONF_PAGE_HEADER_NAME, TemplateSlot::CONF_PAGE_HEADER_NAME_DEFAULT);
504    }
505
506    public function setConfDokuWiki(string $key, $value): SiteConfig
507    {
508        return $this->setConf($key, $value, self::GLOBAL_SCOPE);
509    }
510
511    /**
512     * @throws ExceptionNotFound
513     */
514    public function getPrimaryColor(): ColorRgb
515    {
516        $value = Site::getPrimaryColorValue();
517        if (
518            $value === null ||
519            (trim($value) === "")) {
520            throw new ExceptionNotFound();
521        }
522        try {
523            return ColorRgb::createFromString($value);
524        } catch (ExceptionCompile $e) {
525            LogUtility::msg("The primary color value configuration ($value) is not valid. Error: {$e->getMessage()}");
526            throw new ExceptionNotFound();
527        }
528    }
529
530    public function setPrimaryColor(string $primaryColorValue): SiteConfig
531    {
532        self::setConf(BrandingColors::PRIMARY_COLOR_CONF, $primaryColorValue);
533        return $this;
534    }
535
536    public function getPrimaryColorOrDefault(string $defaultColor): ColorRgb
537    {
538        try {
539            return $this->getPrimaryColor();
540        } catch (ExceptionNotFound $e) {
541            try {
542                return ColorRgb::createFromString($defaultColor);
543            } catch (ExceptionBadArgument $e) {
544                LogUtility::internalError("The default color $defaultColor is not a color string.", self::CANONICAL, $e);
545                return ColorRgb::getDefaultPrimary();
546            }
547        }
548    }
549
550    public function isBrandingColorInheritanceEnabled(): bool
551    {
552        return $this->getValue(BrandingColors::BRANDING_COLOR_INHERITANCE_ENABLE_CONF, BrandingColors::BRANDING_COLOR_INHERITANCE_ENABLE_CONF_DEFAULT) === 1;
553    }
554
555    /**
556     * @throws ExceptionNotFound
557     */
558    public function getSecondaryColor(): ColorRgb
559    {
560        $secondaryColor = Site::getSecondaryColor();
561        if ($secondaryColor === null) {
562            throw new ExceptionNotFound();
563        }
564        return $secondaryColor;
565    }
566
567    public function isXhtmlCacheOn(): bool
568    {
569        global $conf;
570        return $conf['cachetime'] !== -1;
571    }
572
573
574}
575