xref: /plugin/fontello/helper.php (revision 95357802f68767bcd0367676ca97b384fa2109c6)
11776c5c5Sdh-tools<?php
21776c5c5Sdh-tools
31776c5c5Sdh-toolsuse dokuwiki\Extension\Plugin;
41776c5c5Sdh-tools
51776c5c5Sdh-tools/**
61776c5c5Sdh-tools * Shared logic for the Fontello plugin.
71776c5c5Sdh-tools */
81776c5c5Sdh-toolsclass helper_plugin_fontello extends Plugin
91776c5c5Sdh-tools{
101776c5c5Sdh-tools    protected const ACTIVE_DIR = DOKU_PLUGIN . 'fontello/assets/active';
111776c5c5Sdh-tools    protected const ACTIVE_CSS = self::ACTIVE_DIR . '/css/fontello.css';
121776c5c5Sdh-tools    protected const ACTIVE_CONFIG = self::ACTIVE_DIR . '/config.json';
131776c5c5Sdh-tools    protected const ACTIVE_MANIFEST = self::ACTIVE_DIR . '/manifest.json';
141776c5c5Sdh-tools    protected const ACTIVE_ENABLED = self::ACTIVE_DIR . '/enabled.json';
151776c5c5Sdh-tools    protected const ACTIVE_FONT_DIR = self::ACTIVE_DIR . '/font';
161776c5c5Sdh-tools
171776c5c5Sdh-tools    /**
181776c5c5Sdh-tools     * Returns true when an active package is available.
191776c5c5Sdh-tools     *
201776c5c5Sdh-tools     * @return bool
211776c5c5Sdh-tools     */
221776c5c5Sdh-tools    public function hasActivePackage()
231776c5c5Sdh-tools    {
241776c5c5Sdh-tools        return file_exists(self::ACTIVE_CONFIG) && file_exists(self::ACTIVE_CSS);
251776c5c5Sdh-tools    }
261776c5c5Sdh-tools
271776c5c5Sdh-tools    /**
281776c5c5Sdh-tools     * Returns the public URL to the generated stylesheet.
291776c5c5Sdh-tools     *
301776c5c5Sdh-tools     * @return string
311776c5c5Sdh-tools     */
321776c5c5Sdh-tools    public function getCssUrl()
331776c5c5Sdh-tools    {
341776c5c5Sdh-tools        $mtime = @filemtime(self::ACTIVE_CSS) ?: time();
351776c5c5Sdh-tools        return DOKU_BASE . 'lib/plugins/fontello/assets/active/css/fontello.css?v=' . $mtime;
361776c5c5Sdh-tools    }
371776c5c5Sdh-tools
381776c5c5Sdh-tools    /**
391776c5c5Sdh-tools     * Load the currently active package information.
401776c5c5Sdh-tools     *
411776c5c5Sdh-tools     * @return array|null
421776c5c5Sdh-tools     */
431776c5c5Sdh-tools    public function getPackageInfo()
441776c5c5Sdh-tools    {
451776c5c5Sdh-tools        if (!$this->hasActivePackage()) return null;
461776c5c5Sdh-tools
471776c5c5Sdh-tools        $config = $this->loadJsonFile(self::ACTIVE_CONFIG);
481776c5c5Sdh-tools        if ($config === null) return null;
491776c5c5Sdh-tools
501776c5c5Sdh-tools        $manifest = $this->loadJsonFile(self::ACTIVE_MANIFEST) ?: [];
511776c5c5Sdh-tools        $prefix = (string) ($config['css_prefix_text'] ?? 'icon-');
521776c5c5Sdh-tools        $icons = $this->extractIcons($config);
531776c5c5Sdh-tools        $enabledNames = $this->loadEnabledIconNames($icons);
541776c5c5Sdh-tools        $enabledMap = array_fill_keys($enabledNames, true);
551776c5c5Sdh-tools
561776c5c5Sdh-tools        foreach ($icons as $index => $icon) {
571776c5c5Sdh-tools            $icons[$index]['enabled'] = isset($enabledMap[$icon['name']]);
581776c5c5Sdh-tools        }
591776c5c5Sdh-tools
601776c5c5Sdh-tools        return [
611776c5c5Sdh-tools            'prefix' => $prefix,
621776c5c5Sdh-tools            'icons' => $icons,
631776c5c5Sdh-tools            'icon_count' => count($icons),
641776c5c5Sdh-tools            'enabled_count' => count($enabledNames),
651776c5c5Sdh-tools            'font_files' => $manifest['font_files'] ?? [],
661776c5c5Sdh-tools            'imported_at' => $manifest['imported_at'] ?? null,
671776c5c5Sdh-tools            'zip_name' => $manifest['zip_name'] ?? '',
681776c5c5Sdh-tools        ];
691776c5c5Sdh-tools    }
701776c5c5Sdh-tools
711776c5c5Sdh-tools    /**
721776c5c5Sdh-tools     * Return all active icons for toolbar or picker integrations.
731776c5c5Sdh-tools     *
741776c5c5Sdh-tools     * @return array
751776c5c5Sdh-tools     */
761776c5c5Sdh-tools    public function getActiveIcons()
771776c5c5Sdh-tools    {
781776c5c5Sdh-tools        $package = $this->getPackageInfo();
791776c5c5Sdh-tools        if ($package === null) return [];
801776c5c5Sdh-tools
811776c5c5Sdh-tools        return array_values(array_filter($package['icons'], static function ($icon) {
821776c5c5Sdh-tools            return !empty($icon['enabled']);
831776c5c5Sdh-tools        }));
841776c5c5Sdh-tools    }
851776c5c5Sdh-tools
861776c5c5Sdh-tools    /**
871776c5c5Sdh-tools     * Check if the given icon exists in the active package.
881776c5c5Sdh-tools     *
891776c5c5Sdh-tools     * @param string $iconName
901776c5c5Sdh-tools     * @return bool
911776c5c5Sdh-tools     */
921776c5c5Sdh-tools    public function hasIcon($iconName)
931776c5c5Sdh-tools    {
941776c5c5Sdh-tools        return $this->getIconClass($iconName) !== null;
951776c5c5Sdh-tools    }
961776c5c5Sdh-tools
971776c5c5Sdh-tools    /**
981776c5c5Sdh-tools     * Return the CSS class for an icon name.
991776c5c5Sdh-tools     *
1001776c5c5Sdh-tools     * @param string $iconName
1011776c5c5Sdh-tools     * @return string|null
1021776c5c5Sdh-tools     */
1031776c5c5Sdh-tools    public function getIconClass($iconName)
1041776c5c5Sdh-tools    {
1051776c5c5Sdh-tools        $package = $this->getPackageInfo();
1061776c5c5Sdh-tools        if ($package === null) return null;
1071776c5c5Sdh-tools
1081776c5c5Sdh-tools        foreach ($package['icons'] as $icon) {
1091776c5c5Sdh-tools            if ($icon['name'] === $iconName) return $icon['class'];
1101776c5c5Sdh-tools        }
1111776c5c5Sdh-tools
1121776c5c5Sdh-tools        return null;
1131776c5c5Sdh-tools    }
1141776c5c5Sdh-tools
1151776c5c5Sdh-tools    /**
1161776c5c5Sdh-tools     * Parse a Fontello icon token.
1171776c5c5Sdh-tools     *
1181776c5c5Sdh-tools     * @param string $token
1191776c5c5Sdh-tools     * @return array|null
1201776c5c5Sdh-tools     */
1211776c5c5Sdh-tools    public function parseIconToken($token)
1221776c5c5Sdh-tools    {
1231776c5c5Sdh-tools        if (!preg_match('/^<icon:([A-Za-z0-9_-]+)((?:\|[A-Za-z0-9_-]+)*)>$/', $token, $match)) {
1241776c5c5Sdh-tools            return null;
1251776c5c5Sdh-tools        }
1261776c5c5Sdh-tools
1271776c5c5Sdh-tools        $flags = [];
1281776c5c5Sdh-tools        if ($match[2] !== '') {
1291776c5c5Sdh-tools            foreach (explode('|', ltrim($match[2], '|')) as $flag) {
1301776c5c5Sdh-tools                if ($flag === '') continue;
1311776c5c5Sdh-tools                if (!in_array($flag, ['toc', 'notoc'], true)) return null;
1321776c5c5Sdh-tools                $flags[$flag] = true;
1331776c5c5Sdh-tools            }
1341776c5c5Sdh-tools        }
1351776c5c5Sdh-tools
1361776c5c5Sdh-tools        return [
1371776c5c5Sdh-tools            'raw' => $token,
1381776c5c5Sdh-tools            'name' => $match[1],
1391776c5c5Sdh-tools            'flags' => $flags,
1401776c5c5Sdh-tools            'toc' => isset($flags['toc']),
1411776c5c5Sdh-tools            'notoc' => isset($flags['notoc']),
1421776c5c5Sdh-tools        ];
1431776c5c5Sdh-tools    }
1441776c5c5Sdh-tools
1451776c5c5Sdh-tools    /**
1461776c5c5Sdh-tools     * Return the XHTML markup for a known icon.
1471776c5c5Sdh-tools     *
1481776c5c5Sdh-tools     * @param string $iconName
1491776c5c5Sdh-tools     * @return string|null
1501776c5c5Sdh-tools     */
1511776c5c5Sdh-tools    public function renderIconXhtml($iconName)
1521776c5c5Sdh-tools    {
1531776c5c5Sdh-tools        $class = $this->getIconClass($iconName);
1541776c5c5Sdh-tools        if ($class === null) return null;
1551776c5c5Sdh-tools
1561776c5c5Sdh-tools        return '<span class="fontello-icon ' . hsc($class) . '" aria-hidden="true"></span>';
1571776c5c5Sdh-tools    }
1581776c5c5Sdh-tools
1591776c5c5Sdh-tools    /**
1601776c5c5Sdh-tools     * Decide whether a parsed icon token should remain visible in the TOC.
1611776c5c5Sdh-tools     *
1621776c5c5Sdh-tools     * @param array $token
1631776c5c5Sdh-tools     * @return bool
1641776c5c5Sdh-tools     */
1651776c5c5Sdh-tools    public function iconTokenShowsInToc(array $token)
1661776c5c5Sdh-tools    {
1671776c5c5Sdh-tools        if (!empty($token['notoc'])) return false;
1681776c5c5Sdh-tools        if (!empty($token['toc'])) return true;
1691776c5c5Sdh-tools
1701776c5c5Sdh-tools        return (bool) $this->getConf('showInToc');
1711776c5c5Sdh-tools    }
1721776c5c5Sdh-tools
1731776c5c5Sdh-tools    /**
1741776c5c5Sdh-tools     * Persist which icons should be offered in toolbar or picker integrations.
1751776c5c5Sdh-tools     *
1761776c5c5Sdh-tools     * This does not affect inline rendering of imported icons.
1771776c5c5Sdh-tools     *
1781776c5c5Sdh-tools     * @param array $iconNames
1791776c5c5Sdh-tools     * @return int
1801776c5c5Sdh-tools     */
1811776c5c5Sdh-tools    public function saveEnabledIconNames(array $iconNames)
1821776c5c5Sdh-tools    {
1831776c5c5Sdh-tools        $package = $this->getPackageInfo();
1841776c5c5Sdh-tools        if ($package === null) {
1851776c5c5Sdh-tools            throw new RuntimeException($this->getLang('err_no_package'));
1861776c5c5Sdh-tools        }
1871776c5c5Sdh-tools
1881776c5c5Sdh-tools        $requested = array_fill_keys(array_map('strval', $iconNames), true);
1891776c5c5Sdh-tools        $enabled = [];
1901776c5c5Sdh-tools
1911776c5c5Sdh-tools        foreach ($package['icons'] as $icon) {
1921776c5c5Sdh-tools            if (isset($requested[$icon['name']])) {
1931776c5c5Sdh-tools                $enabled[] = $icon['name'];
1941776c5c5Sdh-tools            }
1951776c5c5Sdh-tools        }
1961776c5c5Sdh-tools
1971776c5c5Sdh-tools        io_makeFileDir(self::ACTIVE_ENABLED);
1981776c5c5Sdh-tools        file_put_contents(self::ACTIVE_ENABLED, json_encode($enabled, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
1991776c5c5Sdh-tools
2001776c5c5Sdh-tools        return count($enabled);
2011776c5c5Sdh-tools    }
2021776c5c5Sdh-tools
2031776c5c5Sdh-tools    /**
2041776c5c5Sdh-tools     * Import a Fontello ZIP package.
2051776c5c5Sdh-tools     *
2061776c5c5Sdh-tools     * @param array $upload
2071776c5c5Sdh-tools     * @return array
2081776c5c5Sdh-tools     */
2091776c5c5Sdh-tools    public function importPackage(array $upload)
2101776c5c5Sdh-tools    {
2111776c5c5Sdh-tools        if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
2121776c5c5Sdh-tools            throw new RuntimeException($this->uploadErrorMessage((int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE)));
2131776c5c5Sdh-tools        }
2141776c5c5Sdh-tools
2151776c5c5Sdh-tools        $tmpName = (string) ($upload['tmp_name'] ?? '');
2161776c5c5Sdh-tools        if ($tmpName === '' || !is_uploaded_file($tmpName) && !file_exists($tmpName)) {
2171776c5c5Sdh-tools            throw new RuntimeException($this->getLang('err_upload_missing'));
2181776c5c5Sdh-tools        }
2191776c5c5Sdh-tools
2201776c5c5Sdh-tools        $archive = $this->openArchive($tmpName);
2211776c5c5Sdh-tools        $map = $archive['map'];
2221776c5c5Sdh-tools        $configEntry = $this->findRequiredEntry($map, 'config.json', $this->getLang('err_missing_config'));
2231776c5c5Sdh-tools        $this->findRequiredEntry($map, 'css/fontello.css', $this->getLang('err_missing_css'));
2241776c5c5Sdh-tools        $fontEntries = $this->findFontEntries($map);
2251776c5c5Sdh-tools        if ($fontEntries === []) {
2261776c5c5Sdh-tools            $this->closeArchive($archive);
2271776c5c5Sdh-tools            throw new RuntimeException($this->getLang('err_missing_fonts'));
2281776c5c5Sdh-tools        }
2291776c5c5Sdh-tools
2301776c5c5Sdh-tools        $configJson = $this->readArchiveEntry($archive, $configEntry);
2311776c5c5Sdh-tools        $config = json_decode($configJson, true);
2321776c5c5Sdh-tools        if (!is_array($config) || !isset($config['glyphs']) || !is_array($config['glyphs'])) {
2331776c5c5Sdh-tools            $this->closeArchive($archive);
2341776c5c5Sdh-tools            throw new RuntimeException($this->getLang('err_invalid_config'));
2351776c5c5Sdh-tools        }
2361776c5c5Sdh-tools
2371776c5c5Sdh-tools        $icons = $this->extractIcons($config);
2381776c5c5Sdh-tools        if ($icons === []) {
2391776c5c5Sdh-tools            $this->closeArchive($archive);
2401776c5c5Sdh-tools            throw new RuntimeException($this->getLang('err_no_icons'));
2411776c5c5Sdh-tools        }
2421776c5c5Sdh-tools
2431776c5c5Sdh-tools        $fontFiles = [];
2441776c5c5Sdh-tools        $fontContents = [];
2451776c5c5Sdh-tools        foreach ($fontEntries as $relative => $original) {
2461776c5c5Sdh-tools            $basename = basename($relative);
2471776c5c5Sdh-tools            $fontFiles[] = $basename;
2481776c5c5Sdh-tools            $fontContents[$basename] = $this->readArchiveEntry($archive, $original);
2491776c5c5Sdh-tools        }
2501776c5c5Sdh-tools
2511776c5c5Sdh-tools        $manifest = [
2521776c5c5Sdh-tools            'zip_name' => (string) ($upload['name'] ?? ''),
2531776c5c5Sdh-tools            'imported_at' => date('c'),
2541776c5c5Sdh-tools            'prefix' => (string) ($config['css_prefix_text'] ?? 'icon-'),
2551776c5c5Sdh-tools            'icon_count' => count($icons),
2561776c5c5Sdh-tools            'font_files' => array_values($fontFiles),
2571776c5c5Sdh-tools        ];
2581776c5c5Sdh-tools
2591776c5c5Sdh-tools        $css = $this->buildCss($config, $fontFiles);
2601776c5c5Sdh-tools
2611776c5c5Sdh-tools        $this->closeArchive($archive);
2621776c5c5Sdh-tools        $this->resetActiveDirectory();
2631776c5c5Sdh-tools
2641776c5c5Sdh-tools        foreach ($fontContents as $basename => $content) {
2651776c5c5Sdh-tools            $target = self::ACTIVE_FONT_DIR . '/' . $basename;
2661776c5c5Sdh-tools            io_makeFileDir($target);
2671776c5c5Sdh-tools            file_put_contents($target, $content);
2681776c5c5Sdh-tools        }
2691776c5c5Sdh-tools
2701776c5c5Sdh-tools        io_makeFileDir(self::ACTIVE_CONFIG);
2711776c5c5Sdh-tools        file_put_contents(self::ACTIVE_CONFIG, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
2721776c5c5Sdh-tools        io_makeFileDir(self::ACTIVE_MANIFEST);
2731776c5c5Sdh-tools        file_put_contents(self::ACTIVE_MANIFEST, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
2741776c5c5Sdh-tools        io_makeFileDir(self::ACTIVE_ENABLED);
275*95357802SDaniel Hofer        $enabledJson = json_encode(array_column($icons, 'name'), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
276*95357802SDaniel Hofer        file_put_contents(self::ACTIVE_ENABLED, $enabledJson);
2771776c5c5Sdh-tools        io_makeFileDir(self::ACTIVE_CSS);
2781776c5c5Sdh-tools        file_put_contents(self::ACTIVE_CSS, $css);
2791776c5c5Sdh-tools        $this->purgeDokuWikiCaches();
2801776c5c5Sdh-tools
2811776c5c5Sdh-tools        return $this->getPackageInfo() ?: $manifest;
2821776c5c5Sdh-tools    }
2831776c5c5Sdh-tools
2841776c5c5Sdh-tools    /**
2851776c5c5Sdh-tools     * Extract icon metadata from the package config.
2861776c5c5Sdh-tools     *
2871776c5c5Sdh-tools     * @param array $config
2881776c5c5Sdh-tools     * @return array
2891776c5c5Sdh-tools     */
2901776c5c5Sdh-tools    protected function extractIcons(array $config)
2911776c5c5Sdh-tools    {
2921776c5c5Sdh-tools        $prefix = (string) ($config['css_prefix_text'] ?? 'icon-');
2931776c5c5Sdh-tools        $icons = [];
2941776c5c5Sdh-tools
2951776c5c5Sdh-tools        foreach ($config['glyphs'] ?? [] as $glyph) {
2961776c5c5Sdh-tools            $name = trim((string) ($glyph['css'] ?? ''));
2971776c5c5Sdh-tools            $code = $glyph['code'] ?? null;
2981776c5c5Sdh-tools            if ($name === '' || !is_numeric($code)) continue;
2991776c5c5Sdh-tools
3001776c5c5Sdh-tools            $icon = [
3011776c5c5Sdh-tools                'name' => $name,
3021776c5c5Sdh-tools                'class' => $prefix . $name,
3031776c5c5Sdh-tools                'code' => strtolower(dechex((int) $code)),
3041776c5c5Sdh-tools            ];
3051776c5c5Sdh-tools
3061776c5c5Sdh-tools            // Fontello packages may contain duplicate css names; keep the last one.
3071776c5c5Sdh-tools            $icons[$icon['class']] = $icon;
3081776c5c5Sdh-tools        }
3091776c5c5Sdh-tools
3101776c5c5Sdh-tools        $icons = array_values($icons);
3111776c5c5Sdh-tools
3121776c5c5Sdh-tools        usort($icons, static function ($left, $right) {
3131776c5c5Sdh-tools            return strcmp($left['name'], $right['name']);
3141776c5c5Sdh-tools        });
3151776c5c5Sdh-tools
3161776c5c5Sdh-tools        return $icons;
3171776c5c5Sdh-tools    }
3181776c5c5Sdh-tools
3191776c5c5Sdh-tools    /**
3201776c5c5Sdh-tools     * Load enabled icon names. Missing or invalid state means all icons are enabled.
3211776c5c5Sdh-tools     *
3221776c5c5Sdh-tools     * @param array $icons
3231776c5c5Sdh-tools     * @return array
3241776c5c5Sdh-tools     */
3251776c5c5Sdh-tools    protected function loadEnabledIconNames(array $icons)
3261776c5c5Sdh-tools    {
3271776c5c5Sdh-tools        $allNames = array_column($icons, 'name');
3281776c5c5Sdh-tools        $enabled = $this->loadJsonFile(self::ACTIVE_ENABLED);
3291776c5c5Sdh-tools        if ($enabled === null || array_values($enabled) !== $enabled) return $allNames;
3301776c5c5Sdh-tools
3311776c5c5Sdh-tools        $known = array_fill_keys($allNames, true);
3321776c5c5Sdh-tools        $names = [];
3331776c5c5Sdh-tools
3341776c5c5Sdh-tools        foreach ($enabled as $name) {
3351776c5c5Sdh-tools            $name = (string) $name;
3361776c5c5Sdh-tools            if (isset($known[$name])) {
3371776c5c5Sdh-tools                $names[$name] = true;
3381776c5c5Sdh-tools            }
3391776c5c5Sdh-tools        }
3401776c5c5Sdh-tools
3411776c5c5Sdh-tools        return array_keys($names);
3421776c5c5Sdh-tools    }
3431776c5c5Sdh-tools
3441776c5c5Sdh-tools    /**
3451776c5c5Sdh-tools     * Build a normalized entry map for the archive.
3461776c5c5Sdh-tools     *
3471776c5c5Sdh-tools     * @param ZipArchive $zip
3481776c5c5Sdh-tools     * @return array
3491776c5c5Sdh-tools     */
3501776c5c5Sdh-tools    protected function buildArchiveMap(array $originalNames)
3511776c5c5Sdh-tools    {
3521776c5c5Sdh-tools        $roots = [];
3531776c5c5Sdh-tools        $hasTopLevelFiles = false;
3541776c5c5Sdh-tools
3551776c5c5Sdh-tools        foreach ($originalNames as $name) {
3561776c5c5Sdh-tools            $name = str_replace('\\', '/', $name);
3571776c5c5Sdh-tools            if (substr($name, -1) === '/') continue;
3581776c5c5Sdh-tools            $name = trim($name, '/');
3591776c5c5Sdh-tools            if ($name === '') continue;
3601776c5c5Sdh-tools            $parts = explode('/', $name, 2);
3611776c5c5Sdh-tools            $roots[$parts[0]] = true;
3621776c5c5Sdh-tools            if (count($parts) === 1) $hasTopLevelFiles = true;
3631776c5c5Sdh-tools        }
3641776c5c5Sdh-tools
3651776c5c5Sdh-tools        $stripRoot = count($roots) === 1 && !$hasTopLevelFiles;
3661776c5c5Sdh-tools        $map = [];
3671776c5c5Sdh-tools
3681776c5c5Sdh-tools        foreach ($originalNames as $name) {
3691776c5c5Sdh-tools            $name = str_replace('\\', '/', $name);
3701776c5c5Sdh-tools            if (substr($name, -1) === '/') continue;
3711776c5c5Sdh-tools            $name = trim($name, '/');
3721776c5c5Sdh-tools            if ($name === '') continue;
3731776c5c5Sdh-tools            $relative = $name;
3741776c5c5Sdh-tools            if ($stripRoot) {
3751776c5c5Sdh-tools                $relative = explode('/', $name, 2)[1] ?? '';
3761776c5c5Sdh-tools            }
3771776c5c5Sdh-tools            if ($relative === '' || substr($relative, -1) === '/') continue;
3781776c5c5Sdh-tools            $map[$relative] = $name;
3791776c5c5Sdh-tools        }
3801776c5c5Sdh-tools
3811776c5c5Sdh-tools        return $map;
3821776c5c5Sdh-tools    }
3831776c5c5Sdh-tools
3841776c5c5Sdh-tools    /**
3851776c5c5Sdh-tools     * Find a required archive entry.
3861776c5c5Sdh-tools     *
3871776c5c5Sdh-tools     * @param array $map
3881776c5c5Sdh-tools     * @param string $relativePath
3891776c5c5Sdh-tools     * @param string $errorMessage
3901776c5c5Sdh-tools     * @return string
3911776c5c5Sdh-tools     */
3921776c5c5Sdh-tools    protected function findRequiredEntry(array $map, $relativePath, $errorMessage)
3931776c5c5Sdh-tools    {
3941776c5c5Sdh-tools        if (!isset($map[$relativePath])) {
3951776c5c5Sdh-tools            throw new RuntimeException($errorMessage);
3961776c5c5Sdh-tools        }
3971776c5c5Sdh-tools
3981776c5c5Sdh-tools        return $map[$relativePath];
3991776c5c5Sdh-tools    }
4001776c5c5Sdh-tools
4011776c5c5Sdh-tools    /**
4021776c5c5Sdh-tools     * Return all supported font entries.
4031776c5c5Sdh-tools     *
4041776c5c5Sdh-tools     * @param array $map
4051776c5c5Sdh-tools     * @return array
4061776c5c5Sdh-tools     */
4071776c5c5Sdh-tools    protected function findFontEntries(array $map)
4081776c5c5Sdh-tools    {
4091776c5c5Sdh-tools        $fonts = [];
4101776c5c5Sdh-tools        foreach ($map as $relative => $original) {
4111776c5c5Sdh-tools            if (!str_starts_with($relative, 'font/')) continue;
4121776c5c5Sdh-tools            $extension = strtolower(pathinfo($relative, PATHINFO_EXTENSION));
4131776c5c5Sdh-tools            if (!in_array($extension, ['eot', 'svg', 'ttf', 'woff', 'woff2'], true)) continue;
4141776c5c5Sdh-tools            $fonts[$relative] = $original;
4151776c5c5Sdh-tools        }
4161776c5c5Sdh-tools
4171776c5c5Sdh-tools        return $fonts;
4181776c5c5Sdh-tools    }
4191776c5c5Sdh-tools
4201776c5c5Sdh-tools    /**
4211776c5c5Sdh-tools     * Read a single entry from the archive.
4221776c5c5Sdh-tools     *
4231776c5c5Sdh-tools     * @param ZipArchive $zip
4241776c5c5Sdh-tools     * @param string $entryName
4251776c5c5Sdh-tools     * @return string
4261776c5c5Sdh-tools     */
4271776c5c5Sdh-tools    protected function openArchive($tmpName)
4281776c5c5Sdh-tools    {
4291776c5c5Sdh-tools        if (class_exists('ZipArchive')) {
4301776c5c5Sdh-tools            $zip = new ZipArchive();
4311776c5c5Sdh-tools            if ($zip->open($tmpName) === true) {
4321776c5c5Sdh-tools                $names = [];
4331776c5c5Sdh-tools                for ($i = 0; $i < $zip->numFiles; $i++) {
4341776c5c5Sdh-tools                    $names[] = $zip->getNameIndex($i);
4351776c5c5Sdh-tools                }
4361776c5c5Sdh-tools                return [
4371776c5c5Sdh-tools                    'type' => 'ziparchive',
4381776c5c5Sdh-tools                    'handle' => $zip,
4391776c5c5Sdh-tools                    'map' => $this->buildArchiveMap($names),
4401776c5c5Sdh-tools                ];
4411776c5c5Sdh-tools            }
4421776c5c5Sdh-tools        }
4431776c5c5Sdh-tools
4441776c5c5Sdh-tools        if ($this->canUseSystemZipTools()) {
4451776c5c5Sdh-tools            return [
4461776c5c5Sdh-tools                'type' => 'system',
4471776c5c5Sdh-tools                'path' => $tmpName,
4481776c5c5Sdh-tools                'map' => $this->buildArchiveMap($this->listArchiveEntries($tmpName)),
4491776c5c5Sdh-tools            ];
4501776c5c5Sdh-tools        }
4511776c5c5Sdh-tools
4521776c5c5Sdh-tools        throw new RuntimeException($this->getLang('err_zip_support'));
4531776c5c5Sdh-tools    }
4541776c5c5Sdh-tools
4551776c5c5Sdh-tools    /**
4561776c5c5Sdh-tools     * Close an open archive handle when needed.
4571776c5c5Sdh-tools     *
4581776c5c5Sdh-tools     * @param array $archive
4591776c5c5Sdh-tools     * @return void
4601776c5c5Sdh-tools     */
4611776c5c5Sdh-tools    protected function closeArchive(array $archive)
4621776c5c5Sdh-tools    {
4631776c5c5Sdh-tools        if (($archive['type'] ?? '') === 'ziparchive' && isset($archive['handle'])) {
4641776c5c5Sdh-tools            $archive['handle']->close();
4651776c5c5Sdh-tools        }
4661776c5c5Sdh-tools    }
4671776c5c5Sdh-tools
4681776c5c5Sdh-tools    /**
4691776c5c5Sdh-tools     * Read a single entry from the archive.
4701776c5c5Sdh-tools     *
4711776c5c5Sdh-tools     * @param array $archive
4721776c5c5Sdh-tools     * @param string $entryName
4731776c5c5Sdh-tools     * @return string
4741776c5c5Sdh-tools     */
4751776c5c5Sdh-tools    protected function readArchiveEntry(array $archive, $entryName)
4761776c5c5Sdh-tools    {
4771776c5c5Sdh-tools        if (($archive['type'] ?? '') === 'ziparchive') {
4781776c5c5Sdh-tools            $content = $archive['handle']->getFromName($entryName);
4791776c5c5Sdh-tools            if ($content === false) {
4801776c5c5Sdh-tools                throw new RuntimeException(sprintf($this->getLang('err_archive_read'), $entryName));
4811776c5c5Sdh-tools            }
4821776c5c5Sdh-tools            return $content;
4831776c5c5Sdh-tools        }
4841776c5c5Sdh-tools
4851776c5c5Sdh-tools        $command = 'unzip -p ' . escapeshellarg($archive['path']) . ' ' . escapeshellarg($entryName);
4861776c5c5Sdh-tools        $descriptorSpec = [
4871776c5c5Sdh-tools            1 => ['pipe', 'w'],
4881776c5c5Sdh-tools            2 => ['pipe', 'w'],
4891776c5c5Sdh-tools        ];
4901776c5c5Sdh-tools        $process = proc_open($command, $descriptorSpec, $pipes);
4911776c5c5Sdh-tools        if (!is_resource($process)) {
4921776c5c5Sdh-tools            throw new RuntimeException($this->getLang('err_zip_open'));
4931776c5c5Sdh-tools        }
4941776c5c5Sdh-tools
4951776c5c5Sdh-tools        $content = stream_get_contents($pipes[1]);
4961776c5c5Sdh-tools        $error = stream_get_contents($pipes[2]);
4971776c5c5Sdh-tools        fclose($pipes[1]);
4981776c5c5Sdh-tools        fclose($pipes[2]);
4991776c5c5Sdh-tools        $exitCode = proc_close($process);
5001776c5c5Sdh-tools
5011776c5c5Sdh-tools        if ($exitCode !== 0) {
502*95357802SDaniel Hofer            $message = trim($error) !== '' ? trim($error) : sprintf($this->getLang('err_archive_read'), $entryName);
503*95357802SDaniel Hofer            throw new RuntimeException($message);
5041776c5c5Sdh-tools        }
5051776c5c5Sdh-tools
5061776c5c5Sdh-tools        return $content;
5071776c5c5Sdh-tools    }
5081776c5c5Sdh-tools
5091776c5c5Sdh-tools    /**
5101776c5c5Sdh-tools     * List archive entries using zipinfo.
5111776c5c5Sdh-tools     *
5121776c5c5Sdh-tools     * @param string $tmpName
5131776c5c5Sdh-tools     * @return array
5141776c5c5Sdh-tools     */
5151776c5c5Sdh-tools    protected function listArchiveEntries($tmpName)
5161776c5c5Sdh-tools    {
5171776c5c5Sdh-tools        $output = [];
5181776c5c5Sdh-tools        $exitCode = 0;
5191776c5c5Sdh-tools        exec('zipinfo -1 ' . escapeshellarg($tmpName), $output, $exitCode);
5201776c5c5Sdh-tools        if ($exitCode !== 0) {
5211776c5c5Sdh-tools            throw new RuntimeException($this->getLang('err_zip_open'));
5221776c5c5Sdh-tools        }
5231776c5c5Sdh-tools
5241776c5c5Sdh-tools        return $output;
5251776c5c5Sdh-tools    }
5261776c5c5Sdh-tools
5271776c5c5Sdh-tools    /**
5281776c5c5Sdh-tools     * Check whether system ZIP tools can be used as a fallback.
5291776c5c5Sdh-tools     *
5301776c5c5Sdh-tools     * @return bool
5311776c5c5Sdh-tools     */
5321776c5c5Sdh-tools    protected function canUseSystemZipTools()
5331776c5c5Sdh-tools    {
5341776c5c5Sdh-tools        if (!function_exists('exec') || !function_exists('proc_open')) return false;
5351776c5c5Sdh-tools
5361776c5c5Sdh-tools        return $this->commandExists('unzip') && $this->commandExists('zipinfo');
5371776c5c5Sdh-tools    }
5381776c5c5Sdh-tools
5391776c5c5Sdh-tools    /**
5401776c5c5Sdh-tools     * Check whether a shell command exists.
5411776c5c5Sdh-tools     *
5421776c5c5Sdh-tools     * @param string $command
5431776c5c5Sdh-tools     * @return bool
5441776c5c5Sdh-tools     */
5451776c5c5Sdh-tools    protected function commandExists($command)
5461776c5c5Sdh-tools    {
5471776c5c5Sdh-tools        $output = [];
5481776c5c5Sdh-tools        $exitCode = 0;
5491776c5c5Sdh-tools        exec('command -v ' . escapeshellarg($command), $output, $exitCode);
5501776c5c5Sdh-tools        return $exitCode === 0 && !empty($output);
5511776c5c5Sdh-tools    }
5521776c5c5Sdh-tools
5531776c5c5Sdh-tools    /**
5541776c5c5Sdh-tools     * Remove the current active package and recreate the base directory.
5551776c5c5Sdh-tools     *
5561776c5c5Sdh-tools     * @return void
5571776c5c5Sdh-tools     */
5581776c5c5Sdh-tools    protected function resetActiveDirectory()
5591776c5c5Sdh-tools    {
5601776c5c5Sdh-tools        if (file_exists(self::ACTIVE_DIR)) {
5611776c5c5Sdh-tools            io_rmdir(self::ACTIVE_DIR, true);
5621776c5c5Sdh-tools        }
5631776c5c5Sdh-tools
5641776c5c5Sdh-tools        io_mkdir_p(self::ACTIVE_DIR);
5651776c5c5Sdh-tools    }
5661776c5c5Sdh-tools
5671776c5c5Sdh-tools    /**
5681776c5c5Sdh-tools     * Expire DokuWiki render and asset caches after package changes.
5691776c5c5Sdh-tools     *
5701776c5c5Sdh-tools     * DokuWiki's extension manager uses the same local.php touch pattern.
5711776c5c5Sdh-tools     *
5721776c5c5Sdh-tools     * @return void
5731776c5c5Sdh-tools     */
5741776c5c5Sdh-tools    protected function purgeDokuWikiCaches()
5751776c5c5Sdh-tools    {
5761776c5c5Sdh-tools        global $config_cascade;
5771776c5c5Sdh-tools
5781776c5c5Sdh-tools        $localConfig = reset($config_cascade['main']['local']);
5791776c5c5Sdh-tools        if ($localConfig) {
5801776c5c5Sdh-tools            @touch($localConfig);
5811776c5c5Sdh-tools        }
5821776c5c5Sdh-tools    }
5831776c5c5Sdh-tools
5841776c5c5Sdh-tools    /**
5851776c5c5Sdh-tools     * Generate the public stylesheet from config data.
5861776c5c5Sdh-tools     *
5871776c5c5Sdh-tools     * @param array $config
5881776c5c5Sdh-tools     * @param array $fontFiles
5891776c5c5Sdh-tools     * @return string
5901776c5c5Sdh-tools     */
5911776c5c5Sdh-tools    protected function buildCss(array $config, array $fontFiles)
5921776c5c5Sdh-tools    {
5931776c5c5Sdh-tools        $family = 'fontello';
5941776c5c5Sdh-tools        $icons = $this->extractIcons($config);
5951776c5c5Sdh-tools        $sources = [];
5961776c5c5Sdh-tools
5971776c5c5Sdh-tools        $formatMap = [
5981776c5c5Sdh-tools            'eot' => 'embedded-opentype',
5991776c5c5Sdh-tools            'woff2' => 'woff2',
6001776c5c5Sdh-tools            'woff' => 'woff',
6011776c5c5Sdh-tools            'ttf' => 'truetype',
6021776c5c5Sdh-tools            'svg' => 'svg',
6031776c5c5Sdh-tools        ];
6041776c5c5Sdh-tools        $priority = ['eot', 'woff2', 'woff', 'ttf', 'svg'];
6051776c5c5Sdh-tools
6061776c5c5Sdh-tools        foreach ($priority as $extension) {
6071776c5c5Sdh-tools            foreach ($fontFiles as $fontFile) {
6081776c5c5Sdh-tools                if (strtolower(pathinfo($fontFile, PATHINFO_EXTENSION)) !== $extension) continue;
6091776c5c5Sdh-tools                $url = "../font/$fontFile";
6101776c5c5Sdh-tools                if ($extension === 'svg') {
6111776c5c5Sdh-tools                    $url .= '#' . $family;
6121776c5c5Sdh-tools                }
6131776c5c5Sdh-tools                $sources[] = "url('$url') format('" . $formatMap[$extension] . "')";
6141776c5c5Sdh-tools            }
6151776c5c5Sdh-tools        }
6161776c5c5Sdh-tools
6171776c5c5Sdh-tools        $css = "@font-face {\n";
6181776c5c5Sdh-tools        $css .= "  font-family: '$family';\n";
6191776c5c5Sdh-tools        $css .= '  src: ' . implode(",\n       ", $sources) . ";\n";
6201776c5c5Sdh-tools        $css .= "  font-weight: normal;\n";
6211776c5c5Sdh-tools        $css .= "  font-style: normal;\n";
6221776c5c5Sdh-tools        $css .= "}\n\n";
6231776c5c5Sdh-tools        $css .= ".fontello-icon {\n";
6241776c5c5Sdh-tools        $css .= "  display: inline-block;\n";
6251776c5c5Sdh-tools        $css .= "}\n\n";
6261776c5c5Sdh-tools        $css .= ".fontello-icon:before {\n";
6271776c5c5Sdh-tools        $css .= "  font-family: '$family';\n";
6281776c5c5Sdh-tools        $css .= "  font-style: normal;\n";
6291776c5c5Sdh-tools        $css .= "  font-weight: normal;\n";
6301776c5c5Sdh-tools        $css .= "  speak: never;\n";
6311776c5c5Sdh-tools        $css .= "  display: inline-block;\n";
6321776c5c5Sdh-tools        $css .= "  text-decoration: inherit;\n";
6331776c5c5Sdh-tools        $css .= "  width: 1em;\n";
6341776c5c5Sdh-tools        $css .= "  margin-right: .2em;\n";
6351776c5c5Sdh-tools        $css .= "  text-align: center;\n";
6361776c5c5Sdh-tools        $css .= "  font-variant: normal;\n";
6371776c5c5Sdh-tools        $css .= "  text-transform: none;\n";
6381776c5c5Sdh-tools        $css .= "  line-height: 1em;\n";
6391776c5c5Sdh-tools        $css .= "  margin-left: .2em;\n";
6401776c5c5Sdh-tools        $css .= "  -webkit-font-smoothing: antialiased;\n";
6411776c5c5Sdh-tools        $css .= "  -moz-osx-font-smoothing: grayscale;\n";
6421776c5c5Sdh-tools        $css .= "}\n\n";
6431776c5c5Sdh-tools
6441776c5c5Sdh-tools        foreach ($icons as $icon) {
6451776c5c5Sdh-tools            $css .= '.fontello-icon.' . $icon['class'] . ':before { content: "\\' . $icon['code'] . "\"; }\n";
6461776c5c5Sdh-tools        }
6471776c5c5Sdh-tools
6481776c5c5Sdh-tools        return $css;
6491776c5c5Sdh-tools    }
6501776c5c5Sdh-tools
6511776c5c5Sdh-tools    /**
6521776c5c5Sdh-tools     * Load a JSON file from disk.
6531776c5c5Sdh-tools     *
6541776c5c5Sdh-tools     * @param string $file
6551776c5c5Sdh-tools     * @return array|null
6561776c5c5Sdh-tools     */
6571776c5c5Sdh-tools    protected function loadJsonFile($file)
6581776c5c5Sdh-tools    {
6591776c5c5Sdh-tools        if (!file_exists($file)) return null;
6601776c5c5Sdh-tools
6611776c5c5Sdh-tools        $json = file_get_contents($file);
6621776c5c5Sdh-tools        if ($json === false) return null;
6631776c5c5Sdh-tools
6641776c5c5Sdh-tools        $decoded = json_decode($json, true);
6651776c5c5Sdh-tools        return is_array($decoded) ? $decoded : null;
6661776c5c5Sdh-tools    }
6671776c5c5Sdh-tools
6681776c5c5Sdh-tools    /**
6691776c5c5Sdh-tools     * Translate PHP upload error codes.
6701776c5c5Sdh-tools     *
6711776c5c5Sdh-tools     * @param int $error
6721776c5c5Sdh-tools     * @return string
6731776c5c5Sdh-tools     */
6741776c5c5Sdh-tools    protected function uploadErrorMessage($error)
6751776c5c5Sdh-tools    {
6761776c5c5Sdh-tools        return match ($error) {
6771776c5c5Sdh-tools            UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => $this->getLang('err_upload_too_large'),
6781776c5c5Sdh-tools            UPLOAD_ERR_PARTIAL => $this->getLang('err_upload_partial'),
6791776c5c5Sdh-tools            UPLOAD_ERR_NO_FILE => $this->getLang('err_upload_missing'),
6801776c5c5Sdh-tools            default => $this->getLang('err_upload_generic'),
6811776c5c5Sdh-tools        };
6821776c5c5Sdh-tools    }
6831776c5c5Sdh-tools}
684