diff --git a/.gitignore b/.gitignore index 380e437..a737644 100755 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ composer.lock .*.sw[a-z] *.un~ Session.vim +/.idea diff --git a/Datatables/Datatable.php b/Datatables/Datatable.php index a65fdf1..86576e6 100755 --- a/Datatables/Datatable.php +++ b/Datatables/Datatable.php @@ -19,10 +19,10 @@ * { "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; @@ -30,10 +30,11 @@ 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 { @@ -194,6 +195,8 @@ class Datatable */ protected $datatable; + protected $htmlPurifier; + public function __construct(array $request, EntityRepository $repository, ClassMetadata $metadata, EntityManager $em, $serializer) { $this->em = $em; @@ -201,8 +204,8 @@ public function __construct(array $request, EntityRepository $repository, ClassM $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(); @@ -213,6 +216,8 @@ public function __construct(array $request, EntityRepository $repository, ClassM $identifiers = $this->metadata->getIdentifierFieldNames(); $this->rootEntityIdentifier = array_shift($identifiers); + + $this->htmlPurifierInit(); } /** @@ -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; @@ -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' ); } @@ -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' ); } @@ -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); + } } /** @@ -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; @@ -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'] . "%"); @@ -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); } @@ -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 @@ -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)); @@ -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); @@ -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); @@ -639,6 +689,10 @@ public function executeSearch() } } } + + // Iterate and sanitize + $item = $this->purifyRecursive($item); + $output['aaData'][] = $item; } @@ -746,7 +800,7 @@ public function getCountAllResults() return (int) $qb->getQuery()->getSingleScalarResult(); } - + /** * @return int Total query results after searches/filtering */ @@ -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; + } } diff --git a/Datatables/DatatableManager.php b/Datatables/DatatableManager.php index b7edf4f..d2e22cc 100755 --- a/Datatables/DatatableManager.php +++ b/Datatables/DatatableManager.php @@ -1,6 +1,6 @@ = 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); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 443b3f2..313f4fa 100755 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1,6 +1,6 @@ root('lankit_datatables'); + $rootNode = $treeBuilder->root('tejadong_datatables'); $rootNode ->children() diff --git a/DependencyInjection/LanKitDatatablesExtension.php b/DependencyInjection/TejadongDatatablesExtension.php old mode 100755 new mode 100644 similarity index 84% rename from DependencyInjection/LanKitDatatablesExtension.php rename to DependencyInjection/TejadongDatatablesExtension.php index 9c08ec3..636c8b7 --- a/DependencyInjection/LanKitDatatablesExtension.php +++ b/DependencyInjection/TejadongDatatablesExtension.php @@ -1,6 +1,6 @@ setAlias($this->getAlias() . '.' . $key, $service); } - $container->getDefinition('lankit_datatables') + $container->getDefinition('tejadong_datatables') ->replaceArgument(2, $config['datatable']['use_doctrine_paginator']); } @@ -38,6 +38,6 @@ public function load(array $configs, ContainerBuilder $container) */ public function getAlias() { - return 'lankit_datatables'; + return 'tejadong_datatables'; } } diff --git a/LanKitDatatablesBundle.php b/LanKitDatatablesBundle.php deleted file mode 100755 index 48579ef..0000000 --- a/LanKitDatatablesBundle.php +++ /dev/null @@ -1,14 +0,0 @@ - - LanKit\DatatablesBundle\Datatables\DatatableManager + Tejadong\DatatablesBundle\Datatables\DatatableManager - + diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 1077e2a..e3d634e 100755 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -1,5 +1,6 @@ -Getting Started With LanKitDatatablesBundle +Getting Started With TejadongDatatablesBundle =========================================== +*¡¡Attention!!, This bundle is a copy adapted to add compatibility with Symfony 3, the real repository belongs to [LanKit/DatatablesBundle](https://github.com/LanKit/DatatablesBundle).* * [Prerequisites](#prerequisites) * [Installation](#installation) @@ -46,29 +47,20 @@ serializer service in your config file... ```yml // app/config.yml -lankit_datatables: +tejadong_datatables: services: serializer: some_other_serializer # Defaults to jms_serializer.serializer ``` ## Installation -### Step 1: Download LanKitDatatablesBundle using composer +### Step 1: Download TejadongDatatablesBundle using composer -Add LanKitDatatablesBundle to your composer.json: - -```js -{ - "require": { - "lankit/datatables-bundle": "*" - } -} -``` Use composer to download the bundle using the following command: ``` bash -$ php composer.phar update lankit/datatables-bundle +$ php composer require tejadong/datatables-bundle ``` ### Step 2: Enable the bundle @@ -83,11 +75,24 @@ public function registerBundles() { $bundles = array( // ... - new LanKit\DatatablesBundle\LanKitDatatablesBundle(), + new Tejadong\DatatablesBundle\TejadongDatatablesBundle(), ); } ``` +### Step 3: Add the config format + + +Add settings to support date filtering d/m/Y H:i:s : + +```yml +doctrine: + orm: + dql: + datetime_functions: + date_format: DoctrineExtensions\Query\Mysql\DateFormat +``` + ## Usage To respond to a DataTables.js request from a controller, you can do the following: @@ -96,7 +101,7 @@ To respond to a DataTables.js request from a controller, you can do the followin public function getDatatableAction() { - $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer'); + $datatable = $this->get('tejadong_datatables')->getDatatable('AcmeDemoBundle:Customer'); return $datatable->getSearchResults(); } @@ -108,12 +113,12 @@ By default an entity association is inner joined. This can be changed as a defau can be set on a per columm basis: ``` php -use LanKit\DatatablesBundle\Datatables\DataTable; +use Tejadong\DatatablesBundle\Datatables\DataTable; ... public function getDatatableAction() { - $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer'); + $datatable = $this->get('tejadong_datatables')->getDatatable('AcmeDemoBundle:Customer'); // The default type for all joins is inner. Change it to left if desired. $datatable->setDefaultJoinType(Datatable::JOIN_LEFT); @@ -132,12 +137,12 @@ If you need a different format for the response, you can specify the result type constants `Datatable::RESULT_ARRAY` and `Datatable::RESULT_JSON`: ``` php -use LanKit\DatatablesBundle\Datatables\DataTable; +use Tejadong\DatatablesBundle\Datatables\DataTable; ... public function getDatatableAction() { - $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer'); + $datatable = $this->get('tejadong_datatables')->getDatatable('AcmeDemoBundle:Customer'); // Get the results as an array $datatableArray = $datatable->getSearchResults(Datatable::RESULT_ARRAY); @@ -156,7 +161,7 @@ is passed the QueryBuilder instance as an argument. public function getDatatableAction() { - $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer'); + $datatable = $this->get('tejadong_datatables')->getDatatable('AcmeDemoBundle:Customer'); // Add the $datatable variable, or other needed variables, to the callback scope $datatable->addWhereBuilderCallback(function($qb) use ($datatable) { @@ -187,7 +192,7 @@ then you can toggle it with the `hideFilteredCount` method. public function getDatatableAction() { - $datatable = $this->get('lankit_datatables') + $datatable = $this->get('tejadong_datatables') ->getDatatable('AcmeDemoBundle:Customer') ->addWhereBuilderCallback(function($qb) use ($datatable) { // ... @@ -245,7 +250,7 @@ You can toggle and modify these properties with the methods `setDtRowClass`, `us public function getDatatableAction() { - $datatable = $this->get('lankit_datatables') + $datatable = $this->get('tejadong_datatables') ->getDatatable('AcmeDemoBundle:Customer') ->setDtRowClass('special-class') // Add whatever class(es) you want. Separate classes with a space. ->useDtRowId(true); @@ -266,7 +271,7 @@ MS SQL. You may receive an error like... To get around this you can disable the use of the Paginator by doing the following... ```yml -lankit_datatables: +tejadong_datatables: datatable: use_doctrine_paginator: false ``` diff --git a/TejadongDatatablesBundle.php b/TejadongDatatablesBundle.php new file mode 100644 index 0000000..5f6c1b3 --- /dev/null +++ b/TejadongDatatablesBundle.php @@ -0,0 +1,14 @@ +