![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/mautic.corals.io/app/bundles/CoreBundle/Entity/ |
<?php namespace Mautic\CoreBundle\Entity; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Util\ClassUtils; use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Query\Expression\CompositeExpression; use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; use Mautic\CoreBundle\Cache\ResultCacheHelper; use Mautic\CoreBundle\Cache\ResultCacheOptions; use Mautic\CoreBundle\Doctrine\Paginator\SimplePaginator; use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\InputHelper; use Mautic\CoreBundle\Helper\SearchStringHelper; use Mautic\UserBundle\Entity\User; use Symfony\Contracts\Translation\TranslatorInterface; /** * @template T of object * * @extends ServiceEntityRepository<T> */ class CommonRepository extends ServiceEntityRepository { /** * @phpstan-param class-string<T>|null $entityFQCN */ public function __construct(ManagerRegistry $registry, string $entityFQCN = null) { parent::__construct($registry, $entityFQCN ?? str_replace('Repository', '', static::class)); } /** * Stores the parsed columns and their negate status for addAdvancedSearchWhereClause(). * * @var array */ protected $advancedFilterCommands = []; /** * @var User|null */ protected $currentUser; /** * @var TranslatorInterface */ protected $translator; /** * This eliminates chance for parameter name collision. * * @see CommonRepository::generateRandomParameterName() * * @var int */ protected $lastUsedParameterId = 0; /** * @var ExpressionBuilder|null */ private $expressionBuilder; /** * @param string $alias * @param object $entity * * @return mixed */ public function checkUniqueAlias($alias, $entity = null) { $q = $this->createQueryBuilder('e') ->select('count(e.id) as aliascount') ->where('e.alias = :alias'); $q->setParameter('alias', $alias); if (!empty($entity) && $entity->getId()) { $q->andWhere('e.id != :id'); $q->setParameter('id', $entity->getId()); } $results = $q->getQuery()->getSingleResult(); return $results['aliascount']; } /** * Examines the arguments passed to getEntities and converts ORM properties to dBAL column names. * * @param string $entityClass */ public function convertOrmProperties($entityClass, array $args): array { $properties = $this->getBaseColumns($entityClass); // check force filters if (isset($args['filter']['force']) && is_array($args['filter']['force'])) { $this->convertOrmPropertiesToColumns($args['filter']['force'], $properties); } if (isset($args['filter']['where']) && is_array($args['filter']['where'])) { $this->convertOrmPropertiesToColumns($args['filter']['where'], $properties); } // check order by if (isset($args['order'])) { if (is_array($args['order'])) { foreach ($args['order'] as &$o) { $alias = ''; if (str_contains($o, '.')) { [$alias, $o] = explode('.', $o); } if (in_array($o, $properties)) { $o = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $o); $o = strtolower($o); } $o = (!empty($alias)) ? $alias.'.'.$o : $o; } } } return $args; } /** * @param class-string $className * * @return mixed * * @throws \Doctrine\ORM\Mapping\MappingException * @throws \Exception */ public function createFromArray($className, &$data) { $entity = new $className(); $meta = $this->_em->getClassMetadata($className); $ormProperties = $this->getBaseColumns($className, true); foreach ($ormProperties as $property => $dbCol) { if (isset($data[$dbCol])) { $v = $data[$dbCol]; if ($v && $meta->hasAssociation($property)) { $map = $meta->getAssociationMapping($property); $v = $this->_em->getRepository($map['targetEntity'])->find($v); if (empty($v)) { throw new \Exception('Associate data not found'); } } $method = 'set'.ucfirst($property); if (method_exists($entity, $method)) { $entity->$method($v); } unset($data[$dbCol]); } } return $entity; } /** * Delete an array of entities. * * @param array $entities */ public function deleteEntities($entities): void { // iterate over the results so the events are dispatched on each delete $batchSize = 20; $i = 0; foreach ($entities as $entity) { $this->deleteEntity($entity, false); if (0 === ++$i % $batchSize) { $this->_em->flush(); } } $this->_em->flush(); } /** * Delete an entity through the repository. * * @param object $entity * @param bool $flush true by default; use false if persisting in batches */ public function deleteEntity($entity, $flush = true): void { // delete entity $this->_em->remove($entity); if ($flush) { $this->_em->flush(); } } public function detachEntities(array $entities): void { foreach ($entities as $entity) { $this->getEntityManager()->detach($entity); } } /** * @param mixed $entity */ public function detachEntity($entity): void { $this->getEntityManager()->detach($entity); } public function refetchEntity(object &$entity): void { if ($this->getEntityManager()->contains($entity)) { $this->getEntityManager()->detach($entity); $metadata = $this->getEntityManager()->getClassMetadata(ClassUtils::getClass($entity)); $identifierValues = $metadata->getIdentifierValues($entity); if (count($identifierValues) > 1) { throw new \RuntimeException('Multiple identifiers are not supported.'); } $entity = $this->getEntity(array_pop($identifierValues)); } } /** * @return mixed|null */ public function findOneBySlugs($alias, $catAlias = null, $lang = null) { try { $q = $this->createQueryBuilder($this->getTableAlias()) ->setParameter('alias', $alias); $expr = $q->expr()->andX( $q->expr()->eq($this->getTableAlias().'.alias', ':alias') ); $metadata = $this->getClassMetadata(); if (null !== $catAlias) { if (isset($metadata->associationMappings['category'])) { $q->leftJoin($this->getTableAlias().'.category', 'category') ->setParameter('catAlias', $catAlias); $expr->add( $q->expr()->eq('category.alias', ':catAlias') ); } else { // This entity does not have a category mapping so return null return null; } } if (isset($metadata->fieldMappings['language'])) { if ($lang) { // Find the landing page with the specific requested locale $q->setParameter('lang', $lang); $expr->add( $q->expr()->eq($this->getTableAlias().'.language', ':lang') ); } elseif (isset($metadata->associationMappings['translationParent'])) { // Find the parent translation $expr->add( $q->expr()->isNull($this->getTableAlias().'.translationParent') ); } } // Check for variants and return parent only if (isset($metadata->associationMappings['variantParent'])) { $expr->add( $q->expr()->isNull($this->getTableAlias().'.variantParent') ); } $q->where($expr); $entity = $q->getQuery()->getSingleResult(); } catch (\Exception) { $entity = null; } return $entity; } /** * Gets the properties of an ORM entity. * * @param string $entityClass * @param bool $returnColumnNames * * @return array */ public function getBaseColumns($entityClass, $returnColumnNames = false) { static $baseCols = [true => [], false => []]; if ($this->getEntityName() === $entityClass) { if (empty($baseCols[$returnColumnNames][$entityClass])) { // Use metadata $metadata = $this->getClassMetadata(); $baseCols[true][$entityClass] = $metadata->getColumnNames(); $baseCols[false][$entityClass] = $metadata->getFieldNames(); foreach ($metadata->getAssociationMappings() as $field => $association) { if (in_array($association['type'], [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE])) { $baseCols[true][$entityClass][] = $association['joinColumns'][0]['name']; $baseCols[false][$entityClass][] = $field; } } } return $baseCols[$returnColumnNames][$entityClass]; } return $this->getEntityManager()->getRepository($entityClass)->getBaseColumns($entityClass, $returnColumnNames); } /** * Get a list of entities. * * @param array<string,mixed> $args * * @return object[]|array<int,mixed>|iterable<object>|\Doctrine\ORM\Internal\Hydration\IterableResult<object>|Paginator<object>|SimplePaginator<mixed> */ public function getEntities(array $args = []) { $alias = $this->getTableAlias(); if (isset($args['qb'])) { $q = $args['qb']; } else { $q = $this->_em ->createQueryBuilder() ->select($alias) ->from($this->getEntityName(), $alias, "{$alias}.id"); if ($this->getClassMetadata()->hasAssociation('category')) { $q->leftJoin($this->getTableAlias().'.category', 'cat'); } } $this->buildClauses($q, $args); $query = $q->getQuery(); if (isset($args['result_cache'])) { if (!$args['result_cache'] instanceof ResultCacheOptions) { throw new \InvalidArgumentException(sprintf('The value of the key "result_cache" must be an instance of "%s"', ResultCacheOptions::class)); } ResultCacheHelper::enableOrmQueryCache($query, $args['result_cache']); } if (isset($args['hydration_mode'])) { $hydrationMode = constant('\\Doctrine\\ORM\\Query::'.strtoupper($args['hydration_mode'])); $query->setHydrationMode($hydrationMode); } else { $hydrationMode = Query::HYDRATE_OBJECT; } if (array_key_exists('iterable_mode', $args) && true === $args['iterable_mode']) { // Hydrate one by one return $query->toIterable([], $hydrationMode); } if (!empty($args['iterator_mode'])) { // When you remove the following, please search for the "iterator_mode" in the project. @\trigger_error('Using "iterator_mode" is deprecated. Use "iterable_mode" instead. Usage of "iterator_mode" will be removed in 6.0.', \E_USER_DEPRECATED); return $query->iterate(null, $hydrationMode); } elseif (empty($args['ignore_paginator'])) { if (!empty($args['use_simple_paginator'])) { // FAST paginator that can handle only simple queries using no joins or ManyToOne joins. return new SimplePaginator($query); } else { // SLOW paginator that can handle complex queries using oneToMany/ManyToMany joins. return new Paginator($query, false); } } else { // All results return $query->getResult($hydrationMode); } } /** * Get a single entity. * * @param int $id */ public function getEntity($id = 0): ?object { try { if (is_array($id)) { $q = $this->createQueryBuilder($this->getTableAlias()); $this->buildSelectClause($q, $id['select']); $q->where($this->getTableAlias().'.id = :id') ->setParameter('id', (int) $id['id']); $entity = $q->getQuery()->getSingleResult(); } else { $entity = $this->find((int) $id); } } catch (\Exception) { $entity = null; } return $entity; } /** * @return ExpressionBuilder */ public function getExpressionBuilder() { if (null === $this->expressionBuilder) { $this->expressionBuilder = new ExpressionBuilder(); } return $this->expressionBuilder; } /** * @param QueryBuilder|DbalQueryBuilder $q * @param array<mixed> $filter */ public function getFilterExpr($q, array $filter, ?string $unique = null): array { $unique = ($unique) ?: $this->generateRandomParameterName(); $parameter = []; if (isset($filter['group'])) { $expr = $q->expr()->orX(); foreach ($filter['group'] as $orGroup) { $groupExpr = $q->expr()->andX(); foreach ($orGroup as $subFilter) { [$subExpr, $subParameters] = $this->getFilterExpr($q, $subFilter); $groupExpr->add($subExpr); if (!empty($subParameters)) { $parameter = array_merge($parameter, $subParameters); } } $expr->add($groupExpr); } } elseif (str_contains($filter['column'], ',')) { $columns = explode(',', $filter['column']); $expr = $q->expr()->orX(); $setParameter = false; foreach ($columns as $c) { $subFilter = $filter; $subFilter['column'] = trim($c); [$subExpr, $parameterUsed] = $this->getFilterExpr($q, $subFilter, $unique); if ($parameterUsed) { $setParameter = true; } $expr->add($subExpr); } if ($setParameter) { $parameter = [$unique => $filter['value']]; } } else { $func = (!empty($filter['operator'])) ? $filter['operator'] : $filter['expr']; if (in_array($func, ['isNull', 'isNotNull'])) { $expr = $q->expr()->{$func}($filter['column']); } elseif (in_array($func, ['in', 'notIn'])) { $expr = $q->expr()->{$func}($filter['column'], ':'.$unique); $q->setParameter($unique, $filter['value'], ArrayParameterType::STRING); } elseif (in_array($func, ['like', 'notLike'])) { if (isset($filter['strict']) && !$filter['strict']) { if (is_numeric($filter['value'])) { // Postgres doesn't like using "LIKE" with numbers $func = ('like' == $func) ? 'eq' : 'neq'; } else { $filter['value'] = "%{$filter['value']}%"; } } $expr = $q->expr()->{$func}($filter['column'], ':'.$unique); $parameter = [$unique => $filter['value']]; } else { if (isset($filter['strict']) && !$filter['strict']) { $filter['value'] = "%{$filter['value']}%"; } $expr = $q->expr()->{$func}($filter['column'], ':'.$unique); $parameter = [$unique => $filter['value']]; } if (!empty($filter['not'])) { $expr = $q->expr()->not($expr); } } return [$expr, $parameter]; } /** * Returns a andX Expr() that takes into account isPublished, publishUp and publishDown dates * The Expr() sets a :now and :true parameter that must be set in the calling function. * * @param string|null $alias * @param bool $setNowParameter * @param bool $setTrueParameter * @param bool $allowNullForPublishedUp Allow entities without a published up date * * @return mixed */ public function getPublishedByDateExpression( $q, $alias = null, $setNowParameter = true, $setTrueParameter = true, $allowNullForPublishedUp = true ) { $isORM = $q instanceof QueryBuilder; if (null === $alias) { $alias = $this->getTableAlias(); } if ($setNowParameter) { $now = new \DateTime(); if (!$isORM) { $dtHelper = new DateTimeHelper($now, 'Y-m-d H:i:s', 'local'); $now = $dtHelper->toUtcString(); } $q->setParameter('now', $now); } if ($setTrueParameter) { $q->setParameter('true', true, 'boolean'); } if ($isORM) { $pub = 'isPublished'; $pubUp = 'publishUp'; $pubDown = 'publishDown'; } else { $pub = 'is_published'; $pubUp = 'publish_up'; $pubDown = 'publish_down'; } $expr = $q->expr()->andX( $q->expr()->eq("$alias.$pub", ':true'), $q->expr()->orX( $q->expr()->isNull("$alias.$pubDown"), $q->expr()->gte("$alias.$pubDown", ':now') ) ); if ($allowNullForPublishedUp) { $expr->add( $q->expr()->orX( $q->expr()->isNull("$alias.$pubUp"), $q->expr()->lte("$alias.$pubUp", ':now') ) ); } else { $expr->add( $q->expr()->andX( $q->expr()->isNotNull("$alias.$pubUp"), $q->expr()->lte("$alias.$pubUp", ':now') ) ); } return $expr; } /** * Get an array of rows from one table using DBAL. * * @param int $start * @param int $limit */ public function getRows($start = 0, $limit = 100, array $order = [], array $where = [], array $select = null, array $allowedJoins = []): array { $alias = $this->getTableAlias(); $metadata = $this->getClassMetadata(); $table = $metadata->getTableName(); $q = $this->_em->getConnection()->createQueryBuilder(); $q->select('count(*)') ->from($table, $alias); // Join associations for permission filtering $this->buildDbalJoinsFromAssociations($q, $metadata->getAssociationMappings(), $alias, $allowedJoins); $this->buildWhereClauseFromArray($q, $where); $count = $q->executeQuery()->fetchOne(); if ($select) { foreach ($select as &$column) { if (!str_contains($column, '.')) { $column = $alias.'.'.$column; } } $selectString = implode(', ', $select); } else { $selectString = $alias.'.*'; } $q->resetQueryPart('select') ->select($selectString) ->setFirstResult($start) ->setMaxResults($limit); $this->buildOrderByClauseFromArray($q, $order); $results = $q->executeQuery()->fetchAllAssociative(); return [ 'total' => $count, 'results' => $results, ]; } /** * Returns a single value for a single row. * * @param int $id * @param string $column * * @return string|null */ public function getValue($id, $column) { $q = $this->_em->getConnection()->createQueryBuilder(); $q->select($this->getTableAlias().'.'.$column) ->from($this->getClassMetadata()->getTableName(), $this->getTableAlias()) ->where($this->getTableAlias().'.id = :id') ->setParameter('id', $id); $result = $q->executeQuery()->fetchAssociative(); return $result[$column] ?? null; } /** * Array of search commands supported by the repository. * * @return string[]|array<string, string[]> */ public function getSearchCommands(): array { return ['mautic.core.searchcommand.ids']; } /** * Gets a list of published entities as an array id => label. * * @param array $parameters Parameters used in $expr * @param string $labelColumn Column that houses the label * @param string $valueColumn Column that houses the value * @param string $extraColumns String of extra select columns * @param int $limit Limit for results * * @return mixed[] */ public function getSimpleList(CompositeExpression $expr = null, array $parameters = [], $labelColumn = null, $valueColumn = 'id', $extraColumns = null, $limit = 0): array { $q = $this->_em->getConnection()->createQueryBuilder(); $alias = $prefix = $this->getTableAlias(); if (!empty($prefix)) { $prefix .= '.'; } $tableName = $this->_em->getClassMetadata($this->getEntityName())->getTableName(); $class = '\\'.$this->getClassName(); $reflection = new \ReflectionClass(new $class()); // Get the label column if necessary if (null == $labelColumn) { $labelColumn = $reflection->hasMethod('getTitle') ? 'title' : 'name'; } $q->select($prefix.$valueColumn.' as value, '.$prefix.$labelColumn.' as label'.($extraColumns ? ", $extraColumns" : '')) ->from($tableName, $alias) ->orderBy($prefix.$labelColumn); if (null !== $expr && $expr->count()) { $q->where($expr); } foreach ($parameters as $key => $value) { $q->setParameter($key, $value, is_array($value) ? ArrayParameterType::STRING : null); } // Published only if ($reflection->hasMethod('getIsPublished')) { $q->andWhere( $q->expr()->eq($prefix.'is_published', ':true') ) ->setParameter('true', true, 'boolean'); } if ($limit) { $q->setMaxResults((int) $limit); } return $q->executeQuery()->fetchAllAssociative(); } /** * @return string[] */ public function getStandardSearchCommands(): array { return [ 'mautic.core.searchcommand.ispublished', 'mautic.core.searchcommand.isunpublished', 'mautic.core.searchcommand.isuncategorized', 'mautic.core.searchcommand.ismine', 'mautic.core.searchcommand.category', 'mautic.core.searchcommand.ids', ]; } /** * @return literal-string */ public function getTableAlias(): string { return 'e'; } /** * @return mixed[] */ public function getTableColumns(): array { $columns = $this->getClassMetadata()->getColumnNames(); if ($associations = $this->getClassMetadata()->getAssociationMappings()) { foreach ($associations as $association) { if (!empty($association['joinColumnFieldNames'])) { $columns = array_merge($columns, array_values($association['joinColumnFieldNames'])); } } } natcasesort($columns); return array_values($columns); } /** * Returns entity table name. * * @return string */ public function getTableName() { return $this->getClassMetadata()->getTableName(); } /** * Persist an array of entities. * * @param array|ArrayCollection $entities */ public function saveEntities($entities): void { // iterate over the results so the events are dispatched on each delete $batchSize = 20; $i = 0; foreach ($entities as $entity) { $this->saveEntity($entity, false); if (0 === ++$i % $batchSize) { $this->getEntityManager()->flush(); } } $this->getEntityManager()->flush(); } /** * Save an entity through the repository. * * @param object $entity * @param bool $flush true by default; use false if persisting in batches */ public function saveEntity($entity, $flush = true): void { $this->getEntityManager()->persist($entity); if ($flush) { $this->getEntityManager()->flush(); } } /** * Insert entity if it does not exist, update if it does. * ID is set to the enity after upsert. * Main reason to use this over fetch/save is to avoid race conditions. * * Warning: This method use DBAL, not ORM. It will save only the entity you send it. * It will NOT save the entity's associations. Entity manager won't know that the entity was flushed. */ public function upsert(object $entity): void { $connection = $this->getEntityManager()->getConnection(); $metadata = $this->getClassMetadata(); $identifier = $metadata->getSingleIdentifierFieldName(); $makeUpdate = fn (string $column) => "{$column} = VALUES({$column})"; $columns = []; $values = []; $types = []; $set = []; $update = []; $hasId = $metadata->containsForeignIdentifier; foreach ($metadata->getFieldNames() as $fieldName) { $value = $metadata->getFieldValue($entity, $fieldName); if ($metadata->isIdentifier($fieldName)) { if ($value) { $hasId = true; } else { continue; } } $column = $metadata->getColumnName($fieldName); $columns[] = $column; $values[] = $value; $types[] = $metadata->getTypeOfField($fieldName); $set[] = '?'; $update[] = $makeUpdate($column); } foreach ($metadata->getAssociationNames() as $fieldName) { $assocEntity = $metadata->getFieldValue($entity, $fieldName); if (!$metadata->isAssociationWithSingleJoinColumn($fieldName) || !is_object($assocEntity)) { continue; } $idCol = ucfirst($metadata->getSingleAssociationReferencedJoinColumnName($fieldName)); $idGetter = "get{$idCol}"; $column = $metadata->getSingleAssociationJoinColumnName($fieldName); $columns[] = $column; $values[] = $assocEntity->$idGetter(); $types[] = Types::STRING; $set[] = '?'; $update[] = $makeUpdate($column); } $numberOfRowsAffected = $connection->executeStatement( 'INSERT INTO '.$this->getTableName().' ('.implode(', ', $columns).')'. ' VALUES ('.implode(', ', $set).')'. ' ON DUPLICATE KEY UPDATE '.implode(', ', $update), $values, $types ); if ($entity instanceof UpsertInterface) { $entity->setHasBeenInserted(UpsertInterface::ROWS_AFFECTED_ON_INSERT === $numberOfRowsAffected); $entity->setHasBeenUpdated(UpsertInterface::ROWS_AFFECTED_ON_UPDATE === $numberOfRowsAffected); } if ($hasId) { return; } $id = (int) $connection->lastInsertId(); $metadata->setFieldValue($entity, $identifier, $id); } /** * Set the current user (i.e. from security context) for use within repositories. * * @param User $user */ public function setCurrentUser($user): void { if (!$user instanceof User) { // just create a blank user entity $user = new User(); } $this->currentUser = $user; } public function setTranslator(TranslatorInterface $translator): void { $this->translator = $translator; } /** * Validate array for one order by condition. * * @param array $clause ['col' => 'column_a', 'dir' => 'ASC'] * * @return array * * @throws \InvalidArgumentException */ protected function validateOrderByClause($clause) { $msg = '"%s" is missing in the order by clause array.'; if (empty($clause['col'])) { throw new \InvalidArgumentException(sprintf($msg, 'col')); } if (empty($clause['dir'])) { $clause['dir'] = 'ASC'; } $clause['dir'] = $this->sanitize(strtoupper($clause['dir'])); $clause['col'] = $this->sanitize($clause['col'], ['_.']); return $clause; } /** * Validate the array for one where condition. * * @param array $clause ['expr' => 'expression', 'col' => 'DB column', 'val' => 'value to search for'] * * @throws \InvalidArgumentException */ protected function validateWhereClause(array $clause): array { $msg = '"%s" is missing in the where clause array.'; if (empty($clause['expr'])) { throw new \InvalidArgumentException(sprintf($msg, 'expr')); } if (empty($clause['col']) && empty($clause['column'])) { throw new \InvalidArgumentException(sprintf($msg, 'col')); } if (!array_key_exists('val', $clause) && !array_key_exists('value', $clause)) { $clause['val'] = ''; } $clause['expr'] = $this->sanitize($clause['expr']); $clause['col'] = $this->sanitize($clause['column'] ?? $clause['col'], ['_', '.']); if (isset($clause['value'])) { $clause['val'] = $clause['value']; } unset($clause['value'], $clause['column']); // Value will be sanitized by Doctrine return $clause; } /** * @param QueryBuilder|DbalQueryBuilder $qb * @param \StdClass|mixed[] $filters * * @return mixed[] */ protected function addAdvancedSearchWhereClause($qb, $filters): array { $parseFilters = []; if (isset($filters->root[0])) { // Function is determined by the second clause type $type = (isset($filters->root[1])) ? $filters->root[1]->type : $filters->root[0]->type; $parseFilters = &$filters->root; } elseif (isset($filters->children[0])) { $type = (isset($filters->children[1])) ? $filters->children[1]->type : $filters->children[0]->type; $parseFilters = &$filters->children; } elseif (is_array($filters)) { $type = (isset($filters[1])) ? $filters[1]->type : $filters[0]->type; $parseFilters = &$filters; } if (empty($type)) { $type = 'and'; } $parameters = []; $expressions = $qb->expr()->{"{$type}X"}(); if ($parseFilters) { $this->parseSearchFilters($parseFilters, $qb, $expressions, $parameters); } return [$expressions, $parameters]; } /** * @param QueryBuilder|DbalQueryBuilder $qb * @param \StdClass $filter */ protected function addCatchAllWhereClause($qb, $filter): array { foreach (['name', 'title'] as $column) { if ($this->getClassMetadata()->hasField($column)) { return $this->addStandardCatchAllWhereClause( $qb, $filter, [ $this->getTableAlias().'.'.$column, ] ); } } return [ false, [], ]; } /** * Unique handling for $filter->not since dbal does not support the not() function with it's QueryBuilder. * * @param QueryBuilder $q * @param object $filter */ protected function addDbalCatchAllWhereClause(&$q, $filter, array $columns): array { $unique = $this->generateRandomParameterName(); // ensure that the string has a unique parameter identifier $string = ($filter->strict) ? $filter->string : "{$filter->string}"; if ($filter->not) { $xFunc = 'andX'; $exprFunc = 'notLike'; } else { $xFunc = 'orX'; $exprFunc = 'like'; } $expr = $q->expr()->$xFunc(); foreach ($columns as $column) { $expr->add( $q->expr()->$exprFunc($column, ":$unique") ); } return [ $expr, ["$unique" => $string], ]; } /** * @param QueryBuilder|DbalQueryBuilder $q * @param \StdClass $filter */ protected function addSearchCommandWhereClause($q, $filter): array { $command = $filter->command; $expr = false; switch ($command) { case $this->translator->trans('mautic.core.searchcommand.ids'): case $this->translator->trans('mautic.core.searchcommand.ids', [], null, 'en_US'): $expr = $this->getIdsExpr($q, $filter); break; } return [ $expr, [], ]; } /** * @param QueryBuilder $q * @param object $filter */ protected function addStandardCatchAllWhereClause(&$q, $filter, array $columns): array { $unique = $this->generateRandomParameterName(); // ensure that the string has a unique parameter identifier $string = $filter->string; if (!$filter->strict) { if (!str_contains($string, '%')) { $string = "%$string%"; } } $ormQb = true; if ($q instanceof QueryBuilder) { $xFunc = 'orX'; $exprFunc = 'like'; } else { $ormQb = false; if ($filter->not) { $xFunc = 'andX'; $exprFunc = 'notLike'; } else { $xFunc = 'orX'; $exprFunc = 'like'; } } $expr = $q->expr()->$xFunc(); foreach ($columns as $col) { $expr->add( $q->expr()->$exprFunc($col, ":$unique") ); } if ($ormQb && $filter->not) { $expr = $q->expr()->not($expr); } return [ $expr, ["$unique" => $string], ]; } /** * @param DbalQueryBuilder|QueryBuilder $q * @param \StdClass $filter */ protected function addStandardSearchCommandWhereClause(&$q, $filter): array { $command = $filter->command; $unique = $this->generateRandomParameterName(); $returnParameter = true; // returning a parameter that is not used will lead to a Doctrine error $expr = false; $prefix = $this->getTableAlias(); $isDbalQB = $q instanceof DbalQueryBuilder; switch ($command) { case $this->translator->trans('mautic.core.searchcommand.ispublished'): case $this->translator->trans('mautic.core.searchcommand.ispublished', [], null, 'en_US'): $column = $isDbalQB ? 'is_published' : 'isPublished'; $expr = $q->expr()->eq("{$prefix}.{$column}", ":{$unique}"); $forceParameters = [$unique => true]; break; case $this->translator->trans('mautic.core.searchcommand.isunpublished'): case $this->translator->trans('mautic.core.searchcommand.isunpublished', [], null, 'en_US'): $column = $isDbalQB ? 'is_published' : 'isPublished'; $expr = $q->expr()->eq("{$prefix}.{$column}", ":{$unique}"); $forceParameters = [$unique => false]; break; case $this->translator->trans('mautic.core.searchcommand.isuncategorized'): case $this->translator->trans('mautic.core.searchcommand.isuncategorized', [], null, 'en_US'): $expr = $q->expr()->orX( $q->expr()->isNull("$prefix.category"), $q->expr()->eq("$prefix.category", $q->expr()->literal('')) ); $returnParameter = false; break; case $this->translator->trans('mautic.core.searchcommand.ismine'): case $this->translator->trans('mautic.core.searchcommand.ismine', [], null, 'en_US'): $column = $isDbalQB ? 'created_by' : 'createdBy'; $expr = $q->expr()->eq("{$prefix}.{$column}", ":{$unique}"); $forceParameters = [$unique => $this->currentUser->getId()]; break; case $this->translator->trans('mautic.core.searchcommand.category'): case $this->translator->trans('mautic.core.searchcommand.category', [], null, 'en_US'): // Find the category prefix $joins = $q->getDQLPart('join'); $catPrefix = false; foreach ($joins as $joinStatements) { /** @var Query\Expr\Join $join */ foreach ($joinStatements as $join) { if (str_contains($join->getJoin(), '.category')) { $catPrefix = $join->getAlias(); break; } } if (false !== $catPrefix) { break; } } if (false === $catPrefix) { $catPrefix = 'c'; } $expr = $q->expr()->like("{$catPrefix}.alias", ":{$unique}"); $filter->strict = true; break; case $this->translator->trans('mautic.core.searchcommand.ids'): case $this->translator->trans('mautic.core.searchcommand.ids', [], null, 'en_US'): $expr = $this->getIdsExpr($q, $filter); $returnParameter = false; break; } if ($expr && $filter->not) { $expr = $q->expr()->not($expr); } if (!empty($forceParameters)) { $parameters = $forceParameters; } elseif (!$returnParameter) { $parameters = []; } else { $string = $filter->string; if (!$filter->strict) { if (!str_contains($string, '%')) { $string = "$string%"; } } $parameters = ["$unique" => $string]; } return [$expr, $parameters]; } protected function appendExpression($appendTo, $expr) { if ($expr instanceof CompositeExpression || $expr instanceof Query\Expr\Composite) { if ($expr->count()) { $appendTo->add($expr); } } elseif (!empty($expr)) { $appendTo->add($expr); } } /** * @param QueryBuilder $q */ protected function buildClauses($q, array $args): bool { $this->buildSelectClause($q, $args); $this->buildIndexByClause($q, $args); $this->buildWhereClause($q, $args); $this->buildOrderByClause($q, $args); $this->buildLimiterClauses($q, $args); return true; } protected function buildDbalJoinsFromAssociations(DbalQueryBuilder $q, $associations, $alias, array $allowed): bool { $joinAdded = false; foreach ($associations as $property => $association) { $subJoinAdded = false; $targetMetdata = $this->_em->getRepository($association['targetEntity'])->getClassMetadata(); if ($propertyAllowedJoins = preg_grep('/^'.$property.'\..*/', $allowed)) { foreach ($propertyAllowedJoins as $key => $join) { $propertyAllowedJoins[$key] = str_replace($property.'.', '', $join); } $subJoinAdded = $this->buildDbalJoinsFromAssociations($q, $targetMetdata->getAssociationMappings(), $property, $propertyAllowedJoins); } if ($subJoinAdded || in_array($property, $allowed)) { // Unset the property so that it's not used again in other the next level unset($allowed[$property]); $targetTable = $targetMetdata->getTableName(); $hasNullable = false; $joinColumns = []; foreach ($association['joinColumns'] as $join) { if (!empty($join['nullable'])) { $hasNullable = true; } $joinColumns[] = $alias.'.'.$join['name'].' = '.$property.'.'.$join['referencedColumnName']; } $joinType = ($hasNullable) ? 'leftJoin' : 'join'; $q->$joinType($alias, $targetTable, $property, implode(' AND ', $joinColumns)); $joinAdded = true; } } return $joinAdded; } protected function buildIndexByClause($q, array $args) { if (!empty($args['index_by'])) { if (is_array($args['index_by'])) { [$indexAlias, $indexBy] = $args['index_by']; } else { $indexAlias = $this->getTableAlias(); $indexBy = $args['index_by']; } if (!str_starts_with($indexBy, $indexAlias)) { $indexBy = $indexAlias.'.'.$indexBy; } $q->indexBy($indexAlias, $indexBy); } } /** * @param QueryBuilder|DbalQueryBuilder $q */ protected function buildLimiterClauses($q, array $args): void { $start = array_key_exists('start', $args) ? $args['start'] : 0; $limit = array_key_exists('limit', $args) ? $args['limit'] : 0; if (!empty($limit)) { $q->setFirstResult($start) ->setMaxResults($limit); } } /** * @param QueryBuilder|DbalQueryBuilder $q */ protected function buildOrderByClause($q, array $args): void { $orderBy = array_key_exists('orderBy', $args) ? $args['orderBy'] : ''; if (!empty($args['filter']['order'])) { $this->buildOrderByClauseFromArray($q, $args['filter']['order']); } elseif (empty($orderBy)) { $defaultOrder = $this->getDefaultOrder(); foreach ($defaultOrder as $order) { $q->addOrderBy($order[0], $order[1]); } } else { $orderByDir = $this->sanitize( array_key_exists('orderByDir', $args) ? $args['orderByDir'] : '' ); // add direction after each column $parts = explode(',', $orderBy); foreach ($parts as $order) { $order = $this->sanitize($order, ['_', '.']); $q->addOrderBy($order, $orderByDir); } } } /** * Build order by from an array. * * @param QueryBuilder|DbalQueryBuilder $query * @param array $clauses [['col' => 'column_a', 'dir' => 'ASC']] * * @return array */ protected function buildOrderByClauseFromArray($query, array $clauses) { if ($clauses && is_array($clauses)) { foreach ($clauses as $clause) { $clause = $this->validateOrderByClause($clause); $column = (!str_contains($clause['col'], '.')) ? $this->getTableAlias().'.'.$clause['col'] : $clause['col']; $query->addOrderBy($column, $clause['dir']); } } } /** * @param QueryBuilder|DbalQueryBuilder $q */ protected function buildSelectClause($q, array $args) { $isOrm = $q instanceof QueryBuilder; if (isset($args['select'])) { // Build a custom select if (is_string($args['select'])) { $args['select'] = explode(',', $args['select']); } $selects = []; foreach ($args['select'] as $select) { if (str_contains($select, '.')) { [$alias, $select] = explode('.', $select); } else { $alias = $this->getTableAlias(); } if (!isset($selects[$alias])) { $selects[$alias] = []; } $selects[$alias][] = $select; } $partials = []; $ormColumns = $this->getBaseColumns($this->getClassName()); $dbalColumns = $this->getTableColumns(); foreach ($selects as $alias => $columns) { if ($isOrm) { if ($columns = array_intersect($columns, $ormColumns)) { $columns = array_map([$this, 'sanitize'], $columns); $partials[] = 'partial '.$alias.'.{'.implode(',', $columns).'}'; } } else { if ($columns = array_intersect($columns, $dbalColumns)) { foreach ($columns as $column) { $partials[] = $alias.'.'.$this->sanitize($column); } } } } if ($partials) { $newSelect = implode(', ', $partials); $select = ($isOrm) ? $q->getDQLPart('select') : $q->getQueryPart('select'); if ($isOrm) { $q->select($newSelect); } else { if (!$select || $this->getTableAlias() === $select || $this->getTableAlias().'.*' === $select) { $q->select($newSelect); } elseif (is_string($select) && str_contains($select, $this->getTableAlias().',')) { $q->select(str_replace($this->getTableAlias().',', $newSelect.',', $select)); } elseif (is_string($select) && str_contains($select, $this->getTableAlias().'.*,')) { $q->select(str_replace($this->getTableAlias().'.*,', $newSelect.',', $select)); } } } } if ($isOrm) { if (!$q->getDQLPart('select')) { $q->select($this->getTableAlias()); } } else { if (!$q->getQueryPart('select')) { $q->select($this->getTableAlias().'.*'); } } } /** * @param QueryBuilder|DbalQueryBuilder $q */ protected function buildWhereClause($q, array $args) { $filter = array_key_exists('filter', $args) ? $args['filter'] : ''; $filterHelper = new SearchStringHelper(); $advancedFilters = new \stdClass(); $advancedFilters->root = []; $advancedFilters->commands = []; // Reset advanced filter commands to be used in search query building $this->advancedFilterCommands = []; $advancedFilterStrings = []; $queryParameters = []; $queryExpression = $q->expr()->andX(); if (isset($args['ids'])) { $ids = array_map('intval', $args['ids']); $param = $this->generateRandomParameterName(); if ($q instanceof QueryBuilder) { $queryExpression->add( $q->expr()->in($this->getTableAlias().'.id', ':'.$param) ); $queryParameters[$param] = $ids; } else { $queryExpression->add( $q->expr()->in($this->getTableAlias().'.id', ':'.$param) ); $q->setParameter($param, $ids, ArrayParameterType::INTEGER); } } elseif (!empty($args['ownedBy'])) { $queryExpression->add( $q->expr()->in($this->getTableAlias().'.'.$args['ownedBy'][0], (string) $args['ownedBy'][1]) ); } if (!empty($filter)) { if (is_array($filter)) { if (!empty($filter['where'])) { // build clauses from array $this->buildWhereClauseFromArray($q, $filter['where']); } elseif (!empty($filter['criteria']) || !empty($filter['force'])) { $criteria = !empty($filter['criteria']) ? $filter['criteria'] : $filter['force']; if (is_array($criteria)) { // defined columns with keys of column, expr, value foreach ($criteria as $criterion) { if ($criterion instanceof Query\Expr || $criterion instanceof CompositeExpression) { $queryExpression->add($criterion); if (isset($criterion->parameters) && is_array($criterion->parameters)) { $queryParameters = array_merge($queryParameters, $criterion->parameters); unset($criterion->parameters); } } elseif (is_array($criterion)) { [$expr, $parameters] = $this->getFilterExpr($q, $criterion); $queryExpression->add($expr); if (is_array($parameters)) { $queryParameters = array_merge($queryParameters, $parameters); } } else { // string so parse as advanced search $advancedFilterStrings[] = $criterion; } } } else { // string so parse as advanced search $advancedFilterStrings[] = $criteria; } } if (!empty($filter['string'])) { $advancedFilterStrings[] = $filter['string']; } } else { $advancedFilterStrings[] = $filter; } if (!empty($advancedFilterStrings)) { foreach ($advancedFilterStrings as $parseString) { $parsed = $filterHelper->parseString($parseString); $advancedFilters->root = array_merge($advancedFilters->root, $parsed->root); $filterHelper->mergeCommands($advancedFilters, $parsed->commands); } $this->advancedFilterCommands = $advancedFilters->commands; [$expr, $parameters] = $this->addAdvancedSearchWhereClause($q, $advancedFilters); $this->appendExpression($queryExpression, $expr); if (is_array($parameters)) { $queryParameters = array_merge($queryParameters, $parameters); } } } // parse the filter if set if ($queryExpression->count()) { $q->andWhere($queryExpression); } // Parameters have to be set even if there are no expressions just in case a search command // passed back a parameter it used foreach ($queryParameters as $k => $v) { if (true === $v || false === $v) { $q->setParameter($k, $v, 'boolean'); } else { $q->setParameter($k, $v); } } } /** * @param QueryBuilder|DbalQueryBuilder $query * @param array $clauses [['expr' => 'expression', 'col' => 'DB column', 'val' => 'value to search for']] */ protected function buildWhereClauseFromArray($query, array $clauses, $expr = null) { $columnValue = ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'like', 'notLike', 'in', 'notIn', 'between', 'notBetween']; $justColumn = ['isNull', 'isNotNull', 'isEmpty', 'isNotEmpty']; $andOr = ['andX', 'orX']; if ($clauses && is_array($clauses)) { foreach ($clauses as $clause) { if (!empty($clause['internal']) && 'formula' === $clause['expr']) { $whereClause = array_key_exists('value', $clause) ? $clause['value'] : $clause['val']; if ($expr) { $expr->add($whereClause); } else { $query->andWhere($whereClause); } continue; } if (in_array($clause['expr'], $andOr)) { $composite = $query->expr()->{$clause['expr']}(); $this->buildWhereClauseFromArray($query, $clause['val'], $composite); if (null === $expr) { $query->andWhere($composite); } else { $expr->add($composite); } } else { $clause = $this->validateWhereClause($clause); $column = (!str_contains($clause['col'], '.')) ? $this->getTableAlias().'.'.$clause['col'] : $clause['col']; $whereClause = null; switch ($clause['expr']) { case 'between': case 'notBetween': if (is_array($clause['val']) && 2 === count($clause['val'])) { $not = 'notBetween' === $clause['expr'] ? ' NOT' : ''; $param = $this->generateRandomParameterName(); $query->setParameter($param, $clause['val'][0]); $param2 = $this->generateRandomParameterName(); $query->setParameter($param2, $clause['val'][1]); $whereClause = $column.$not.' BETWEEN :'.$param.' AND :'.$param2; } break; case 'isEmpty': case 'isNotEmpty': if ('isEmpty' === $clause['expr']) { $whereClause = $query->expr()->orX( $query->expr()->eq($column, $query->expr()->literal('')), $query->expr()->isNull($column) ); } else { $whereClause = $query->expr()->andX( $query->expr()->neq($column, $query->expr()->literal('')), $query->expr()->isNotNull($column) ); } break; case 'in': case 'notIn': $parsed = str_getcsv(html_entity_decode($clause['val']), ',', '"'); $param = $this->generateRandomParameterName(); $arg = count($parsed) > 1 ? $parsed : array_shift($parsed); if (is_array($arg)) { $whereClause = $query->expr()->{$clause['expr']}($column, ':'.$param); $query->setParameter($param, $arg, ArrayParameterType::STRING); } else { $expression = 'in' === $clause['expr'] ? 'eq' : 'neq'; $whereClause = $query->expr()->{$expression}($column, ':'.$param); $query->setParameter($param, $arg); } break; default: if (method_exists($query->expr(), $clause['expr'])) { if (in_array($clause['expr'], $columnValue)) { $param = $this->generateRandomParameterName(); $whereClause = $query->expr()->{$clause['expr']}($column, ':'.$param); $query->setParameter($param, $clause['val']); } elseif (in_array($clause['expr'], $justColumn)) { $whereClause = $query->expr()->{$clause['expr']}($column); } } } if ($whereClause) { if ($expr) { $expr->add($whereClause); } else { $query->andWhere($whereClause); } } } } } } /** * Generate a unique parameter name from int using base conversion. * This eliminates chance for parameter name collision and provides unique result for each number. * Duplicate method because of DI refactoring difficulty. * * @see \Mautic\LeadBundle\Segment\RandomParameterName * @see https://stackoverflow.com/questions/307486/short-unique-id-in-php/1516430#1516430 */ public function generateRandomParameterName(): string { $value = base_convert((string) $this->lastUsedParameterId, 10, 36); ++$this->lastUsedParameterId; return 'par'.$value; } protected function getDefaultOrder(): array { return []; } /** * @return mixed */ protected function getIdsExpr(&$q, $filter) { if ($ids = array_map('intval', explode(',', $filter->string))) { return $q->expr()->in($this->getTableAlias().'.id', $ids); } return false; } /** * Test to see if a given command is supported by the repository. * * @param string $command * @param string $subcommand */ protected function isSupportedSearchCommand(&$command, &$subcommand): bool { $commands = $this->getSearchCommands(); foreach ($commands as $k => $c) { if (is_array($c)) { // subcommands if ($this->translator->trans($k) == $command || $this->translator->trans($k, [], null, 'en_US') == $command) { foreach ($c as $subc) { if ($this->translator->trans($subc) == $subcommand || $this->translator->trans($subc, [], null, 'en_US') == $subcommand) { return true; } } } } elseif ($this->translator->trans($c) == $command || $this->translator->trans($c, [], null, 'en_US') == $command) { return true; } elseif ($this->translator->trans($c) == "{$command}:{$subcommand}" || $this->translator->trans($c, [], null, 'en_US') == "{$command}:{$subcommand}" ) { $command = "{$command}:{$subcommand}"; $subcommand = ''; return true; } } return false; } /** * @param \StdClass $parseFilters * @param QueryBuilder|DbalQueryBuilder $qb */ protected function parseSearchFilters($parseFilters, $qb, $expressions, &$parameters) { foreach ($parseFilters as $f) { /** @phpstan-ignore-line we are iterating over StdClass. We should refactor this into a collection of DTO objects in M6 */ if (isset($f->children)) { [$expr, $params] = $this->addAdvancedSearchWhereClause($qb, $f); } else { if (!empty($f->command)) { if ($this->isSupportedSearchCommand($f->command, $f->string)) { [$expr, $params] = $this->addSearchCommandWhereClause($qb, $f); } else { // treat the command:string as if its a single word $f->string = $f->command.':'.$f->string; $f->not = false; $f->strict = true; [$expr, $params] = $this->addCatchAllWhereClause($qb, $f); } } else { [$expr, $params] = $this->addCatchAllWhereClause($qb, $f); } } if (!empty($params)) { $parameters = array_merge($parameters, $params); } $this->appendExpression($expressions, $expr); } } /** * Sanitizes a string to alphanum plus characters in the second argument. * * @param string $sqlAttr * @param array $allowedCharacters */ protected function sanitize($sqlAttr, $allowedCharacters = []): string { return InputHelper::alphanum($sqlAttr, false, null, $allowedCharacters); } private function convertOrmPropertiesToColumns(array &$filters, array $properties): void { foreach ($filters as &$f) { $key = (isset($f['col'])) ? 'col' : 'column'; $col = $f[$key]; $alias = ''; if (str_contains($col, '.')) { [$alias, $col] = explode('.', $col); } if (in_array($col, $properties)) { $col = preg_replace('/(?<=\\w)(?=[A-Z])/', '_$1', $col); $col = strtolower($col); } $f[$key] = (!empty($alias)) ? $alias.'.'.$col : $col; } } /** * Checks if table contains any rows. */ protected function tableHasRows(string $table): bool { $query = $this->getEntityManager()->getConnection()->createQueryBuilder(); $query->select('null') ->from($table) ->setMaxResults(1); return (bool) count($query->executeQuery()->fetchAllAssociative()); } }