1*1776c5c5Sdh-tools<?php 2*1776c5c5Sdh-tools 3*1776c5c5Sdh-toolsuse dokuwiki\Extension\Plugin; 4*1776c5c5Sdh-tools 5*1776c5c5Sdh-tools/** 6*1776c5c5Sdh-tools * Shared logic for the Fontello plugin. 7*1776c5c5Sdh-tools */ 8*1776c5c5Sdh-toolsclass helper_plugin_fontello extends Plugin 9*1776c5c5Sdh-tools{ 10*1776c5c5Sdh-tools protected const ACTIVE_DIR = DOKU_PLUGIN . 'fontello/assets/active'; 11*1776c5c5Sdh-tools protected const ACTIVE_CSS = self::ACTIVE_DIR . '/css/fontello.css'; 12*1776c5c5Sdh-tools protected const ACTIVE_CONFIG = self::ACTIVE_DIR . '/config.json'; 13*1776c5c5Sdh-tools protected const ACTIVE_MANIFEST = self::ACTIVE_DIR . '/manifest.json'; 14*1776c5c5Sdh-tools protected const ACTIVE_ENABLED = self::ACTIVE_DIR . '/enabled.json'; 15*1776c5c5Sdh-tools protected const ACTIVE_FONT_DIR = self::ACTIVE_DIR . '/font'; 16*1776c5c5Sdh-tools 17*1776c5c5Sdh-tools /** 18*1776c5c5Sdh-tools * Returns true when an active package is available. 19*1776c5c5Sdh-tools * 20*1776c5c5Sdh-tools * @return bool 21*1776c5c5Sdh-tools */ 22*1776c5c5Sdh-tools public function hasActivePackage() 23*1776c5c5Sdh-tools { 24*1776c5c5Sdh-tools return file_exists(self::ACTIVE_CONFIG) && file_exists(self::ACTIVE_CSS); 25*1776c5c5Sdh-tools } 26*1776c5c5Sdh-tools 27*1776c5c5Sdh-tools /** 28*1776c5c5Sdh-tools * Returns the public URL to the generated stylesheet. 29*1776c5c5Sdh-tools * 30*1776c5c5Sdh-tools * @return string 31*1776c5c5Sdh-tools */ 32*1776c5c5Sdh-tools public function getCssUrl() 33*1776c5c5Sdh-tools { 34*1776c5c5Sdh-tools $mtime = @filemtime(self::ACTIVE_CSS) ?: time(); 35*1776c5c5Sdh-tools return DOKU_BASE . 'lib/plugins/fontello/assets/active/css/fontello.css?v=' . $mtime; 36*1776c5c5Sdh-tools } 37*1776c5c5Sdh-tools 38*1776c5c5Sdh-tools /** 39*1776c5c5Sdh-tools * Load the currently active package information. 40*1776c5c5Sdh-tools * 41*1776c5c5Sdh-tools * @return array|null 42*1776c5c5Sdh-tools */ 43*1776c5c5Sdh-tools public function getPackageInfo() 44*1776c5c5Sdh-tools { 45*1776c5c5Sdh-tools if (!$this->hasActivePackage()) return null; 46*1776c5c5Sdh-tools 47*1776c5c5Sdh-tools $config = $this->loadJsonFile(self::ACTIVE_CONFIG); 48*1776c5c5Sdh-tools if ($config === null) return null; 49*1776c5c5Sdh-tools 50*1776c5c5Sdh-tools $manifest = $this->loadJsonFile(self::ACTIVE_MANIFEST) ?: []; 51*1776c5c5Sdh-tools $prefix = (string) ($config['css_prefix_text'] ?? 'icon-'); 52*1776c5c5Sdh-tools $icons = $this->extractIcons($config); 53*1776c5c5Sdh-tools $enabledNames = $this->loadEnabledIconNames($icons); 54*1776c5c5Sdh-tools $enabledMap = array_fill_keys($enabledNames, true); 55*1776c5c5Sdh-tools 56*1776c5c5Sdh-tools foreach ($icons as $index => $icon) { 57*1776c5c5Sdh-tools $icons[$index]['enabled'] = isset($enabledMap[$icon['name']]); 58*1776c5c5Sdh-tools } 59*1776c5c5Sdh-tools 60*1776c5c5Sdh-tools return [ 61*1776c5c5Sdh-tools 'prefix' => $prefix, 62*1776c5c5Sdh-tools 'icons' => $icons, 63*1776c5c5Sdh-tools 'icon_count' => count($icons), 64*1776c5c5Sdh-tools 'enabled_count' => count($enabledNames), 65*1776c5c5Sdh-tools 'font_files' => $manifest['font_files'] ?? [], 66*1776c5c5Sdh-tools 'imported_at' => $manifest['imported_at'] ?? null, 67*1776c5c5Sdh-tools 'zip_name' => $manifest['zip_name'] ?? '', 68*1776c5c5Sdh-tools ]; 69*1776c5c5Sdh-tools } 70*1776c5c5Sdh-tools 71*1776c5c5Sdh-tools /** 72*1776c5c5Sdh-tools * Return all active icons for toolbar or picker integrations. 73*1776c5c5Sdh-tools * 74*1776c5c5Sdh-tools * @return array 75*1776c5c5Sdh-tools */ 76*1776c5c5Sdh-tools public function getActiveIcons() 77*1776c5c5Sdh-tools { 78*1776c5c5Sdh-tools $package = $this->getPackageInfo(); 79*1776c5c5Sdh-tools if ($package === null) return []; 80*1776c5c5Sdh-tools 81*1776c5c5Sdh-tools return array_values(array_filter($package['icons'], static function ($icon) { 82*1776c5c5Sdh-tools return !empty($icon['enabled']); 83*1776c5c5Sdh-tools })); 84*1776c5c5Sdh-tools } 85*1776c5c5Sdh-tools 86*1776c5c5Sdh-tools /** 87*1776c5c5Sdh-tools * Check if the given icon exists in the active package. 88*1776c5c5Sdh-tools * 89*1776c5c5Sdh-tools * @param string $iconName 90*1776c5c5Sdh-tools * @return bool 91*1776c5c5Sdh-tools */ 92*1776c5c5Sdh-tools public function hasIcon($iconName) 93*1776c5c5Sdh-tools { 94*1776c5c5Sdh-tools return $this->getIconClass($iconName) !== null; 95*1776c5c5Sdh-tools } 96*1776c5c5Sdh-tools 97*1776c5c5Sdh-tools /** 98*1776c5c5Sdh-tools * Return the CSS class for an icon name. 99*1776c5c5Sdh-tools * 100*1776c5c5Sdh-tools * @param string $iconName 101*1776c5c5Sdh-tools * @return string|null 102*1776c5c5Sdh-tools */ 103*1776c5c5Sdh-tools public function getIconClass($iconName) 104*1776c5c5Sdh-tools { 105*1776c5c5Sdh-tools $package = $this->getPackageInfo(); 106*1776c5c5Sdh-tools if ($package === null) return null; 107*1776c5c5Sdh-tools 108*1776c5c5Sdh-tools foreach ($package['icons'] as $icon) { 109*1776c5c5Sdh-tools if ($icon['name'] === $iconName) return $icon['class']; 110*1776c5c5Sdh-tools } 111*1776c5c5Sdh-tools 112*1776c5c5Sdh-tools return null; 113*1776c5c5Sdh-tools } 114*1776c5c5Sdh-tools 115*1776c5c5Sdh-tools /** 116*1776c5c5Sdh-tools * Parse a Fontello icon token. 117*1776c5c5Sdh-tools * 118*1776c5c5Sdh-tools * @param string $token 119*1776c5c5Sdh-tools * @return array|null 120*1776c5c5Sdh-tools */ 121*1776c5c5Sdh-tools public function parseIconToken($token) 122*1776c5c5Sdh-tools { 123*1776c5c5Sdh-tools if (!preg_match('/^<icon:([A-Za-z0-9_-]+)((?:\|[A-Za-z0-9_-]+)*)>$/', $token, $match)) { 124*1776c5c5Sdh-tools return null; 125*1776c5c5Sdh-tools } 126*1776c5c5Sdh-tools 127*1776c5c5Sdh-tools $flags = []; 128*1776c5c5Sdh-tools if ($match[2] !== '') { 129*1776c5c5Sdh-tools foreach (explode('|', ltrim($match[2], '|')) as $flag) { 130*1776c5c5Sdh-tools if ($flag === '') continue; 131*1776c5c5Sdh-tools if (!in_array($flag, ['toc', 'notoc'], true)) return null; 132*1776c5c5Sdh-tools $flags[$flag] = true; 133*1776c5c5Sdh-tools } 134*1776c5c5Sdh-tools } 135*1776c5c5Sdh-tools 136*1776c5c5Sdh-tools return [ 137*1776c5c5Sdh-tools 'raw' => $token, 138*1776c5c5Sdh-tools 'name' => $match[1], 139*1776c5c5Sdh-tools 'flags' => $flags, 140*1776c5c5Sdh-tools 'toc' => isset($flags['toc']), 141*1776c5c5Sdh-tools 'notoc' => isset($flags['notoc']), 142*1776c5c5Sdh-tools ]; 143*1776c5c5Sdh-tools } 144*1776c5c5Sdh-tools 145*1776c5c5Sdh-tools /** 146*1776c5c5Sdh-tools * Return the XHTML markup for a known icon. 147*1776c5c5Sdh-tools * 148*1776c5c5Sdh-tools * @param string $iconName 149*1776c5c5Sdh-tools * @return string|null 150*1776c5c5Sdh-tools */ 151*1776c5c5Sdh-tools public function renderIconXhtml($iconName) 152*1776c5c5Sdh-tools { 153*1776c5c5Sdh-tools $class = $this->getIconClass($iconName); 154*1776c5c5Sdh-tools if ($class === null) return null; 155*1776c5c5Sdh-tools 156*1776c5c5Sdh-tools return '<span class="fontello-icon ' . hsc($class) . '" aria-hidden="true"></span>'; 157*1776c5c5Sdh-tools } 158*1776c5c5Sdh-tools 159*1776c5c5Sdh-tools /** 160*1776c5c5Sdh-tools * Decide whether a parsed icon token should remain visible in the TOC. 161*1776c5c5Sdh-tools * 162*1776c5c5Sdh-tools * @param array $token 163*1776c5c5Sdh-tools * @return bool 164*1776c5c5Sdh-tools */ 165*1776c5c5Sdh-tools public function iconTokenShowsInToc(array $token) 166*1776c5c5Sdh-tools { 167*1776c5c5Sdh-tools if (!empty($token['notoc'])) return false; 168*1776c5c5Sdh-tools if (!empty($token['toc'])) return true; 169*1776c5c5Sdh-tools 170*1776c5c5Sdh-tools return (bool) $this->getConf('showInToc'); 171*1776c5c5Sdh-tools } 172*1776c5c5Sdh-tools 173*1776c5c5Sdh-tools /** 174*1776c5c5Sdh-tools * Persist which icons should be offered in toolbar or picker integrations. 175*1776c5c5Sdh-tools * 176*1776c5c5Sdh-tools * This does not affect inline rendering of imported icons. 177*1776c5c5Sdh-tools * 178*1776c5c5Sdh-tools * @param array $iconNames 179*1776c5c5Sdh-tools * @return int 180*1776c5c5Sdh-tools */ 181*1776c5c5Sdh-tools public function saveEnabledIconNames(array $iconNames) 182*1776c5c5Sdh-tools { 183*1776c5c5Sdh-tools $package = $this->getPackageInfo(); 184*1776c5c5Sdh-tools if ($package === null) { 185*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_no_package')); 186*1776c5c5Sdh-tools } 187*1776c5c5Sdh-tools 188*1776c5c5Sdh-tools $requested = array_fill_keys(array_map('strval', $iconNames), true); 189*1776c5c5Sdh-tools $enabled = []; 190*1776c5c5Sdh-tools 191*1776c5c5Sdh-tools foreach ($package['icons'] as $icon) { 192*1776c5c5Sdh-tools if (isset($requested[$icon['name']])) { 193*1776c5c5Sdh-tools $enabled[] = $icon['name']; 194*1776c5c5Sdh-tools } 195*1776c5c5Sdh-tools } 196*1776c5c5Sdh-tools 197*1776c5c5Sdh-tools io_makeFileDir(self::ACTIVE_ENABLED); 198*1776c5c5Sdh-tools file_put_contents(self::ACTIVE_ENABLED, json_encode($enabled, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 199*1776c5c5Sdh-tools 200*1776c5c5Sdh-tools return count($enabled); 201*1776c5c5Sdh-tools } 202*1776c5c5Sdh-tools 203*1776c5c5Sdh-tools /** 204*1776c5c5Sdh-tools * Import a Fontello ZIP package. 205*1776c5c5Sdh-tools * 206*1776c5c5Sdh-tools * @param array $upload 207*1776c5c5Sdh-tools * @return array 208*1776c5c5Sdh-tools */ 209*1776c5c5Sdh-tools public function importPackage(array $upload) 210*1776c5c5Sdh-tools { 211*1776c5c5Sdh-tools if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { 212*1776c5c5Sdh-tools throw new RuntimeException($this->uploadErrorMessage((int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE))); 213*1776c5c5Sdh-tools } 214*1776c5c5Sdh-tools 215*1776c5c5Sdh-tools $tmpName = (string) ($upload['tmp_name'] ?? ''); 216*1776c5c5Sdh-tools if ($tmpName === '' || !is_uploaded_file($tmpName) && !file_exists($tmpName)) { 217*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_upload_missing')); 218*1776c5c5Sdh-tools } 219*1776c5c5Sdh-tools 220*1776c5c5Sdh-tools $archive = $this->openArchive($tmpName); 221*1776c5c5Sdh-tools $map = $archive['map']; 222*1776c5c5Sdh-tools $configEntry = $this->findRequiredEntry($map, 'config.json', $this->getLang('err_missing_config')); 223*1776c5c5Sdh-tools $this->findRequiredEntry($map, 'css/fontello.css', $this->getLang('err_missing_css')); 224*1776c5c5Sdh-tools $fontEntries = $this->findFontEntries($map); 225*1776c5c5Sdh-tools if ($fontEntries === []) { 226*1776c5c5Sdh-tools $this->closeArchive($archive); 227*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_missing_fonts')); 228*1776c5c5Sdh-tools } 229*1776c5c5Sdh-tools 230*1776c5c5Sdh-tools $configJson = $this->readArchiveEntry($archive, $configEntry); 231*1776c5c5Sdh-tools $config = json_decode($configJson, true); 232*1776c5c5Sdh-tools if (!is_array($config) || !isset($config['glyphs']) || !is_array($config['glyphs'])) { 233*1776c5c5Sdh-tools $this->closeArchive($archive); 234*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_invalid_config')); 235*1776c5c5Sdh-tools } 236*1776c5c5Sdh-tools 237*1776c5c5Sdh-tools $icons = $this->extractIcons($config); 238*1776c5c5Sdh-tools if ($icons === []) { 239*1776c5c5Sdh-tools $this->closeArchive($archive); 240*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_no_icons')); 241*1776c5c5Sdh-tools } 242*1776c5c5Sdh-tools 243*1776c5c5Sdh-tools $fontFiles = []; 244*1776c5c5Sdh-tools $fontContents = []; 245*1776c5c5Sdh-tools foreach ($fontEntries as $relative => $original) { 246*1776c5c5Sdh-tools $basename = basename($relative); 247*1776c5c5Sdh-tools $fontFiles[] = $basename; 248*1776c5c5Sdh-tools $fontContents[$basename] = $this->readArchiveEntry($archive, $original); 249*1776c5c5Sdh-tools } 250*1776c5c5Sdh-tools 251*1776c5c5Sdh-tools $manifest = [ 252*1776c5c5Sdh-tools 'zip_name' => (string) ($upload['name'] ?? ''), 253*1776c5c5Sdh-tools 'imported_at' => date('c'), 254*1776c5c5Sdh-tools 'prefix' => (string) ($config['css_prefix_text'] ?? 'icon-'), 255*1776c5c5Sdh-tools 'icon_count' => count($icons), 256*1776c5c5Sdh-tools 'font_files' => array_values($fontFiles), 257*1776c5c5Sdh-tools ]; 258*1776c5c5Sdh-tools 259*1776c5c5Sdh-tools $css = $this->buildCss($config, $fontFiles); 260*1776c5c5Sdh-tools 261*1776c5c5Sdh-tools $this->closeArchive($archive); 262*1776c5c5Sdh-tools $this->resetActiveDirectory(); 263*1776c5c5Sdh-tools 264*1776c5c5Sdh-tools foreach ($fontContents as $basename => $content) { 265*1776c5c5Sdh-tools $target = self::ACTIVE_FONT_DIR . '/' . $basename; 266*1776c5c5Sdh-tools io_makeFileDir($target); 267*1776c5c5Sdh-tools file_put_contents($target, $content); 268*1776c5c5Sdh-tools } 269*1776c5c5Sdh-tools 270*1776c5c5Sdh-tools io_makeFileDir(self::ACTIVE_CONFIG); 271*1776c5c5Sdh-tools file_put_contents(self::ACTIVE_CONFIG, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 272*1776c5c5Sdh-tools io_makeFileDir(self::ACTIVE_MANIFEST); 273*1776c5c5Sdh-tools file_put_contents(self::ACTIVE_MANIFEST, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 274*1776c5c5Sdh-tools io_makeFileDir(self::ACTIVE_ENABLED); 275*1776c5c5Sdh-tools file_put_contents(self::ACTIVE_ENABLED, json_encode(array_column($icons, 'name'), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 276*1776c5c5Sdh-tools io_makeFileDir(self::ACTIVE_CSS); 277*1776c5c5Sdh-tools file_put_contents(self::ACTIVE_CSS, $css); 278*1776c5c5Sdh-tools $this->purgeDokuWikiCaches(); 279*1776c5c5Sdh-tools 280*1776c5c5Sdh-tools return $this->getPackageInfo() ?: $manifest; 281*1776c5c5Sdh-tools } 282*1776c5c5Sdh-tools 283*1776c5c5Sdh-tools /** 284*1776c5c5Sdh-tools * Extract icon metadata from the package config. 285*1776c5c5Sdh-tools * 286*1776c5c5Sdh-tools * @param array $config 287*1776c5c5Sdh-tools * @return array 288*1776c5c5Sdh-tools */ 289*1776c5c5Sdh-tools protected function extractIcons(array $config) 290*1776c5c5Sdh-tools { 291*1776c5c5Sdh-tools $prefix = (string) ($config['css_prefix_text'] ?? 'icon-'); 292*1776c5c5Sdh-tools $icons = []; 293*1776c5c5Sdh-tools 294*1776c5c5Sdh-tools foreach ($config['glyphs'] ?? [] as $glyph) { 295*1776c5c5Sdh-tools $name = trim((string) ($glyph['css'] ?? '')); 296*1776c5c5Sdh-tools $code = $glyph['code'] ?? null; 297*1776c5c5Sdh-tools if ($name === '' || !is_numeric($code)) continue; 298*1776c5c5Sdh-tools 299*1776c5c5Sdh-tools $icon = [ 300*1776c5c5Sdh-tools 'name' => $name, 301*1776c5c5Sdh-tools 'class' => $prefix . $name, 302*1776c5c5Sdh-tools 'code' => strtolower(dechex((int) $code)), 303*1776c5c5Sdh-tools ]; 304*1776c5c5Sdh-tools 305*1776c5c5Sdh-tools // Fontello packages may contain duplicate css names; keep the last one. 306*1776c5c5Sdh-tools $icons[$icon['class']] = $icon; 307*1776c5c5Sdh-tools } 308*1776c5c5Sdh-tools 309*1776c5c5Sdh-tools $icons = array_values($icons); 310*1776c5c5Sdh-tools 311*1776c5c5Sdh-tools usort($icons, static function ($left, $right) { 312*1776c5c5Sdh-tools return strcmp($left['name'], $right['name']); 313*1776c5c5Sdh-tools }); 314*1776c5c5Sdh-tools 315*1776c5c5Sdh-tools return $icons; 316*1776c5c5Sdh-tools } 317*1776c5c5Sdh-tools 318*1776c5c5Sdh-tools /** 319*1776c5c5Sdh-tools * Load enabled icon names. Missing or invalid state means all icons are enabled. 320*1776c5c5Sdh-tools * 321*1776c5c5Sdh-tools * @param array $icons 322*1776c5c5Sdh-tools * @return array 323*1776c5c5Sdh-tools */ 324*1776c5c5Sdh-tools protected function loadEnabledIconNames(array $icons) 325*1776c5c5Sdh-tools { 326*1776c5c5Sdh-tools $allNames = array_column($icons, 'name'); 327*1776c5c5Sdh-tools $enabled = $this->loadJsonFile(self::ACTIVE_ENABLED); 328*1776c5c5Sdh-tools if ($enabled === null || array_values($enabled) !== $enabled) return $allNames; 329*1776c5c5Sdh-tools 330*1776c5c5Sdh-tools $known = array_fill_keys($allNames, true); 331*1776c5c5Sdh-tools $names = []; 332*1776c5c5Sdh-tools 333*1776c5c5Sdh-tools foreach ($enabled as $name) { 334*1776c5c5Sdh-tools $name = (string) $name; 335*1776c5c5Sdh-tools if (isset($known[$name])) { 336*1776c5c5Sdh-tools $names[$name] = true; 337*1776c5c5Sdh-tools } 338*1776c5c5Sdh-tools } 339*1776c5c5Sdh-tools 340*1776c5c5Sdh-tools return array_keys($names); 341*1776c5c5Sdh-tools } 342*1776c5c5Sdh-tools 343*1776c5c5Sdh-tools /** 344*1776c5c5Sdh-tools * Build a normalized entry map for the archive. 345*1776c5c5Sdh-tools * 346*1776c5c5Sdh-tools * @param ZipArchive $zip 347*1776c5c5Sdh-tools * @return array 348*1776c5c5Sdh-tools */ 349*1776c5c5Sdh-tools protected function buildArchiveMap(array $originalNames) 350*1776c5c5Sdh-tools { 351*1776c5c5Sdh-tools $roots = []; 352*1776c5c5Sdh-tools $hasTopLevelFiles = false; 353*1776c5c5Sdh-tools 354*1776c5c5Sdh-tools foreach ($originalNames as $name) { 355*1776c5c5Sdh-tools $name = str_replace('\\', '/', $name); 356*1776c5c5Sdh-tools if (substr($name, -1) === '/') continue; 357*1776c5c5Sdh-tools $name = trim($name, '/'); 358*1776c5c5Sdh-tools if ($name === '') continue; 359*1776c5c5Sdh-tools $parts = explode('/', $name, 2); 360*1776c5c5Sdh-tools $roots[$parts[0]] = true; 361*1776c5c5Sdh-tools if (count($parts) === 1) $hasTopLevelFiles = true; 362*1776c5c5Sdh-tools } 363*1776c5c5Sdh-tools 364*1776c5c5Sdh-tools $stripRoot = count($roots) === 1 && !$hasTopLevelFiles; 365*1776c5c5Sdh-tools $map = []; 366*1776c5c5Sdh-tools 367*1776c5c5Sdh-tools foreach ($originalNames as $name) { 368*1776c5c5Sdh-tools $name = str_replace('\\', '/', $name); 369*1776c5c5Sdh-tools if (substr($name, -1) === '/') continue; 370*1776c5c5Sdh-tools $name = trim($name, '/'); 371*1776c5c5Sdh-tools if ($name === '') continue; 372*1776c5c5Sdh-tools $relative = $name; 373*1776c5c5Sdh-tools if ($stripRoot) { 374*1776c5c5Sdh-tools $relative = explode('/', $name, 2)[1] ?? ''; 375*1776c5c5Sdh-tools } 376*1776c5c5Sdh-tools if ($relative === '' || substr($relative, -1) === '/') continue; 377*1776c5c5Sdh-tools $map[$relative] = $name; 378*1776c5c5Sdh-tools } 379*1776c5c5Sdh-tools 380*1776c5c5Sdh-tools return $map; 381*1776c5c5Sdh-tools } 382*1776c5c5Sdh-tools 383*1776c5c5Sdh-tools /** 384*1776c5c5Sdh-tools * Find a required archive entry. 385*1776c5c5Sdh-tools * 386*1776c5c5Sdh-tools * @param array $map 387*1776c5c5Sdh-tools * @param string $relativePath 388*1776c5c5Sdh-tools * @param string $errorMessage 389*1776c5c5Sdh-tools * @return string 390*1776c5c5Sdh-tools */ 391*1776c5c5Sdh-tools protected function findRequiredEntry(array $map, $relativePath, $errorMessage) 392*1776c5c5Sdh-tools { 393*1776c5c5Sdh-tools if (!isset($map[$relativePath])) { 394*1776c5c5Sdh-tools throw new RuntimeException($errorMessage); 395*1776c5c5Sdh-tools } 396*1776c5c5Sdh-tools 397*1776c5c5Sdh-tools return $map[$relativePath]; 398*1776c5c5Sdh-tools } 399*1776c5c5Sdh-tools 400*1776c5c5Sdh-tools /** 401*1776c5c5Sdh-tools * Return all supported font entries. 402*1776c5c5Sdh-tools * 403*1776c5c5Sdh-tools * @param array $map 404*1776c5c5Sdh-tools * @return array 405*1776c5c5Sdh-tools */ 406*1776c5c5Sdh-tools protected function findFontEntries(array $map) 407*1776c5c5Sdh-tools { 408*1776c5c5Sdh-tools $fonts = []; 409*1776c5c5Sdh-tools foreach ($map as $relative => $original) { 410*1776c5c5Sdh-tools if (!str_starts_with($relative, 'font/')) continue; 411*1776c5c5Sdh-tools $extension = strtolower(pathinfo($relative, PATHINFO_EXTENSION)); 412*1776c5c5Sdh-tools if (!in_array($extension, ['eot', 'svg', 'ttf', 'woff', 'woff2'], true)) continue; 413*1776c5c5Sdh-tools $fonts[$relative] = $original; 414*1776c5c5Sdh-tools } 415*1776c5c5Sdh-tools 416*1776c5c5Sdh-tools return $fonts; 417*1776c5c5Sdh-tools } 418*1776c5c5Sdh-tools 419*1776c5c5Sdh-tools /** 420*1776c5c5Sdh-tools * Read a single entry from the archive. 421*1776c5c5Sdh-tools * 422*1776c5c5Sdh-tools * @param ZipArchive $zip 423*1776c5c5Sdh-tools * @param string $entryName 424*1776c5c5Sdh-tools * @return string 425*1776c5c5Sdh-tools */ 426*1776c5c5Sdh-tools protected function openArchive($tmpName) 427*1776c5c5Sdh-tools { 428*1776c5c5Sdh-tools if (class_exists('ZipArchive')) { 429*1776c5c5Sdh-tools $zip = new ZipArchive(); 430*1776c5c5Sdh-tools if ($zip->open($tmpName) === true) { 431*1776c5c5Sdh-tools $names = []; 432*1776c5c5Sdh-tools for ($i = 0; $i < $zip->numFiles; $i++) { 433*1776c5c5Sdh-tools $names[] = $zip->getNameIndex($i); 434*1776c5c5Sdh-tools } 435*1776c5c5Sdh-tools return [ 436*1776c5c5Sdh-tools 'type' => 'ziparchive', 437*1776c5c5Sdh-tools 'handle' => $zip, 438*1776c5c5Sdh-tools 'map' => $this->buildArchiveMap($names), 439*1776c5c5Sdh-tools ]; 440*1776c5c5Sdh-tools } 441*1776c5c5Sdh-tools } 442*1776c5c5Sdh-tools 443*1776c5c5Sdh-tools if ($this->canUseSystemZipTools()) { 444*1776c5c5Sdh-tools return [ 445*1776c5c5Sdh-tools 'type' => 'system', 446*1776c5c5Sdh-tools 'path' => $tmpName, 447*1776c5c5Sdh-tools 'map' => $this->buildArchiveMap($this->listArchiveEntries($tmpName)), 448*1776c5c5Sdh-tools ]; 449*1776c5c5Sdh-tools } 450*1776c5c5Sdh-tools 451*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_zip_support')); 452*1776c5c5Sdh-tools } 453*1776c5c5Sdh-tools 454*1776c5c5Sdh-tools /** 455*1776c5c5Sdh-tools * Close an open archive handle when needed. 456*1776c5c5Sdh-tools * 457*1776c5c5Sdh-tools * @param array $archive 458*1776c5c5Sdh-tools * @return void 459*1776c5c5Sdh-tools */ 460*1776c5c5Sdh-tools protected function closeArchive(array $archive) 461*1776c5c5Sdh-tools { 462*1776c5c5Sdh-tools if (($archive['type'] ?? '') === 'ziparchive' && isset($archive['handle'])) { 463*1776c5c5Sdh-tools $archive['handle']->close(); 464*1776c5c5Sdh-tools } 465*1776c5c5Sdh-tools } 466*1776c5c5Sdh-tools 467*1776c5c5Sdh-tools /** 468*1776c5c5Sdh-tools * Read a single entry from the archive. 469*1776c5c5Sdh-tools * 470*1776c5c5Sdh-tools * @param array $archive 471*1776c5c5Sdh-tools * @param string $entryName 472*1776c5c5Sdh-tools * @return string 473*1776c5c5Sdh-tools */ 474*1776c5c5Sdh-tools protected function readArchiveEntry(array $archive, $entryName) 475*1776c5c5Sdh-tools { 476*1776c5c5Sdh-tools if (($archive['type'] ?? '') === 'ziparchive') { 477*1776c5c5Sdh-tools $content = $archive['handle']->getFromName($entryName); 478*1776c5c5Sdh-tools if ($content === false) { 479*1776c5c5Sdh-tools throw new RuntimeException(sprintf($this->getLang('err_archive_read'), $entryName)); 480*1776c5c5Sdh-tools } 481*1776c5c5Sdh-tools return $content; 482*1776c5c5Sdh-tools } 483*1776c5c5Sdh-tools 484*1776c5c5Sdh-tools $command = 'unzip -p ' . escapeshellarg($archive['path']) . ' ' . escapeshellarg($entryName); 485*1776c5c5Sdh-tools $descriptorSpec = [ 486*1776c5c5Sdh-tools 1 => ['pipe', 'w'], 487*1776c5c5Sdh-tools 2 => ['pipe', 'w'], 488*1776c5c5Sdh-tools ]; 489*1776c5c5Sdh-tools $process = proc_open($command, $descriptorSpec, $pipes); 490*1776c5c5Sdh-tools if (!is_resource($process)) { 491*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_zip_open')); 492*1776c5c5Sdh-tools } 493*1776c5c5Sdh-tools 494*1776c5c5Sdh-tools $content = stream_get_contents($pipes[1]); 495*1776c5c5Sdh-tools $error = stream_get_contents($pipes[2]); 496*1776c5c5Sdh-tools fclose($pipes[1]); 497*1776c5c5Sdh-tools fclose($pipes[2]); 498*1776c5c5Sdh-tools $exitCode = proc_close($process); 499*1776c5c5Sdh-tools 500*1776c5c5Sdh-tools if ($exitCode !== 0) { 501*1776c5c5Sdh-tools throw new RuntimeException(trim($error) !== '' ? trim($error) : sprintf($this->getLang('err_archive_read'), $entryName)); 502*1776c5c5Sdh-tools } 503*1776c5c5Sdh-tools 504*1776c5c5Sdh-tools return $content; 505*1776c5c5Sdh-tools } 506*1776c5c5Sdh-tools 507*1776c5c5Sdh-tools /** 508*1776c5c5Sdh-tools * List archive entries using zipinfo. 509*1776c5c5Sdh-tools * 510*1776c5c5Sdh-tools * @param string $tmpName 511*1776c5c5Sdh-tools * @return array 512*1776c5c5Sdh-tools */ 513*1776c5c5Sdh-tools protected function listArchiveEntries($tmpName) 514*1776c5c5Sdh-tools { 515*1776c5c5Sdh-tools $output = []; 516*1776c5c5Sdh-tools $exitCode = 0; 517*1776c5c5Sdh-tools exec('zipinfo -1 ' . escapeshellarg($tmpName), $output, $exitCode); 518*1776c5c5Sdh-tools if ($exitCode !== 0) { 519*1776c5c5Sdh-tools throw new RuntimeException($this->getLang('err_zip_open')); 520*1776c5c5Sdh-tools } 521*1776c5c5Sdh-tools 522*1776c5c5Sdh-tools return $output; 523*1776c5c5Sdh-tools } 524*1776c5c5Sdh-tools 525*1776c5c5Sdh-tools /** 526*1776c5c5Sdh-tools * Check whether system ZIP tools can be used as a fallback. 527*1776c5c5Sdh-tools * 528*1776c5c5Sdh-tools * @return bool 529*1776c5c5Sdh-tools */ 530*1776c5c5Sdh-tools protected function canUseSystemZipTools() 531*1776c5c5Sdh-tools { 532*1776c5c5Sdh-tools if (!function_exists('exec') || !function_exists('proc_open')) return false; 533*1776c5c5Sdh-tools 534*1776c5c5Sdh-tools return $this->commandExists('unzip') && $this->commandExists('zipinfo'); 535*1776c5c5Sdh-tools } 536*1776c5c5Sdh-tools 537*1776c5c5Sdh-tools /** 538*1776c5c5Sdh-tools * Check whether a shell command exists. 539*1776c5c5Sdh-tools * 540*1776c5c5Sdh-tools * @param string $command 541*1776c5c5Sdh-tools * @return bool 542*1776c5c5Sdh-tools */ 543*1776c5c5Sdh-tools protected function commandExists($command) 544*1776c5c5Sdh-tools { 545*1776c5c5Sdh-tools $output = []; 546*1776c5c5Sdh-tools $exitCode = 0; 547*1776c5c5Sdh-tools exec('command -v ' . escapeshellarg($command), $output, $exitCode); 548*1776c5c5Sdh-tools return $exitCode === 0 && !empty($output); 549*1776c5c5Sdh-tools } 550*1776c5c5Sdh-tools 551*1776c5c5Sdh-tools /** 552*1776c5c5Sdh-tools * Remove the current active package and recreate the base directory. 553*1776c5c5Sdh-tools * 554*1776c5c5Sdh-tools * @return void 555*1776c5c5Sdh-tools */ 556*1776c5c5Sdh-tools protected function resetActiveDirectory() 557*1776c5c5Sdh-tools { 558*1776c5c5Sdh-tools if (file_exists(self::ACTIVE_DIR)) { 559*1776c5c5Sdh-tools io_rmdir(self::ACTIVE_DIR, true); 560*1776c5c5Sdh-tools } 561*1776c5c5Sdh-tools 562*1776c5c5Sdh-tools io_mkdir_p(self::ACTIVE_DIR); 563*1776c5c5Sdh-tools } 564*1776c5c5Sdh-tools 565*1776c5c5Sdh-tools /** 566*1776c5c5Sdh-tools * Expire DokuWiki render and asset caches after package changes. 567*1776c5c5Sdh-tools * 568*1776c5c5Sdh-tools * DokuWiki's extension manager uses the same local.php touch pattern. 569*1776c5c5Sdh-tools * 570*1776c5c5Sdh-tools * @return void 571*1776c5c5Sdh-tools */ 572*1776c5c5Sdh-tools protected function purgeDokuWikiCaches() 573*1776c5c5Sdh-tools { 574*1776c5c5Sdh-tools global $config_cascade; 575*1776c5c5Sdh-tools 576*1776c5c5Sdh-tools $localConfig = reset($config_cascade['main']['local']); 577*1776c5c5Sdh-tools if ($localConfig) { 578*1776c5c5Sdh-tools @touch($localConfig); 579*1776c5c5Sdh-tools } 580*1776c5c5Sdh-tools } 581*1776c5c5Sdh-tools 582*1776c5c5Sdh-tools /** 583*1776c5c5Sdh-tools * Generate the public stylesheet from config data. 584*1776c5c5Sdh-tools * 585*1776c5c5Sdh-tools * @param array $config 586*1776c5c5Sdh-tools * @param array $fontFiles 587*1776c5c5Sdh-tools * @return string 588*1776c5c5Sdh-tools */ 589*1776c5c5Sdh-tools protected function buildCss(array $config, array $fontFiles) 590*1776c5c5Sdh-tools { 591*1776c5c5Sdh-tools $family = 'fontello'; 592*1776c5c5Sdh-tools $icons = $this->extractIcons($config); 593*1776c5c5Sdh-tools $sources = []; 594*1776c5c5Sdh-tools 595*1776c5c5Sdh-tools $formatMap = [ 596*1776c5c5Sdh-tools 'eot' => 'embedded-opentype', 597*1776c5c5Sdh-tools 'woff2' => 'woff2', 598*1776c5c5Sdh-tools 'woff' => 'woff', 599*1776c5c5Sdh-tools 'ttf' => 'truetype', 600*1776c5c5Sdh-tools 'svg' => 'svg', 601*1776c5c5Sdh-tools ]; 602*1776c5c5Sdh-tools $priority = ['eot', 'woff2', 'woff', 'ttf', 'svg']; 603*1776c5c5Sdh-tools 604*1776c5c5Sdh-tools foreach ($priority as $extension) { 605*1776c5c5Sdh-tools foreach ($fontFiles as $fontFile) { 606*1776c5c5Sdh-tools if (strtolower(pathinfo($fontFile, PATHINFO_EXTENSION)) !== $extension) continue; 607*1776c5c5Sdh-tools $url = "../font/$fontFile"; 608*1776c5c5Sdh-tools if ($extension === 'svg') { 609*1776c5c5Sdh-tools $url .= '#' . $family; 610*1776c5c5Sdh-tools } 611*1776c5c5Sdh-tools $sources[] = "url('$url') format('" . $formatMap[$extension] . "')"; 612*1776c5c5Sdh-tools } 613*1776c5c5Sdh-tools } 614*1776c5c5Sdh-tools 615*1776c5c5Sdh-tools $css = "@font-face {\n"; 616*1776c5c5Sdh-tools $css .= " font-family: '$family';\n"; 617*1776c5c5Sdh-tools $css .= ' src: ' . implode(",\n ", $sources) . ";\n"; 618*1776c5c5Sdh-tools $css .= " font-weight: normal;\n"; 619*1776c5c5Sdh-tools $css .= " font-style: normal;\n"; 620*1776c5c5Sdh-tools $css .= "}\n\n"; 621*1776c5c5Sdh-tools $css .= ".fontello-icon {\n"; 622*1776c5c5Sdh-tools $css .= " display: inline-block;\n"; 623*1776c5c5Sdh-tools $css .= "}\n\n"; 624*1776c5c5Sdh-tools $css .= ".fontello-icon:before {\n"; 625*1776c5c5Sdh-tools $css .= " font-family: '$family';\n"; 626*1776c5c5Sdh-tools $css .= " font-style: normal;\n"; 627*1776c5c5Sdh-tools $css .= " font-weight: normal;\n"; 628*1776c5c5Sdh-tools $css .= " speak: never;\n"; 629*1776c5c5Sdh-tools $css .= " display: inline-block;\n"; 630*1776c5c5Sdh-tools $css .= " text-decoration: inherit;\n"; 631*1776c5c5Sdh-tools $css .= " width: 1em;\n"; 632*1776c5c5Sdh-tools $css .= " margin-right: .2em;\n"; 633*1776c5c5Sdh-tools $css .= " text-align: center;\n"; 634*1776c5c5Sdh-tools $css .= " font-variant: normal;\n"; 635*1776c5c5Sdh-tools $css .= " text-transform: none;\n"; 636*1776c5c5Sdh-tools $css .= " line-height: 1em;\n"; 637*1776c5c5Sdh-tools $css .= " margin-left: .2em;\n"; 638*1776c5c5Sdh-tools $css .= " -webkit-font-smoothing: antialiased;\n"; 639*1776c5c5Sdh-tools $css .= " -moz-osx-font-smoothing: grayscale;\n"; 640*1776c5c5Sdh-tools $css .= "}\n\n"; 641*1776c5c5Sdh-tools 642*1776c5c5Sdh-tools foreach ($icons as $icon) { 643*1776c5c5Sdh-tools $css .= '.fontello-icon.' . $icon['class'] . ':before { content: "\\' . $icon['code'] . "\"; }\n"; 644*1776c5c5Sdh-tools } 645*1776c5c5Sdh-tools 646*1776c5c5Sdh-tools return $css; 647*1776c5c5Sdh-tools } 648*1776c5c5Sdh-tools 649*1776c5c5Sdh-tools /** 650*1776c5c5Sdh-tools * Load a JSON file from disk. 651*1776c5c5Sdh-tools * 652*1776c5c5Sdh-tools * @param string $file 653*1776c5c5Sdh-tools * @return array|null 654*1776c5c5Sdh-tools */ 655*1776c5c5Sdh-tools protected function loadJsonFile($file) 656*1776c5c5Sdh-tools { 657*1776c5c5Sdh-tools if (!file_exists($file)) return null; 658*1776c5c5Sdh-tools 659*1776c5c5Sdh-tools $json = file_get_contents($file); 660*1776c5c5Sdh-tools if ($json === false) return null; 661*1776c5c5Sdh-tools 662*1776c5c5Sdh-tools $decoded = json_decode($json, true); 663*1776c5c5Sdh-tools return is_array($decoded) ? $decoded : null; 664*1776c5c5Sdh-tools } 665*1776c5c5Sdh-tools 666*1776c5c5Sdh-tools /** 667*1776c5c5Sdh-tools * Translate PHP upload error codes. 668*1776c5c5Sdh-tools * 669*1776c5c5Sdh-tools * @param int $error 670*1776c5c5Sdh-tools * @return string 671*1776c5c5Sdh-tools */ 672*1776c5c5Sdh-tools protected function uploadErrorMessage($error) 673*1776c5c5Sdh-tools { 674*1776c5c5Sdh-tools return match ($error) { 675*1776c5c5Sdh-tools UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => $this->getLang('err_upload_too_large'), 676*1776c5c5Sdh-tools UPLOAD_ERR_PARTIAL => $this->getLang('err_upload_partial'), 677*1776c5c5Sdh-tools UPLOAD_ERR_NO_FILE => $this->getLang('err_upload_missing'), 678*1776c5c5Sdh-tools default => $this->getLang('err_upload_generic'), 679*1776c5c5Sdh-tools }; 680*1776c5c5Sdh-tools } 681*1776c5c5Sdh-tools} 682