Artifact 4b979c86e66a54157646466ffa8e944d37726c76db5141cc8b4303f31a796b1b:


<?php

namespace Garradin\UserTemplate;

use KD2\Brindille;
use KD2\Brindille_Exception;
use KD2\Translate;

use Garradin\Config;
use Garradin\Plugin;
use Garradin\Utils;

use Garradin\Web\Skeleton;
use Garradin\Entities\Files\File;

use Garradin\UserTemplate\Modifiers;
use Garradin\UserTemplate\Functions;
use Garradin\UserTemplate\Sections;

use const Garradin\{WWW_URL, ADMIN_URL, SHARED_USER_TEMPLATES_CACHE_ROOT, USER_TEMPLATES_CACHE_ROOT, DATA_ROOT, LEGAL_LINE};

class UserTemplate extends Brindille
{
	protected $path = null;
	protected $modified;
	protected $file = null;
	protected $code = null;
	protected $cache_path = USER_TEMPLATES_CACHE_ROOT;

	protected $escape_default = 'html';

	static protected $root_variables;

	static public function getRootVariables()
	{
		if (null !== self::$root_variables) {
			return self::$root_variables;
		}

		static $keys = ['color1', 'color2', 'org_name', 'org_address', 'org_email', 'org_phone', 'org_web', 'currency', 'country', 'files'];

		$config = Config::getInstance();

		$files = $config::FILES;

		// Put URL in files array
		array_walk($files, function (&$v, $k) use ($config) {
			$v = $config->fileURL($k);
		});

		$config = array_intersect_key($config->asArray(), array_flip($keys));
		$config['files'] = $files;

		// @deprecated
		// FIXME: remove in a future version
		$config['nom_asso'] = $config['org_name'];
		$config['adresse_asso'] = $config['org_address'];
		$config['email_asso'] = $config['org_email'];
		$config['telephone_asso'] = $config['org_phone'];
		$config['site_asso'] = $config['org_web'];

		self::$root_variables = [
			'root_url'     => WWW_URL,
			'request_url'  => Utils::getRequestURI(),
			'admin_url'    => ADMIN_URL,
			'_GET'         => &$_GET,
			'_POST'        => &$_POST,
			'visitor_lang' => Translate::getHttpLang(),
			'config'       => $config,
			'legal_line'   => LEGAL_LINE,
		];

		return self::$root_variables;
	}

	public function __construct(?File $file = null)
	{
		if ($file) {
			$this->file = $file;
			$this->modified = $file->modified->getTimestamp();
		}

		$this->assignArray(self::getRootVariables());

		$this->registerAll();

		Plugin::fireSignal('usertemplate.init', ['template' => $this]);
	}

	/**
	 * Toggle safe mode
	 *
	 * If set to TRUE, then all functions and sections are removed, except foreach.
	 * Only modifiers can be used.
	 * Useful for templates where you don't want the user to be able to do SQL queries etc.
	 *
	 * @param  bool   $enable
	 * @return void
	 */
	public function toggleSafeMode(bool $safe_mode): void
	{
		if ($safe_mode) {
			$this->_functions = [];
			$this->_sections = [];

			// Register default Brindille modifiers
			$this->registerDefaults();
		}
		else {
			$this->registerAll();
		}
	}

	public function setEscapeDefault(?string $default): void
	{
		$this->escape_default = $default;

		if (null === $default) {
			$this->registerModifier('escape', fn($str) => $str);
		}
		else {
			$this->registerModifier('escape', fn ($str) => htmlspecialchars((string)$str) );
		}
	}

	public function registerAll()
	{
		// Register default Brindille modifiers
		$this->registerDefaults();

		// Common modifiers
		foreach (CommonModifiers::MODIFIERS_LIST as $key => $name) {
			$this->registerModifier(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name);
		}

		foreach (CommonModifiers::FUNCTIONS_LIST as $key => $name) {
			$this->registerFunction(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name);
		}

		// PHP modifiers
		foreach (Modifiers::PHP_MODIFIERS_LIST as $name) {
			$this->registerModifier($name, $name);
		}

		// Local modifiers
		foreach (Modifiers::MODIFIERS_LIST as $name) {
			$this->registerModifier($name, [Modifiers::class, $name]);
		}

		// Local functions
		foreach (Functions::FUNCTIONS_LIST as $name) {
			$this->registerFunction($name, [Functions::class, $name]);
		}

		// Local sections
		foreach (Sections::SECTIONS_LIST as $name) {
			$this->registerSection($name, [Sections::class, $name]);
		}

		$this->registerModifier('money', function ($number, bool $hide_empty = true, bool $force_sign = false): string {
			if ($hide_empty && !$number) {
				return '';
			}

			$sign = ($force_sign && $number > 0) ? '+' : '';

			$out = $sign . Utils::money_format($number, ',', '.', $hide_empty);

			if (!$this->escape_default) {
				return $out;
			}

			return sprintf('<b class="money">%s</b>', str_replace('.', '&nbsp;', $out));
		});

		$this->registerModifier('money_currency', function ($number, bool $hide_empty = true): string {
			$out = $this->_modifiers['money']($number, $hide_empty);

			if ($out !== '') {
				$out .= $this->escape_default == 'html' ? '&nbsp;' : ' ';
				$out .= Config::getInstance()->get('monnaie');
			}

			return $out;
		});
	}

	public function setSource(string $path)
	{
		$this->file = null;
		$this->path = $path;
		$this->modified = filemtime($path);
		// Use shared cache for default templates
		$this->cache_path = SHARED_USER_TEMPLATES_CACHE_ROOT;
	}

	public function setCode(string $code)
	{
		$this->code = $code;
		$this->file = null;
		$this->path = null;
		$this->modified = time();
		// Use custom cache for user templates
		$this->cache_path = USER_TEMPLATES_CACHE_ROOT;
	}

	protected function _getCachePath()
	{
		$hash = sha1($this->file ? $this->file->path : ($this->code ?: $this->path));
		return sprintf('%s/%s.php', $this->cache_path, $hash);
	}

	public function display(): void
	{
		$compiled_path = $this->_getCachePath(true);

		if (!is_dir(dirname($compiled_path))) {
			// Force cache directory mkdir
			Utils::safe_mkdir(dirname($compiled_path), 0777, true);
		}

		if (file_exists($compiled_path) && filemtime($compiled_path) >= $this->modified) {
			require $compiled_path;
			return;
		}

		$tmp_path = $compiled_path . '.tmp';

		if ($this->code) {
			$source = $this->code;
		}
		elseif ($this->file) {
			$source = $this->file->fetch();
		}
		else {
			$source = file_get_contents($this->path);
		}

		try {
			$code = $this->compile($source);
			file_put_contents($tmp_path, $code);

			require $tmp_path;
		}
		catch (Brindille_Exception $e) {
			throw new Brindille_Exception(sprintf("Erreur de syntaxe dans '%s' : %s",
				$this->file ? $this->file->name : ($this->code ? 'code' : Utils::basename($this->path)),
				$e->getMessage()), 0, $e);
		}
		catch (\Throwable $e) {
			// Don't delete temporary file as it can be used to debug
			throw $e;
		}

		if (!file_exists(Utils::dirname($compiled_path))) {
			Utils::safe_mkdir(Utils::dirname($compiled_path), 0777, true);
		}

		rename($tmp_path, $compiled_path);
	}

	public function fetch(): string
	{
		ob_start();
		$this->display();
		return ob_get_clean();
	}

	public function displayPDF(?string $filename = null): void
	{
		header('Content-type: application/pdf');

		if ($filename) {
			header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($filename)));
		}

		Utils::streamPDF($this->fetch());
	}
}