diff --git a/TODO b/TODO index 240d8fe3..4efe9ec7 100644 --- a/TODO +++ b/TODO @@ -3,9 +3,7 @@ 0.* * support duplicated vocation names with different ids * plugins: option to define custom requirements check in json file, to check if system meets the requirement - * add support for defining max myaac version in plugin.json file * cache Menus in templates - * semantic versioning support for plugins (github.com/composer/semver) 1.0: * i18n support (issue #1 on github) @@ -15,6 +13,7 @@ 2.0 * remove compat functions + * remove $template['link_*'] * folder restructure: * var/ (for logs, cache and data), config/, bin, public/ (for index and images and other public content), system/ (for php files and classess) * rename templates to layouts as templates is meant to be used for twig templates diff --git a/index.php b/index.php index a7a9b0d4..bec7841a 100644 --- a/index.php +++ b/index.php @@ -51,7 +51,7 @@ if(preg_match("/^[A-Za-z0-9-_%\'+]+\.png$/i", $uri)) { include(TOOLS . 'signature/index.php'); exit(); } -else if(preg_match("/^(.*)\.(gif|jpg|png|jpeg|tiff|bmp|css|js|less|map|html|php|zip|rar|gz)$/i", $_SERVER['REQUEST_URI'])) { +else if(preg_match("/^(.*)\.(gif|jpg|png|jpeg|tiff|bmp|css|js|less|map|html|php|zip|rar|gz|ttf|woff)$/i", $_SERVER['REQUEST_URI'])) { header("HTTP/1.0 404 Not Found"); exit; } diff --git a/system/libs/plugins.php b/system/libs/plugins.php index 13b84ab0..ff169c75 100644 --- a/system/libs/plugins.php +++ b/system/libs/plugins.php @@ -10,6 +10,34 @@ */ defined('MYAAC') or die('Direct access not allowed!'); +spl_autoload_register(function ($class) { + // project-specific namespace prefix + $prefix = 'Composer\\Semver\\'; + + // base directory for the namespace prefix + $base_dir = LIBS . '/semver/'; + + // does the class use the namespace prefix? + $len = strlen($prefix); + if (strncmp($prefix, $class, $len) !== 0) { + // no, move to the next registered autoloader + return; + } + + // get the relative class name + $relative_class = substr($class, $len); + + // replace the namespace prefix with the base directory, replace namespace + // separators with directory separators in the relative class name, append + // with .php + $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; + + // if the file exists, require it + if (file_exists($file)) { + require $file; + } +}); + class Plugins { private static $warnings = array(); private static $error = null; @@ -67,27 +95,39 @@ class Plugins { $require = $plugin['require']; if(isset($require['myaac'])) { $require_myaac = $require['myaac']; - if(version_compare(MYAAC_VERSION, $require_myaac, '<')) { - self::$warnings[] = "This plugin requires MyAAC version " . $require_myaac . ", you're using version " . MYAAC_VERSION . " - please update."; + if(!self::satisfies(MYAAC_VERSION, $require_myaac)) { + self::$error = "Your AAC version doesn't meet the requirement of this plugin. Required version is: " . $require_myaac . ", and you're using version " . MYAAC_VERSION . "."; $continue = false; } } if(isset($require['php'])) { $require_php = $require['php']; - if(version_compare(phpversion(), $require_php, '<')) { - self::$warnings[] = "This plugin requires PHP version " . $require_php . ", you're using version " . phpversion() . " - please update."; + if(!self::satisfies(phpversion(), $require_php)) { + self::$error = "Your PHP version doesn't meet the requirement of this plugin. Required version is: " . $require_php . ", and you're using version " . phpversion() . "."; $continue = false; } } if(isset($require['database'])) { $require_database = $require['database']; - if($require_database < DATABASE_VERSION) { - self::$warnings[] = "This plugin requires database version " . $require_database . ", you're using version " . DATABASE_VERSION . " - please update."; + if(!self::satisfies(DATABASE_VERSION, $require_database)) { + self::$error = "Your database version doesn't meet the requirement of this plugin. Required version is: " . $require_database . ", and you're using version " . DATABASE_VERSION . "."; $continue = false; } } + + foreach($require as $req => $version) { + if(in_array($req, array('myaac', 'php', 'database'))) { + continue; + } + + if(!self::is_installed($req, $version)) { + self::$error = "This plugin requires another plugin to run correctly. The another plugin is: " . $req . ", with version " . $version . "."; + $continue = false; + break; + } + } } if($continue) { @@ -197,6 +237,44 @@ class Plugins { return false; } + public static function is_installed($plugin_name, $version) { + $filename = BASE . 'plugins/' . $plugin_name . '.json'; + if(!file_exists($filename)) { + return false; + } + + $string = file_get_contents($filename); + $plugin_info = json_decode($string, true); + if($plugin_info == false) { + return false; + } + + if(!isset($plugin_info['version'])) { + return false; + } + + return self::satisfies($plugin_info['version'], $version); + } + + public static function satisfies($version, $constraints) { + $is_semver = false; + $array = array(',', '>', '<', '=', '*', '|', '~'); + foreach($array as $x) { + if(strpos($constraints, $x) !== false) { + $is_semver = true; + } + } + + if($is_semver && !Composer\Semver\Semver::satisfies($version, $constraints)) { + return false; + } + else if(version_compare($version, $constraints, '<')) { + return false; + } + + return true; + } + public static function getWarnings() { return self::$warnings; } diff --git a/system/libs/semver/Comparator.php b/system/libs/semver/Comparator.php new file mode 100644 index 00000000..a9d758f1 --- /dev/null +++ b/system/libs/semver/Comparator.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\Constraint; + +class Comparator +{ + /** + * Evaluates the expression: $version1 > $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function greaterThan($version1, $version2) + { + return self::compare($version1, '>', $version2); + } + + /** + * Evaluates the expression: $version1 >= $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function greaterThanOrEqualTo($version1, $version2) + { + return self::compare($version1, '>=', $version2); + } + + /** + * Evaluates the expression: $version1 < $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function lessThan($version1, $version2) + { + return self::compare($version1, '<', $version2); + } + + /** + * Evaluates the expression: $version1 <= $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function lessThanOrEqualTo($version1, $version2) + { + return self::compare($version1, '<=', $version2); + } + + /** + * Evaluates the expression: $version1 == $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function equalTo($version1, $version2) + { + return self::compare($version1, '==', $version2); + } + + /** + * Evaluates the expression: $version1 != $version2. + * + * @param string $version1 + * @param string $version2 + * + * @return bool + */ + public static function notEqualTo($version1, $version2) + { + return self::compare($version1, '!=', $version2); + } + + /** + * Evaluates the expression: $version1 $operator $version2. + * + * @param string $version1 + * @param string $operator + * @param string $version2 + * + * @return bool + */ + public static function compare($version1, $operator, $version2) + { + $constraint = new Constraint($operator, $version2); + + return $constraint->matches(new Constraint('==', $version1)); + } +} diff --git a/system/libs/semver/Constraint/AbstractConstraint.php b/system/libs/semver/Constraint/AbstractConstraint.php new file mode 100644 index 00000000..be83f750 --- /dev/null +++ b/system/libs/semver/Constraint/AbstractConstraint.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +trigger_error('The ' . __CLASS__ . ' abstract class is deprecated, there is no replacement for it, it will be removed in the next major version.', E_USER_DEPRECATED); + +/** + * Base constraint class. + */ +abstract class AbstractConstraint implements ConstraintInterface +{ + /** @var string */ + protected $prettyString; + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + if ($provider instanceof $this) { + // see note at bottom of this class declaration + return $this->matchSpecific($provider); + } + + // turn matching around to find a match + return $provider->matches($this); + } + + /** + * @param string $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return $this->__toString(); + } + + // implementations must implement a method of this format: + // not declared abstract here because type hinting violates parameter coherence (TODO right word?) + // public function matchSpecific( $provider); +} diff --git a/system/libs/semver/Constraint/Constraint.php b/system/libs/semver/Constraint/Constraint.php new file mode 100644 index 00000000..7a21eb47 --- /dev/null +++ b/system/libs/semver/Constraint/Constraint.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * Defines a constraint. + */ +class Constraint implements ConstraintInterface +{ + /* operator integer values */ + const OP_EQ = 0; + const OP_LT = 1; + const OP_LE = 2; + const OP_GT = 3; + const OP_GE = 4; + const OP_NE = 5; + + /** + * Operator to integer translation table. + * + * @var array + */ + private static $transOpStr = array( + '=' => self::OP_EQ, + '==' => self::OP_EQ, + '<' => self::OP_LT, + '<=' => self::OP_LE, + '>' => self::OP_GT, + '>=' => self::OP_GE, + '<>' => self::OP_NE, + '!=' => self::OP_NE, + ); + + /** + * Integer to operator translation table. + * + * @var array + */ + private static $transOpInt = array( + self::OP_EQ => '==', + self::OP_LT => '<', + self::OP_LE => '<=', + self::OP_GT => '>', + self::OP_GE => '>=', + self::OP_NE => '!=', + ); + + /** @var string */ + protected $operator; + + /** @var string */ + protected $version; + + /** @var string */ + protected $prettyString; + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + if ($provider instanceof $this) { + return $this->matchSpecific($provider); + } + + // turn matching around to find a match + return $provider->matches($this); + } + + /** + * @param string $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return $this->__toString(); + } + + /** + * Get all supported comparison operators. + * + * @return array + */ + public static function getSupportedOperators() + { + return array_keys(self::$transOpStr); + } + + /** + * Sets operator and version to compare with. + * + * @param string $operator + * @param string $version + * + * @throws \InvalidArgumentException if invalid operator is given. + */ + public function __construct($operator, $version) + { + if (!isset(self::$transOpStr[$operator])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid operator "%s" given, expected one of: %s', + $operator, + implode(', ', self::getSupportedOperators()) + )); + } + + $this->operator = self::$transOpStr[$operator]; + $this->version = $version; + } + + /** + * @param string $a + * @param string $b + * @param string $operator + * @param bool $compareBranches + * + * @throws \InvalidArgumentException if invalid operator is given. + * + * @return bool + */ + public function versionCompare($a, $b, $operator, $compareBranches = false) + { + if (!isset(self::$transOpStr[$operator])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid operator "%s" given, expected one of: %s', + $operator, + implode(', ', self::getSupportedOperators()) + )); + } + + $aIsBranch = 'dev-' === substr($a, 0, 4); + $bIsBranch = 'dev-' === substr($b, 0, 4); + + if ($aIsBranch && $bIsBranch) { + return $operator === '==' && $a === $b; + } + + // when branches are not comparable, we make sure dev branches never match anything + if (!$compareBranches && ($aIsBranch || $bIsBranch)) { + return false; + } + + return version_compare($a, $b, $operator); + } + + /** + * @param Constraint $provider + * @param bool $compareBranches + * + * @return bool + */ + public function matchSpecific(Constraint $provider, $compareBranches = false) + { + $noEqualOp = str_replace('=', '', self::$transOpInt[$this->operator]); + $providerNoEqualOp = str_replace('=', '', self::$transOpInt[$provider->operator]); + + $isEqualOp = self::OP_EQ === $this->operator; + $isNonEqualOp = self::OP_NE === $this->operator; + $isProviderEqualOp = self::OP_EQ === $provider->operator; + $isProviderNonEqualOp = self::OP_NE === $provider->operator; + + // '!=' operator is match when other operator is not '==' operator or version is not match + // these kinds of comparisons always have a solution + if ($isNonEqualOp || $isProviderNonEqualOp) { + return !$isEqualOp && !$isProviderEqualOp + || $this->versionCompare($provider->version, $this->version, '!=', $compareBranches); + } + + // an example for the condition is <= 2.0 & < 1.0 + // these kinds of comparisons always have a solution + if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) { + return true; + } + + if ($this->versionCompare($provider->version, $this->version, self::$transOpInt[$this->operator], $compareBranches)) { + // special case, e.g. require >= 1.0 and provide < 1.0 + // 1.0 >= 1.0 but 1.0 is outside of the provided interval + if ($provider->version === $this->version + && self::$transOpInt[$provider->operator] === $providerNoEqualOp + && self::$transOpInt[$this->operator] !== $noEqualOp) { + return false; + } + + return true; + } + + return false; + } + + /** + * @return string + */ + public function __toString() + { + return self::$transOpInt[$this->operator] . ' ' . $this->version; + } +} diff --git a/system/libs/semver/Constraint/ConstraintInterface.php b/system/libs/semver/Constraint/ConstraintInterface.php new file mode 100644 index 00000000..7cb13b6a --- /dev/null +++ b/system/libs/semver/Constraint/ConstraintInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +interface ConstraintInterface +{ + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider); + + /** + * @return string + */ + public function getPrettyString(); + + /** + * @return string + */ + public function __toString(); +} diff --git a/system/libs/semver/Constraint/EmptyConstraint.php b/system/libs/semver/Constraint/EmptyConstraint.php new file mode 100644 index 00000000..faba56bf --- /dev/null +++ b/system/libs/semver/Constraint/EmptyConstraint.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * Defines the absence of a constraint. + */ +class EmptyConstraint implements ConstraintInterface +{ + /** @var string */ + protected $prettyString; + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + return true; + } + + /** + * @param $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return $this->__toString(); + } + + /** + * @return string + */ + public function __toString() + { + return '[]'; + } +} diff --git a/system/libs/semver/Constraint/MultiConstraint.php b/system/libs/semver/Constraint/MultiConstraint.php new file mode 100644 index 00000000..0c547afd --- /dev/null +++ b/system/libs/semver/Constraint/MultiConstraint.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver\Constraint; + +/** + * Defines a conjunctive or disjunctive set of constraints. + */ +class MultiConstraint implements ConstraintInterface +{ + /** @var ConstraintInterface[] */ + protected $constraints; + + /** @var string */ + protected $prettyString; + + /** @var bool */ + protected $conjunctive; + + /** + * @param ConstraintInterface[] $constraints A set of constraints + * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive + */ + public function __construct(array $constraints, $conjunctive = true) + { + $this->constraints = $constraints; + $this->conjunctive = $conjunctive; + } + + /** + * @return ConstraintInterface[] + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * @return bool + */ + public function isConjunctive() + { + return $this->conjunctive; + } + + /** + * @return bool + */ + public function isDisjunctive() + { + return !$this->conjunctive; + } + + /** + * @param ConstraintInterface $provider + * + * @return bool + */ + public function matches(ConstraintInterface $provider) + { + if (false === $this->conjunctive) { + foreach ($this->constraints as $constraint) { + if ($constraint->matches($provider)) { + return true; + } + } + + return false; + } + + foreach ($this->constraints as $constraint) { + if (!$constraint->matches($provider)) { + return false; + } + } + + return true; + } + + /** + * @param string $prettyString + */ + public function setPrettyString($prettyString) + { + $this->prettyString = $prettyString; + } + + /** + * @return string + */ + public function getPrettyString() + { + if ($this->prettyString) { + return $this->prettyString; + } + + return $this->__toString(); + } + + /** + * @return string + */ + public function __toString() + { + $constraints = array(); + foreach ($this->constraints as $constraint) { + $constraints[] = (string) $constraint; + } + + return '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']'; + } +} diff --git a/system/libs/semver/Semver.php b/system/libs/semver/Semver.php new file mode 100644 index 00000000..0225bb55 --- /dev/null +++ b/system/libs/semver/Semver.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\Constraint; + +class Semver +{ + const SORT_ASC = 1; + const SORT_DESC = -1; + + /** @var VersionParser */ + private static $versionParser; + + /** + * Determine if given version satisfies given constraints. + * + * @param string $version + * @param string $constraints + * + * @return bool + */ + public static function satisfies($version, $constraints) + { + if (null === self::$versionParser) { + self::$versionParser = new VersionParser(); + } + + $versionParser = self::$versionParser; + $provider = new Constraint('==', $versionParser->normalize($version)); + $constraints = $versionParser->parseConstraints($constraints); + + return $constraints->matches($provider); + } + + /** + * Return all versions that satisfy given constraints. + * + * @param array $versions + * @param string $constraints + * + * @return array + */ + public static function satisfiedBy(array $versions, $constraints) + { + $versions = array_filter($versions, function ($version) use ($constraints) { + return Semver::satisfies($version, $constraints); + }); + + return array_values($versions); + } + + /** + * Sort given array of versions. + * + * @param array $versions + * + * @return array + */ + public static function sort(array $versions) + { + return self::usort($versions, self::SORT_ASC); + } + + /** + * Sort given array of versions in reverse. + * + * @param array $versions + * + * @return array + */ + public static function rsort(array $versions) + { + return self::usort($versions, self::SORT_DESC); + } + + /** + * @param array $versions + * @param int $direction + * + * @return array + */ + private static function usort(array $versions, $direction) + { + if (null === self::$versionParser) { + self::$versionParser = new VersionParser(); + } + + $versionParser = self::$versionParser; + $normalized = array(); + + // Normalize outside of usort() scope for minor performance increase. + // Creates an array of arrays: [[normalized, key], ...] + foreach ($versions as $key => $version) { + $normalized[] = array($versionParser->normalize($version), $key); + } + + usort($normalized, function (array $left, array $right) use ($direction) { + if ($left[0] === $right[0]) { + return 0; + } + + if (Comparator::lessThan($left[0], $right[0])) { + return -$direction; + } + + return $direction; + }); + + // Recreate input array, using the original indexes which are now in sorted order. + $sorted = array(); + foreach ($normalized as $item) { + $sorted[] = $versions[$item[1]]; + } + + return $sorted; + } +} diff --git a/system/libs/semver/VersionParser.php b/system/libs/semver/VersionParser.php new file mode 100644 index 00000000..359c18c4 --- /dev/null +++ b/system/libs/semver/VersionParser.php @@ -0,0 +1,548 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Semver; + +use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Semver\Constraint\EmptyConstraint; +use Composer\Semver\Constraint\MultiConstraint; +use Composer\Semver\Constraint\Constraint; + +/** + * Version parser. + * + * @author Jordi Boggiano + */ +class VersionParser +{ + /** + * Regex to match pre-release data (sort of). + * + * Due to backwards compatibility: + * - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted. + * - Only stabilities as recognized by Composer are allowed to precede a numerical identifier. + * - Numerical-only pre-release identifiers are not supported, see tests. + * + * |--------------| + * [major].[minor].[patch] -[pre-release] +[build-metadata] + * + * @var string + */ + private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?'; + + /** @var array */ + private static $stabilities = array('stable', 'RC', 'beta', 'alpha', 'dev'); + + /** + * Returns the stability of a version. + * + * @param string $version + * + * @return string + */ + public static function parseStability($version) + { + $version = preg_replace('{#.+$}i', '', $version); + + if ('dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4)) { + return 'dev'; + } + + preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match); + if (!empty($match[3])) { + return 'dev'; + } + + if (!empty($match[1])) { + if ('beta' === $match[1] || 'b' === $match[1]) { + return 'beta'; + } + if ('alpha' === $match[1] || 'a' === $match[1]) { + return 'alpha'; + } + if ('rc' === $match[1]) { + return 'RC'; + } + } + + return 'stable'; + } + + /** + * @param string $stability + * + * @return string + */ + public static function normalizeStability($stability) + { + $stability = strtolower($stability); + + return $stability === 'rc' ? 'RC' : $stability; + } + + /** + * Normalizes a version string to be able to perform comparisons on it. + * + * @param string $version + * @param string $fullVersion optional complete version string to give more context + * + * @throws \UnexpectedValueException + * + * @return string + */ + public function normalize($version, $fullVersion = null) + { + $version = trim($version); + if (null === $fullVersion) { + $fullVersion = $version; + } + + // strip off aliasing + if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) { + $version = $match[1]; + } + + // match master-like branches + if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) { + return '9999999-dev'; + } + + // if requirement is branch-like, use full name + if ('dev-' === strtolower(substr($version, 0, 4))) { + return 'dev-' . substr($version, 4); + } + + // strip off build metadata + if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) { + $version = $match[1]; + } + + // match classical versioning + if (preg_match('{^v?(\d{1,5})(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = $matches[1] + . (!empty($matches[2]) ? $matches[2] : '.0') + . (!empty($matches[3]) ? $matches[3] : '.0') + . (!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; + // match date(time) based versioning + } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = preg_replace('{\D}', '.', $matches[1]); + $index = 2; + } + + // add version modifiers if a version was matched + if (isset($index)) { + if (!empty($matches[$index])) { + if ('stable' === $matches[$index]) { + return $version; + } + $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? ltrim($matches[$index + 1], '.-') : ''); + } + + if (!empty($matches[$index + 2])) { + $version .= '-dev'; + } + + return $version; + } + + // match dev branches + if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) { + try { + return $this->normalizeBranch($match[1]); + } catch (\Exception $e) { + } + } + + $extraMessage = ''; + if (preg_match('{ +as +' . preg_quote($version) . '$}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version'; + } elseif (preg_match('{^' . preg_quote($version) . ' +as +}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-'; + } + + throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage); + } + + /** + * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison. + * + * @param string $branch Branch name (e.g. 2.1.x-dev) + * + * @return string|false Numeric prefix if present (e.g. 2.1.) or false + */ + public function parseNumericAliasPrefix($branch) + { + if (preg_match('{^(?P(\d++\\.)*\d++)(?:\.x)?-dev$}i', $branch, $matches)) { + return $matches['version'] . '.'; + } + + return false; + } + + /** + * Normalizes a branch name to be able to perform comparisons on it. + * + * @param string $name + * + * @return string + */ + public function normalizeBranch($name) + { + $name = trim($name); + + if (in_array($name, array('master', 'trunk', 'default'))) { + return $this->normalize($name); + } + + if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) { + $version = ''; + for ($i = 1; $i < 5; ++$i) { + $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x'; + } + + return str_replace('x', '9999999', $version) . '-dev'; + } + + return 'dev-' . $name; + } + + /** + * Parses a constraint string into MultiConstraint and/or Constraint objects. + * + * @param string $constraints + * + * @return ConstraintInterface + */ + public function parseConstraints($constraints) + { + $prettyConstraint = $constraints; + + if (preg_match('{^([^,\s]*?)@(' . implode('|', self::$stabilities) . ')$}i', $constraints, $match)) { + $constraints = empty($match[1]) ? '*' : $match[1]; + } + + if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraints, $match)) { + $constraints = $match[1]; + } + + $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints)); + $orGroups = array(); + foreach ($orConstraints as $constraints) { + $andConstraints = preg_split('{(?< ,]) *(? 1) { + $constraintObjects = array(); + foreach ($andConstraints as $constraint) { + foreach ($this->parseConstraint($constraint) as $parsedConstraint) { + $constraintObjects[] = $parsedConstraint; + } + } + } else { + $constraintObjects = $this->parseConstraint($andConstraints[0]); + } + + if (1 === count($constraintObjects)) { + $constraint = $constraintObjects[0]; + } else { + $constraint = new MultiConstraint($constraintObjects); + } + + $orGroups[] = $constraint; + } + + if (1 === count($orGroups)) { + $constraint = $orGroups[0]; + } elseif (2 === count($orGroups) + // parse the two OR groups and if they are contiguous we collapse + // them into one constraint + && $orGroups[0] instanceof MultiConstraint + && $orGroups[1] instanceof MultiConstraint + && 2 === count($orGroups[0]->getConstraints()) + && 2 === count($orGroups[1]->getConstraints()) + && ($a = (string) $orGroups[0]) + && substr($a, 0, 3) === '[>=' && (false !== ($posA = strpos($a, '<', 4))) + && ($b = (string) $orGroups[1]) + && substr($b, 0, 3) === '[>=' && (false !== ($posB = strpos($b, '<', 4))) + && substr($a, $posA + 2, -1) === substr($b, 4, $posB - 5) + ) { + $constraint = new MultiConstraint(array( + new Constraint('>=', substr($a, 4, $posA - 5)), + new Constraint('<', substr($b, $posB + 2, -1)), + )); + } else { + $constraint = new MultiConstraint($orGroups, false); + } + + $constraint->setPrettyString($prettyConstraint); + + return $constraint; + } + + /** + * @param string $constraint + * + * @throws \UnexpectedValueException + * + * @return array + */ + private function parseConstraint($constraint) + { + if (preg_match('{^([^,\s]+?)@(' . implode('|', self::$stabilities) . ')$}i', $constraint, $match)) { + $constraint = $match[1]; + if ($match[2] !== 'stable') { + $stabilityModifier = $match[2]; + } + } + + if (preg_match('{^v?[xX*](\.[xX*])*$}i', $constraint)) { + return array(new EmptyConstraint()); + } + + $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?' . self::$modifierRegex . '(?:\+[^\s]+)?'; + + // Tilde Range + // + // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous + // version, to ensure that unstable instances of the current version are allowed. However, if a stability + // suffix is added to the constraint, then a >= match on the current version is used instead. + if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) { + if (substr($constraint, 0, 2) === '~>') { + throw new \UnexpectedValueException( + 'Could not parse version constraint ' . $constraint . ': ' . + 'Invalid operator "~>", you probably meant to use the "~" operator' + ); + } + + // Work out which position in the version we are operating at + if (isset($matches[4]) && '' !== $matches[4]) { + $position = 4; + } elseif (isset($matches[3]) && '' !== $matches[3]) { + $position = 3; + } elseif (isset($matches[2]) && '' !== $matches[2]) { + $position = 2; + } else { + $position = 1; + } + + // Calculate the stability suffix + $stabilitySuffix = ''; + if (!empty($matches[5])) { + $stabilitySuffix .= '-' . $this->expandStability($matches[5]) . (!empty($matches[6]) ? $matches[6] : ''); + } + + if (!empty($matches[7])) { + $stabilitySuffix .= '-dev'; + } + + if (!$stabilitySuffix) { + $stabilitySuffix = '-dev'; + } + + $lowVersion = $this->manipulateVersionString($matches, $position, 0) . $stabilitySuffix; + $lowerBound = new Constraint('>=', $lowVersion); + + // For upper bound, we increment the position of one more significance, + // but highPosition = 0 would be illegal + $highPosition = max(1, $position - 1); + $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev'; + $upperBound = new Constraint('<', $highVersion); + + return array( + $lowerBound, + $upperBound, + ); + } + + // Caret Range + // + // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple. + // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for + // versions 0.X >=0.1.0, and no updates for versions 0.0.X + if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) { + // Work out which position in the version we are operating at + if ('0' !== $matches[1] || '' === $matches[2]) { + $position = 1; + } elseif ('0' !== $matches[2] || '' === $matches[3]) { + $position = 2; + } else { + $position = 3; + } + + // Calculate the stability suffix + $stabilitySuffix = ''; + if (empty($matches[5]) && empty($matches[7])) { + $stabilitySuffix .= '-dev'; + } + + $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); + $lowerBound = new Constraint('>=', $lowVersion); + + // For upper bound, we increment the position of one more significance, + // but highPosition = 0 would be illegal + $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; + $upperBound = new Constraint('<', $highVersion); + + return array( + $lowerBound, + $upperBound, + ); + } + + // X Range + // + // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple. + // A partial version range is treated as an X-Range, so the special character is in fact optional. + if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) { + if (isset($matches[3]) && '' !== $matches[3]) { + $position = 3; + } elseif (isset($matches[2]) && '' !== $matches[2]) { + $position = 2; + } else { + $position = 1; + } + + $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev'; + $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; + + if ($lowVersion === '0.0.0.0-dev') { + return array(new Constraint('<', $highVersion)); + } + + return array( + new Constraint('>=', $lowVersion), + new Constraint('<', $highVersion), + ); + } + + // Hyphen Range + // + // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range, + // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in + // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but + // nothing that would be greater than the provided tuple parts. + if (preg_match('{^(?P' . $versionRegex . ') +- +(?P' . $versionRegex . ')($)}i', $constraint, $matches)) { + // Calculate the stability suffix + $lowStabilitySuffix = ''; + if (empty($matches[6]) && empty($matches[8])) { + $lowStabilitySuffix = '-dev'; + } + + $lowVersion = $this->normalize($matches['from']); + $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix); + + $empty = function ($x) { + return ($x === 0 || $x === '0') ? false : empty($x); + }; + + if ((!$empty($matches[11]) && !$empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) { + $highVersion = $this->normalize($matches['to']); + $upperBound = new Constraint('<=', $highVersion); + } else { + $highMatch = array('', $matches[10], $matches[11], $matches[12], $matches[13]); + $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[11]) ? 1 : 2, 1) . '-dev'; + $upperBound = new Constraint('<', $highVersion); + } + + return array( + $lowerBound, + $upperBound, + ); + } + + // Basic Comparators + if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) { + try { + $version = $this->normalize($matches[2]); + + if (!empty($stabilityModifier) && $this->parseStability($version) === 'stable') { + $version .= '-' . $stabilityModifier; + } elseif ('<' === $matches[1] || '>=' === $matches[1]) { + if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) { + if (substr($matches[2], 0, 4) !== 'dev-') { + $version .= '-dev'; + } + } + } + + return array(new Constraint($matches[1] ?: '=', $version)); + } catch (\Exception $e) { + } + } + + $message = 'Could not parse version constraint ' . $constraint; + if (isset($e)) { + $message .= ': ' . $e->getMessage(); + } + + throw new \UnexpectedValueException($message); + } + + /** + * Increment, decrement, or simply pad a version number. + * + * Support function for {@link parseConstraint()} + * + * @param array $matches Array with version parts in array indexes 1,2,3,4 + * @param int $position 1,2,3,4 - which segment of the version to increment/decrement + * @param int $increment + * @param string $pad The string to pad version parts after $position + * + * @return string The new version + */ + private function manipulateVersionString($matches, $position, $increment = 0, $pad = '0') + { + for ($i = 4; $i > 0; --$i) { + if ($i > $position) { + $matches[$i] = $pad; + } elseif ($i === $position && $increment) { + $matches[$i] += $increment; + // If $matches[$i] was 0, carry the decrement + if ($matches[$i] < 0) { + $matches[$i] = $pad; + --$position; + + // Return null on a carry overflow + if ($i === 1) { + return; + } + } + } + } + + return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4]; + } + + /** + * Expand shorthand stability string to long version. + * + * @param string $stability + * + * @return string + */ + private function expandStability($stability) + { + $stability = strtolower($stability); + + switch ($stability) { + case 'a': + return 'alpha'; + case 'b': + return 'beta'; + case 'p': + case 'pl': + return 'patch'; + case 'rc': + return 'RC'; + default: + return $stability; + } + } +} diff --git a/system/locale/de/.htaccess b/system/locale/de/.htaccess new file mode 100644 index 00000000..3418e55a --- /dev/null +++ b/system/locale/de/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/system/locale/de/admin.php b/system/locale/de/admin.php new file mode 100644 index 00000000..8a128216 --- /dev/null +++ b/system/locale/de/admin.php @@ -0,0 +1,9 @@ + + */ +$locale['title'] = 'MyAAC Admin'; +?> diff --git a/system/locale/de/install.php b/system/locale/de/install.php new file mode 100644 index 00000000..460a6743 --- /dev/null +++ b/system/locale/de/install.php @@ -0,0 +1,108 @@ + + */ +$locale['installation'] = 'Installation'; +$locale['steps'] = 'Schritte'; + +$locale['previous'] = 'Zurück'; +$locale['next'] = 'Weiter'; + +$locale['on'] = 'Ein'; +$locale['off'] = 'Aus'; + +$locale['loaded'] = 'Geladen'; +$locale['not_loaded'] = 'Nicht geladen'; + +$locale['please_fill_all'] = 'Bitte alle Felder ausfüllen!'; +$locale['already_installed'] = 'MyAAC wurde bereits installiert. Bitte löschen install/ Verzeichnis.'; + +// welcome +$locale['step_welcome'] = 'Willkommen'; +$locale['step_welcome_title'] = 'Willkommen beim Installer'; +$locale['step_welcome_desc'] = 'Wählen Sie die Sprache, mit der Sie das Installationsprogramm anzeigen möchten'; + +// license +$locale['step_license'] = 'Lizenz'; +$locale['step_license_title'] = 'GNU/GPL Lizenz'; + +// requirements +$locale['step_requirements'] = 'Anforderungen'; +$locale['step_requirements_title'] = 'Anforderungen überprüfen'; +$locale['step_requirements_php_version'] = 'PHP Version'; +$locale['step_requirements_write_perms'] = 'Schreibberechtigungen'; +$locale['step_requirements_failed'] = 'Die Installation wird deaktiviert, bis diese Anforderungen erfüllt sind.
Für weitere Informationen siehe README Datei.'; +$locale['step_requirements_extension'] = '$EXTENSION$ PHP Erweiterung'; + +// config +$locale['step_config'] = 'Konfiguration'; +$locale['step_config_title'] = 'Grundkonfiguration'; +$locale['step_config_server_path'] = 'Serverpfad'; +$locale['step_config_server_path_desc'] = 'Pfad zu Ihrem TFS-Hauptverzeichnis, in dem Sie sich config.lua befinden.'; +$locale['step_config_mail_admin'] = 'Admin E-Mail'; +$locale['step_config_mail_admin_desc'] = 'Adresse, an die E-Mails aus dem Kontaktformular gesendet werden, z. B. admin@gmail.com'; +$locale['step_config_mail_admin_error'] = 'Admin E-Mail ist nicht korrekt.'; +$locale['step_config_mail_address'] = 'Server E-Mail'; +$locale['step_config_mail_address_desc'] = 'Adresse, die für ausgehende E-Mails (von :) verwendet wird, zB no-reply@your-server.org'; +$locale['step_config_mail_address_error'] = 'Server E-Mail ist nicht korrekt.'; +$locale['step_config_timezone'] = 'Zeitzone'; +$locale['step_config_timezone_desc'] = 'Wird für Datumsfunktionen verwendet'; +$locale['step_config_timezone_error'] = 'Zeitzone ist nicht korrekt.'; +$locale['step_config_client'] = 'Client Version'; +$locale['step_config_client_desc'] = 'Wird für die Downloadseite und einige Vorlagen verwendet'; +$locale['step_config_client_error'] = 'Client ist nicht korrekt.'; +$locale['step_config_usage'] = 'Nutzungsstatistiken'; +$locale['step_config_usage_desc'] = 'MyAAC erlauben, anonyme Nutzungsstatistiken zu melden? Die Daten werden nur einmal alle 30 Tage gesendet und sind vollständig vertraulich.'; +$locale['step_config_note'] = 'Der nächste Schritt dauert einige Zeit. Bitte aktualisieren Sie die Seite nicht und warten Sie, bis sie geladen ist.'; + +// database +$locale['step_database'] = 'Schema importieren'; +$locale['step_database_title'] = 'MySQL schema importieren'; +$locale['step_database_importing'] = 'Ihre Datenbank ist MySQL. Schema wird jetzt importiert...'; +$locale['step_database_error_path'] = 'Bitte geben Sie den Serverpfad an.'; +$locale['step_database_error_config'] = 'Datei config.lua kann nicht gefunden werden. Ist der Serverpfad korrekt? Geh zurück und überprüfe noch einmal.'; +$locale['step_database_error_database_empty'] = 'Der Datenbanktyp kann nicht aus config.lua ermittelt werden. Ihr OTS wird von diesem AAC nicht unterstützt.'; +$locale['step_database_error_only_mysql'] = 'Dieser AAC unterstützt nur MySQL. Aus Ihrer Konfigurationsdatei scheint Ihr OTS die Datenbank $DATABASE_TYPE$ zu verwenden. Bitte ändern Sie Ihre Datenbank in MySQL und folgen Sie dann der Installation erneut.'; +$locale['step_database_error_table'] = 'Die Tabelle $TABLE$ existiert nicht. Bitte importieren Sie zuerst Ihr OTS-Datenbankschema.'; +$locale['step_database_error_table_exist'] = 'Die Tabelle $TABLE$ existiert bereits. Scheint, dass AAC bereits installiert ist. Das Importieren des MySQL-Schemas wird übersprungen..'; +$locale['step_database_error_schema'] = 'Fehler beim Importieren des Schemas:'; +$locale['step_database_success_schema'] = '$PREFIX$ Tabellen wurden erfolgreich installiert.'; +$locale['step_database_error_file'] = '$FILE$ konnte nicht geöffnet werden. Bitte kopieren Sie diesen Inhalt und fügen Sie ihn dort ein:'; +$locale['step_database_adding_field'] = 'Feld hinzufügen'; +$locale['step_database_modifying_field'] = 'Ändern Feld'; +$locale['step_database_changing_field'] = 'Ändern $FIELD$ zu $FIELD_NEW$...'; +$locale['step_database_imported_players'] = 'Importierte Spielerproben...'; +$locale['step_database_loaded_monsters'] = 'Geladen Monsters...'; +$locale['step_database_error_monsters'] = 'Beim Laden der Datei monsters.xml sind einige Probleme aufgetreten. Bitte überprüfen Sie $LOG$ für weitere Informationen.'; +$locale['step_database_loaded_spells'] = 'Geladen Zauber...'; +$locale['step_database_created_account'] = 'Administratorkonto erstellt...'; +$locale['step_database_created_news'] = 'Erstellt newses...'; + +// admin account +$locale['step_admin'] = 'Administratorkonto'; +$locale['step_admin_title'] = 'Administratorkonto erstellen'; +$locale['step_admin_account'] = 'Name des Administratorkontos'; +$locale['step_admin_account_desc'] = 'Name Ihres Admin-Accounts, der für die Anmeldung an der Website und dem Server verwendet wird.'; +$locale['step_admin_account_error_format'] = 'Ungültiges Kontonamensformat. Verwenden Sie nur a-Z und Ziffern 0-9. Mindestens 3, maximal 32 Zeichen.'; +$locale['step_admin_account_error_same'] = 'Das Passwort darf nicht mit dem Kontonamen übereinstimmen.'; +$locale['step_admin_account_id'] = 'Administratorkontonummer'; +$locale['step_admin_account_id_desc'] = 'Nummer Ihres Admin-Accounts, der für die Anmeldung bei der Website und dem Server verwendet wird.'; +$locale['step_admin_account_id_error_format'] = 'Ungültiges Kontonummernformat. Bitte benutzen Sie nur die Nummern 0-9. Mindestens 6, maximal 10 Zeichen.'; +$locale['step_admin_account_id_error_same'] = 'Das Passwort darf nicht mit dem Kontonummer übereinstimmen'; +$locale['step_admin_password'] = 'Administratorkontokennwort'; +$locale['step_admin_password_desc'] = 'Passwort für Ihr Administratorkonto.'; +$locale['step_admin_password_error_empty'] = 'Bitte geben Sie das Passwort für Ihr neues Konto ein.'; +$locale['step_admin_password_error_format'] = 'Ungültiges Passwortformat. Verwenden Sie nur a-Z und Ziffern 0-9. Mindestens 8, maximal 30 Zeichen.'; + +// finish +$locale['step_finish_admin_panel'] = 'Admin-Panel'; +$locale['step_finish_homepage'] = 'Homepage'; +$locale['step_finish'] = 'Finish'; +$locale['step_finish_title'] = 'Installation beendet!'; +$locale['step_finish_desc'] = 'Herzliche Glückwünsche! MyAAC ist bereit zu verwenden!
Sie können sich jetzt im $ADMIN_PANEL$ anmelden, oder die $HOMEPAGE$ besuchen.

+Bitte lösche install/ Verzeichnis.

+Sende Fehler und Vorschläge bei $LINK$, danke!'; +?> diff --git a/system/locale/de/main.php b/system/locale/de/main.php new file mode 100644 index 00000000..3f9b2479 --- /dev/null +++ b/system/locale/de/main.php @@ -0,0 +1,15 @@ + + */ +$locale['name'] = 'Deutsch'; +$locale['lang'] = 'de'; +$locale['encoding'] = 'utf-8'; +$locale['direction']= 'ltr'; + +$locale['error404'] = 'Diese Seite konnte nicht gefunden werden.'; +$locale['news'] = 'Neuesten Nachrichten'; +?> \ No newline at end of file diff --git a/system/locale/en/install.php b/system/locale/en/install.php index 3daebb0d..8a2032f1 100644 --- a/system/locale/en/install.php +++ b/system/locale/en/install.php @@ -42,12 +42,12 @@ $locale['step_config'] = 'Configuration'; $locale['step_config_title'] = 'Basic configuration'; $locale['step_config_server_path'] = 'Server path'; $locale['step_config_server_path_desc'] = 'Path to your TFS main directory, where you have config.lua located.'; -$locale['step_config_mail_admin'] = 'Admin E-Mail'; +$locale['step_config_mail_admin'] = 'Admin Email'; $locale['step_config_mail_admin_desc'] = 'Address where emails from contact form will be delivered, for example admin@gmail.com'; -$locale['step_config_mail_admin_error'] = 'Admin E-Mail is not correct.'; -$locale['step_config_mail_address'] = 'Server E-Mail'; +$locale['step_config_mail_admin_error'] = 'Admin Email is not correct.'; +$locale['step_config_mail_address'] = 'Server Email'; $locale['step_config_mail_address_desc'] = 'Address which will be used for outgoing emails (from:), for example no-reply@your-server.org'; -$locale['step_config_mail_address_error'] = 'Server E-Mail is not correct.'; +$locale['step_config_mail_address_error'] = 'Server Email is not correct.'; $locale['step_config_timezone'] = 'Timezone'; $locale['step_config_timezone_desc'] = 'Used for date functions'; $locale['step_config_timezone_error'] = 'Timezone is not correct.'; diff --git a/system/pages/admin/plugins.php b/system/pages/admin/plugins.php index b32061c2..a2feaef0 100644 --- a/system/pages/admin/plugins.php +++ b/system/pages/admin/plugins.php @@ -73,12 +73,15 @@ else if(isset($_FILES["plugin"]["name"])) foreach(Plugins::getWarnings() as $warning) { warning($warning); } + $info = Plugins::getPluginInfo(); success((isset($info['name']) ? '' . $info['name'] . ' p' : 'P') . 'lugin has been successfully installed.'); } - else - error(Plugins::getError()); - + else { + $error = Plugins::getError(); + error(!empty($error) ? $error : 'Unexpected error happened while installing plugin. Please try again later.'); + } + unlink($targetzip); // delete the Zipped file } else diff --git a/system/templates/faq.html.twig b/system/templates/faq.html.twig index 1d7b9283..c6ff405e 100644 --- a/system/templates/faq.html.twig +++ b/system/templates/faq.html.twig @@ -17,9 +17,9 @@ {% set i = i + 1 %} - {{ faq.question }} + {{ faq.question|raw }} - + {% if canEdit %}