From c82645a42a937e08661427a38b992b29c4d505fe Mon Sep 17 00:00:00 2001 From: "pmoreno.rodriguez" Date: Mon, 13 Oct 2025 21:35:33 +0200 Subject: [PATCH 1/2] wordCount Filter for Grav (#3957) --- .../Common/Twig/Extension/GravExtension.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/system/src/Grav/Common/Twig/Extension/GravExtension.php b/system/src/Grav/Common/Twig/Extension/GravExtension.php index 9a8eeb22e..9dcb49af4 100644 --- a/system/src/Grav/Common/Twig/Extension/GravExtension.php +++ b/system/src/Grav/Common/Twig/Extension/GravExtension.php @@ -140,6 +140,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface new TwigFilter('starts_with', [$this, 'startsWithFilter']), new TwigFilter('truncate', [Utils::class, 'truncate']), new TwigFilter('truncate_html', [Utils::class, 'truncateHTML']), + new TwigFilter('wordcount', [$this, 'wordCountFilter']), new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']), new TwigFilter('array_unique', 'array_unique'), new TwigFilter('basename', 'basename'), @@ -578,6 +579,62 @@ class GravExtension extends AbstractExtension implements GlobalsInterface return $str; } + /** + * Count words in text with improved accuracy for multiple languages + * + * @param string $text The text to count words from + * @param string $locale Optional locale for language-specific counting (default: 'en') + * @return int Number of words + */ + public function wordCountFilter($text, string $locale = 'en'): int + { + if (empty($text)) { + return 0; + } + + // Strip HTML tags and decode entities + $cleanText = html_entity_decode(strip_tags($text), ENT_QUOTES, 'UTF-8'); + + // Remove extra whitespace and normalize + $cleanText = trim(preg_replace('/\s+/', ' ', $cleanText)); + + if (empty($cleanText)) { + return 0; + } + + // Handle different languages + switch (strtolower($locale)) { + case 'zh': + case 'zh-cn': + case 'zh-tw': + case 'chinese': + // Chinese: count characters (excluding spaces and punctuation) + return mb_strlen(preg_replace('/[\s\p{P}]/u', '', $cleanText), 'UTF-8'); + + case 'ja': + case 'japanese': + // Japanese: count characters (excluding spaces) + return mb_strlen(preg_replace('/\s/', '', $cleanText), 'UTF-8'); + + case 'ko': + case 'korean': + // Korean: count characters (excluding spaces) + return mb_strlen(preg_replace('/\s/', '', $cleanText), 'UTF-8'); + + default: + // Western languages: use improved word counting + // Handle contractions, hyphenated words, and numbers better + $words = preg_split('/\s+/', $cleanText, -1, PREG_SPLIT_NO_EMPTY); + + // Filter out pure punctuation + $words = array_filter($words, function($word) { + return preg_match('/\w/', $word); + }); + + return count($words); + } + } + /** * Get Cron object for a crontab 'at' format * From 75d8356f1b335b781b8789740b4826472f2bd833 Mon Sep 17 00:00:00 2001 From: Nakkouch Tarek <98561646+nakkouchtarek@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:36:49 +0100 Subject: [PATCH 2/2] Fixed Twig Sandbox Bypass due to nested expression (#3939) --- system/src/Grav/Common/Security.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php index 4dcf8fb7d..14cb772e3 100644 --- a/system/src/Grav/Common/Security.php +++ b/system/src/Grav/Common/Security.php @@ -281,7 +281,13 @@ class Security 'twig.safe_functions', 'read_file', ]; + $string = preg_replace('/(({{\s*|{%\s*)[^}]*?(' . implode('|', $bad_twig) . ')[^}]*?(\s*}}|\s*%}))/i', '{# $1 #}', $string); + + foreach ($bad_twig as $func) { + $string = preg_replace('/\b' . preg_quote($func, '/') . '(\s*\([^)]*\))?\b/i', '{# $1 #}', $string); + } + return $string; } }