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