diff --git a/Util/Datatable.php b/Util/Datatable.php
index d30096b..7c0fab5 100644
--- a/Util/Datatable.php
+++ b/Util/Datatable.php
@@ -16,6 +16,7 @@
use Ali\DatatableBundle\Util\Formatter\Renderer;
use Ali\DatatableBundle\Util\Factory\Prototype\PrototypeBuilder;
use Twig\Environment;
+use Twig\Markup;
class Datatable
{
@@ -172,18 +173,36 @@ public function execute()
array_unshift($data, $item);
}
}
+ $raw_data = $data;
if (!is_null($this->_renderer))
{
array_walk($data, $this->_renderer);
}
if (!is_null($this->_renderer_obj))
{
+ // ->setRenderers() always renders every column through Twig (falling back to
+ // _default.html.twig's {{ dt_item }}), so auto-escaping is guaranteed here.
+ // The ->setRenderer() closure's output (if any) still needs marking as safe,
+ // since Twig would otherwise re-escape the HTML it already built.
+ if (!is_null($this->_renderer))
+ {
+ $this->_markRendererOutputAsSafe($data, $raw_data);
+ }
$this->_renderer_obj->applyTo($data, $objects);
}
+ else
+ {
+ // No Twig pass is guaranteed to run (no ->setRenderers()), so nothing will
+ // auto-escape leftover raw entity/DB values (or a whole untouched row when
+ // ->setRenderer() isn't used either). In a single pass: mark values the
+ // ->setRenderer() closure built as safe HTML, and escape everything else.
+ $this->_finalizeUnrenderedValues($data, $raw_data);
+ }
if (!empty($this->_multiple))
{
array_walk($data, function($val, $key) use(&$data, $ids) {
- array_unshift($val, "");
+ $safe_id = htmlspecialchars((string) $ids[$key], ENT_QUOTES, 'UTF-8');
+ array_unshift($val, "");
$data[$key] = $val;
});
}
@@ -196,6 +215,68 @@ public function execute()
return new JsonResponse($output);
}
+ /**
+ * Column values untouched by the controller's ->setRenderer() closure are raw entity/DB
+ * data, left as plain strings here and escaped later by Twig (->setRenderers() is
+ * configured whenever this method is called, see execute()). Values the closure *did*
+ * rewrite are trusted, deliberately-built HTML (links, badges, ...), so we wrap them in
+ * Twig\Markup - the same "already safe" marker the |raw filter produces - so the later
+ * Twig pass doesn't mangle them.
+ *
+ * @param array $data data after the renderer closure ran (modified in place)
+ * @param array $raw_data data as it was before the renderer closure ran
+ *
+ * @return void
+ */
+ private function _markRendererOutputAsSafe(array &$data, array $raw_data)
+ {
+ foreach ($data as $row_index => &$row)
+ {
+ foreach ($row as $column_index => &$value)
+ {
+ $original = $raw_data[$row_index][$column_index] ?? null;
+ if (is_string($value) && $value !== $original)
+ {
+ $value = new Markup($value, 'UTF-8');
+ }
+ }
+ }
+ }
+
+ /**
+ * Combines _markRendererOutputAsSafe() and the old _escapeUnsafeValues() into a single
+ * row/column pass, since every cell needs exactly one of those two treatments and doing
+ * them as two separate nested loops over the same data was pure overhead. For each string
+ * cell: if the ->setRenderer() closure changed it, it's deliberately built HTML, so wrap it
+ * in Twig\Markup; otherwise it's untouched entity/DB data and must be escaped so the
+ * client-side DataTables grid (which inserts aaData directly into the DOM) can't interpret
+ * it as HTML/script. Used whenever no ->setRenderers() Twig pass is configured to guarantee
+ * escaping - i.e. when the table has no renderer at all, or only a ->setRenderer() closure
+ * that may leave some columns (or all rows, for columns it doesn't touch) unrendered.
+ *
+ * @param array $data data after any ->setRenderer() closure ran (modified in place)
+ * @param array $raw_data data as it was before the ->setRenderer() closure ran
+ *
+ * @return void
+ */
+ private function _finalizeUnrenderedValues(array &$data, array $raw_data)
+ {
+ foreach ($data as $row_index => &$row)
+ {
+ foreach ($row as $column_index => &$value)
+ {
+ if (!is_string($value))
+ {
+ continue;
+ }
+ $original = $raw_data[$row_index][$column_index] ?? null;
+ $value = $value !== $original
+ ? new Markup($value, 'UTF-8')
+ : htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
+ }
+ }
+ }
+
/**
* get datatable instance by id
* return current instance if null
diff --git a/Util/Formatter/Renderer.php b/Util/Formatter/Renderer.php
index 219b29e..b6400f4 100644
--- a/Util/Formatter/Renderer.php
+++ b/Util/Formatter/Renderer.php
@@ -34,9 +34,7 @@ public function __construct(Environment $twig, array $renderers, array $fields)
*/
public function applyView($view_path, array $params)
{
- $out = $this->twig
- ->render($view_path, $params);
- return html_entity_decode($out);
+ return $this->twig->render($view_path, $params);
}
/**