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