Skip to content
This repository was archived by the owner on May 14, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
81220f6
Added compatibility with symfony 3
tejadong Jun 30, 2018
88c8f43
Added changes.
tejadong Jun 30, 2018
2ec6c95
Added changes.
tejadong Jun 30, 2018
902935f
Fix error in composer.json
tejadong Jun 30, 2018
5e21b80
Changing version in dependencies.
tejadong Jun 30, 2018
0b823a5
Added require symfony/finder.
tejadong Jun 30, 2018
93b7991
Trying fixes
tejadong Jun 30, 2018
4d50c39
Trying fixes
tejadong Jun 30, 2018
e7baa66
Changed all references called "Lankit" to "Tejadong".
tejadong Jun 30, 2018
f4b9817
Fix error in TejadongDatatablesBundle.php
tejadong Jun 30, 2018
09ee255
Updated the help.
tejadong Jun 30, 2018
310e13f
Arreglado al relacionar dos columnas con la misma entidad.
IvanSecaduras Feb 25, 2019
b347ffc
Añadido el directorio /.idea al .gitignore
mengisoft Sep 27, 2019
349f2d8
Añadida función para las columnas personalizadas de Iván
mengisoft Sep 27, 2019
02a4a93
Aplicados más cambios sobre columnas custom de Iván
mengisoft Sep 27, 2019
3040cc7
Fix: hacia comprobación de una función sin antes comprobar si el valo…
mengisoft Jan 20, 2020
8d0a201
Arreglado al relacionar dos columnas con la misma entidad
tejadong Mar 19, 2020
9bdbfae
- Añadida compatibilidad en las búsquedas con el formato de fecha d/m…
tejadong Oct 3, 2020
1560436
Fix version
tejadong Oct 3, 2020
5e7d1fd
Fix: quitada la comprobación del tipo e incluida siempre la comprobac…
tejadong Oct 8, 2020
03c45b0
Fix: modificado el filtrado de fechas y horas porque no funcionaba co…
mengisoft Apr 25, 2022
2023062
Arreglada vulnerabilidad XSS.
mengisoft Jun 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ composer.lock
.*.sw[a-z]
*.un~
Session.vim
/.idea
182 changes: 149 additions & 33 deletions Datatables/Datatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@
* { "mData": "customer.location.address" }
*
* Felix-Antoine Paradis is the author of the original implementation this is
* built off of, see: https://gist.github.com/1638094
* built off of, see: https://gist.github.com/1638094
*/

namespace LanKit\DatatablesBundle\Datatables;
namespace Tejadong\DatatablesBundle\Datatables;

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;

use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\Response;
use HTMLPurifier;
use HTMLPurifier_Config;

class Datatable
{
Expand Down Expand Up @@ -194,15 +195,17 @@ class Datatable
*/
protected $datatable;

protected $htmlPurifier;

public function __construct(array $request, EntityRepository $repository, ClassMetadata $metadata, EntityManager $em, $serializer)
{
$this->em = $em;
$this->request = $request;
$this->repository = $repository;
$this->metadata = $metadata;
$this->serializer = $serializer;
$this->tableName = Container::camelize($metadata->getTableName());
$this->defaultJoinType = self::JOIN_INNER;
$this->tableName = Container::camelize(explode('\\', $metadata->getName())[count(explode('\\', $metadata->getName()))-1]);
$this->defaultJoinType = self::JOIN_LEFT;
$this->defaultResultType = self::RESULT_RESPONSE;
$this->setParameters();
$this->qb = $em->createQueryBuilder();
Expand All @@ -213,6 +216,8 @@ public function __construct(array $request, EntityRepository $repository, ClassM

$identifiers = $this->metadata->getIdentifierFieldNames();
$this->rootEntityIdentifier = array_shift($identifiers);

$this->htmlPurifierInit();
}

/**
Expand Down Expand Up @@ -302,23 +307,28 @@ protected function setRelatedEntityColumnInfo(array &$association, array $fields

// loop through the related entities, checking the associations as we go
$metadata = $this->metadata;

while ($field = array_shift($fields)) {
$columnName .= empty($columnName) ? $field : ".$field";
$entityName = lcfirst(Container::camelize($field));

if ($metadata->hasAssociation($entityName)) {
$joinOn = "$joinName.$entityName";
if ($metadata->isCollectionValuedAssociation($entityName)) {

if ($metadata->isCollectionValuedAssociation($entityName))
$association['containsCollections'] = true;
}
$metadata = $this->em->getClassMetadata(
$metadata->getAssociationTargetClass($entityName)
);

$metadata = $this->em->getClassMetadata($metadata->getAssociationTargetClass($entityName));

$joinName .= '_' . $this->getJoinName(
$metadata,
Container::camelize($metadata->getTableName()),
Container::camelize(explode('\\', $metadata->getName())[count(explode('\\', $metadata->getName()))-1]),
$entityName
);
// The join required to get to the entity in question

$joinName .= '_' . $entityName;

// The join required to get to the entity in question
if (!isset($this->assignedJoins[$joinName])) {
$this->assignedJoins[$joinName]['joinOn'] = $joinOn;
$this->assignedJoins[$joinName]['mdataColumn'] = $columnName;
Expand All @@ -327,7 +337,7 @@ protected function setRelatedEntityColumnInfo(array &$association, array $fields
}
else {
throw new Exception(
"Association '$entityName' not found ($mdataName)",
"Relación '$entityName' no encontrada ($mdataName)",
'404'
);
}
Expand All @@ -336,7 +346,7 @@ protected function setRelatedEntityColumnInfo(array &$association, array $fields
// Check the last field on the last related entity of the dotted notation
if (!$metadata->hasField(lcfirst($lastField))) {
throw new Exception(
"Field '$lastField' on association '$entityName' not found ($mdataName)",
"Propiedad '$lastField' en la relación '$entityName' no encontrada ($mdataName)",
'404'
);
}
Expand All @@ -356,15 +366,14 @@ protected function setSingleFieldColumnInfo(array &$association, $fieldName) {
$fieldName = Container::camelize($fieldName);

if (!$this->metadata->hasField(lcfirst($fieldName))) {
throw new Exception(
"Field '$fieldName' not found.)",
'404'
);
}

$association['fieldName'] = $fieldName;
$association['entityName'] = $this->tableName;
$association['fullName'] = $this->tableName . '.' . lcfirst($fieldName);
$association['fieldName'] = $fieldName;
$association['entityName'] = null;
$association['fullName'] = lcfirst($fieldName);
}else{
$association['fieldName'] = $fieldName;
$association['entityName'] = $this->tableName;
$association['fullName'] = $this->tableName . '.' . lcfirst($fieldName);
}
}

/**
Expand All @@ -380,7 +389,7 @@ protected function getJoinName(ClassMetadata $metadata, $tableName, $entityName)

// If it is self-referencing then we must avoid collisions
if ($metadata->getName() == $this->metadata->getName()) {
$joinName .= "_$entityName";
$joinName = "$entityName";
}

return $joinName;
Expand Down Expand Up @@ -480,8 +489,25 @@ public function setWhere(QueryBuilder $qb)
for ($i=0 ; $i < count($this->parameters); $i++) {
if (isset($this->request['bSearchable_'.$i]) && $this->request['bSearchable_'.$i] == "true") {
$qbParam = "sSearch_global_{$this->associations[$i]['entityName']}_{$this->associations[$i]['fieldName']}";
$fieldType = $this->metadata->getTypeOfField(lcfirst($this->associations[$i]['fieldName']));

switch ($fieldType) {
case "datetime":
$fieldName = "DATE_FORMAT(".$this->associations[$i]['fullName'].", '%d/%m/%Y %H:%i:%s')";
break;
case "time":
$fieldName = "DATE_FORMAT(".$this->associations[$i]['fullName'].", '%H:%i:%s')";
break;
case "date":
$fieldName = "DATE_FORMAT(".$this->associations[$i]['fullName'].", '%d/%m/%Y')";
break;
default:
$fieldName = $this->associations[$i]['fullName'];
break;
}

$orExpr->add($qb->expr()->like(
$this->associations[$i]['fullName'],
$fieldName,
":$qbParam"
));
$qb->setParameter($qbParam, "%" . $this->request['sSearch'] . "%");
Expand All @@ -495,13 +521,31 @@ public function setWhere(QueryBuilder $qb)
for ($i=0 ; $i < count($this->parameters); $i++) {
if (isset($this->request['bSearchable_'.$i]) && $this->request['bSearchable_'.$i] == "true" && $this->request['sSearch_'.$i] != '') {
$qbParam = "sSearch_single_{$this->associations[$i]['entityName']}_{$this->associations[$i]['fieldName']}";
$fieldType = $this->metadata->getTypeOfField(lcfirst($this->associations[$i]['fieldName']));

switch ($fieldType) {
case "datetime":
$fieldName = "DATE_FORMAT(".$this->associations[$i]['fullName'].", '%d/%m/%Y %H:%i:%s')";
break;
case "time":
$fieldName = "DATE_FORMAT(".$this->associations[$i]['fullName'].", '%H:%i:%s')";
break;
case "date":
$fieldName = "DATE_FORMAT(".$this->associations[$i]['fullName'].", '%d/%m/%Y')";
break;
default:
$fieldName = $this->associations[$i]['fullName'];
break;
}

$andExpr->add($qb->expr()->like(
$this->associations[$i]['fullName'],
":$qbParam"
));
$fieldName,
":$qbParam")
);
$qb->setParameter($qbParam, "%" . $this->request['sSearch_'.$i] . "%");
}
}

if ($andExpr->count() > 0) {
$qb->andWhere($andExpr);
}
Expand Down Expand Up @@ -549,7 +593,12 @@ public function setSelect(QueryBuilder $qb)
// Combine all columns to pull
foreach ($this->associations as $column) {
$parts = explode('.', $column['fullName']);
$columns[$parts[0]][] = $parts[1];

if(count($parts) > 1){
$columns[$parts[0]][] = $parts[1];
}else{
$columns['Custom'][] = $parts[0];
}
}

// Partial column results on entities require that we include the identifier as part of the selection
Expand All @@ -565,7 +614,8 @@ public function setSelect(QueryBuilder $qb)
}

foreach ($columns as $columnName => $fields) {
$partials[] = 'partial ' . $columnName . '.{' . implode(',', $fields) . '}';
if($columnName != 'Custom')
$partials[] = 'partial ' . $columnName . '.{' . implode(',', $fields) . '}';
}

$qb->select(implode(',', $partials));
Expand All @@ -576,7 +626,7 @@ public function setSelect(QueryBuilder $qb)
* Method to execute after constructing this object. Configures the object before
* executing getSearchResults()
*/
public function makeSearch()
public function makeSearch()
{
$this->setSelect($this->qb);
$this->setAssociations($this->qb);
Expand Down Expand Up @@ -630,7 +680,7 @@ public function executeSearch()
while ($field = array_shift($fields)) {
$rowRef = &$rowRef[$field];
// We ran into a collection based entity. Combine, merge, and continue on...
if (!empty($fields) && !$this->isAssocArray($rowRef)) {
if (!empty($fields) && $rowRef !== null && !$this->isAssocArray($rowRef)) {
$children = array();
while ($childItem = array_shift($rowRef)) {
$children = array_merge_recursive($children, $childItem);
Expand All @@ -639,6 +689,10 @@ public function executeSearch()
}
}
}

// Iterate and sanitize
$item = $this->purifyRecursive($item);

$output['aaData'][] = $item;
}

Expand Down Expand Up @@ -746,7 +800,7 @@ public function getCountAllResults()

return (int) $qb->getQuery()->getSingleScalarResult();
}

/**
* @return int Total query results after searches/filtering
*/
Expand Down Expand Up @@ -795,4 +849,66 @@ public function getQueryBuilder()
{
return $this->qb;
}

public function setCustom($response, $nombre, $clase, $funcion, $parameters = []){

$data = json_decode($response->getContent(), true);

foreach($data['aaData'] as $key => $registro){

$parametros = [];

if(count($parameters) == 0)
$parametros = array($registro['id']);
else{
$parametros = array_merge(array($registro['id']), $parameters);
}

$data['aaData'][$key][$nombre] = call_user_func_array( array( $clase, $funcion), $parametros );
}

return $response->setContent(json_encode($data));

}

private function htmlPurifierInit()
{
$config = HTMLPurifier_Config::createDefault();

// Permitir todos los elementos y atributos válidos HTML5, para no limitar nada
$config->set('HTML.Allowed', null); // No restringir etiquetas ni atributos

// Permitir completamente el CSS, para evitar que modifique estilos
$config->set('CSS.AllowedProperties', null);

// No transformar ni corregir el HTML
$config->set('HTML.Doctype', 'HTML 4.01 Transitional'); // un doctype flexible
$config->set('HTML.Trusted', false); // seguridad contra XSS
$config->set('Core.EscapeNonASCIICharacters', false);
$config->set('Core.ConvertDocumentToFragment', true);

// Evitar auto-corrección de etiquetas
$config->set('HTML.DefinitionID', 'custom-def');
$config->set('HTML.DefinitionRev', 1);
$config->set('AutoFormat.AutoParagraph', false);
$config->set('AutoFormat.RemoveEmpty', false);

// Deshabilitar filtro CSS agresivo
$config->set('CSS.Trusted', false);
$config->set('CSS.AllowImportant', true);

$this->htmlPurifier = new HTMLPurifier($config);
}

protected function purifyRecursive($data)
{
foreach ($data as $key => $value) {
if (is_string($value)) {
$data[$key] = $this->htmlPurifier->purify($value);
} elseif (is_array($value)) {
$data[$key] = $this->purifyRecursive($value);
}
}
return $data;
}
}
13 changes: 9 additions & 4 deletions Datatables/DatatableManager.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace LanKit\DatatablesBundle\Datatables;
namespace Tejadong\DatatablesBundle\Datatables;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\Bundle\DoctrineBundle\Registry as DoctrineRegistry;
Expand Down Expand Up @@ -50,19 +50,24 @@ protected function getClassName($className) {
*/
public function getDatatable($class)
{
$symfony_version = \Symfony\Component\HttpKernel\Kernel::VERSION;
$request = $symfony_version >= 3
? $this->container->get('request_stack')->getCurrentRequest()->query->all()
: $this->container->get('request')->query->all();

$class = $this->getClassName($class);

$metadata = $this->doctrine->getManager()->getClassMetadata($class);
$repository = $this->doctrine->getRepository($class);

$datatable = new Datatable(
$this->container->get('request')->query->all(),
$request,
$this->doctrine->getRepository($class),
$this->doctrine->getManager()->getClassMetadata($class),
$this->doctrine->getManager(),
$this->container->get('lankit_datatables.serializer')
$this->container->get('tejadong_datatables.serializer')
);
return $datatable->useDoctrinePaginator($this->useDoctrinePaginator);
return $datatable->useDoctrinePaginator($this->useDoctrinePaginator);
}
}

4 changes: 2 additions & 2 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace LanKit\DatatablesBundle\DependencyInjection;
namespace Tejadong\DatatablesBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
Expand All @@ -18,7 +18,7 @@ class Configuration implements ConfigurationInterface
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('lankit_datatables');
$rootNode = $treeBuilder->root('tejadong_datatables');

$rootNode
->children()
Expand Down
Loading