context = $this; $this->setText($text, $data, $quote, $allowPattern); } /** @var string */ protected $text; /** @var bool|array */ protected $quote; /** @var bool */ protected $allowPattern; function setText(?string $text, $data=null, $quote=true, bool $allowPattern=true): void { $this->text = $text; if ($data !== null) { $this->data = A::with($data); $this->quote = $quote; $this->allowPattern = $allowPattern; } } /** * Traiter la valeur $value selon la valeur de $quote * * D'abord la transformer en chaine, puis: * - si $quote===true, la mettre en échappement avec htmlspecialchars() * - si $quote===false ou null, ne pas la mettre en échappement * - sinon, ce doit être une fonction qui met la valeur en échappement */ private static final function quote($value, $quote) { if (A::is_array($value)) $value = print_r(A::with($value), true); elseif (!is_string($value)) $value = strval($value); if ($quote === true) return htmlspecialchars($value); else if ($quote === false || $quote === null) return $value; else return func::call($quote, $value); } /** * Traiter la valeur $value selon la valeur de $quote. Le traitement final est * fait avec la méthode quote() * * - Si $quote est un tableau, alors $quote[$name] est la valeur utilisée pour * décider comment traiter la valeur et sa valeur par défaut est true. * - Sinon prendre la valeur $quote telle quelle */ private static final function quote_nv(string $name, $value, $quote) { if (is_array($quote)) { if (isset($quote[$name])) { $value = self::quote($value, $quote[$name]); } else { # quoter par défaut quand on fournit un tableau $value = self::quote($value, true); } } else { $value = self::quote($value, $quote); } return $value; } /** * Résoudre la valeur du nom $name * - Si $name est de la forme '+keyp', prendre la valeur dans la configuration * - Si $name est de la forme '*keyp', prendre la valeur dans la session * - Sinon, $name est le chemin de clé dans le tableau $values */ private function resolve(string $name, ?array $values, $quote) { switch (substr($name, 0, 1)) { case "+": return $this->getConfig(substr($name, 1)); case "*": if (!session::started()) return ""; return $this->getSession(substr($name, 1)); default: $value = A::pget_s($values, $name, ""); $value = self::quote_nv($name, $value, $quote); return $value; } } function apply() { $text = $this->text; $data = $this->data; $quote = $this->quote; ## d'abord les remplacements complexes if ($this->allowPattern) { if ($data !== null) { $names = array_keys($data); $name_keys_pattern = '('.implode("|", $names).')((?:\.[a-zA-Z0-9_]+)*)'; $or_name_pattern = '|(?:(?:'.implode("|", $names).')(?:\.[a-zA-Z0-9_]+)*)'; } else { $name_keys_pattern = null; $or_name_pattern = ''; } # patterns conditionnels # autoriser un niveau de variable complexe à l'intérieur de la condition, i.e {{if(cond){{var.value}}}} $pattern = '/\{\{(if|unless)\(((?:[+*][a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)'.$or_name_pattern.')\)((?:[^{}]*(?:\{\{[^{}]*\}\})?(?:\{[^{}]*\})?)*)\}\}/s'; $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { $cond = $this->resolve($matches[2], $data, $quote); if ($matches[1] == "if") { if ($cond) $value = $matches[3]; else return ""; } elseif ($matches[1] == "unless") { if (!$cond) $value = $matches[3]; else return ""; } else { return $matches[0]; } $value = self::xml($value, $data, $quote); return $value; }, $text); # valeurs de config $pattern = '/\{\{(\+[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s'; $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { $name = $matches[1]; $value = $this->resolve($name, $data, $quote); return $value; }, $text); # valeurs de la session $pattern = '/\{\{(\*[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s'; $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { $name = $matches[1]; $value = $this->resolve($name, $data, $quote); return $value; }, $text); if ($data !== null) { # indirections $pattern = '/\{\{'.$name_keys_pattern.'\}\}/s'; $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { $name = $matches[1]; $value = $this->resolve("$name$matches[2]", $data, $quote); return $value; }, $text); } } if ($data !== null) { ## ensuite les remplacements simples $froms = array(); $tos = array(); foreach ($data as $name => $value) { $froms[] = "{".$name."}"; $tos[] = self::quote_nv($name, $value, $quote); } $text = str_replace($froms, $tos, $text); } return $text; } function xml(?string $text, $data=null, $quote=true, bool $allowPattern=true): string { $this->setText($text, $data, $quote, $allowPattern); return $this->apply(); } /** * Comme {@link xml()} mais les valeurs ne sont pas mises en échappement par * défaut. */ function string(?string $text, $data=null): string { return $this->xml($text, $data, false); } /** Comme {@link xml()} pour un fichier au format LibreOffice Writer */ function odt(string $file, $data=null, $quote=true): void { $zip = new ZipArchive(); $error = $zip->open($file); if ($error !== true) { throw new IOException("$file: error $error occured on open"); } $oldContent = $zip->getFromName("content.xml"); if ($oldContent !== false) { $newContent = $this->xml($oldContent, $data, $quote); if ($newContent !== $oldContent) { $zip->deleteName("content.xml"); $zip->addFromString("content.xml", $newContent); } } $oldStyles = $zip->getFromName("styles.xml"); if ($oldStyles !== false) { $newStyles = $this->xml($oldStyles, null, $quote); if ($newStyles != $oldStyles) { $zip->deleteName("styles.xml"); $zip->addFromString("styles.xml", $newStyles); } } $zip->close(); } }