vendor\project-biz\database-bundle\src\Database\GenericRepository.php line 1832

Open in your IDE?
  1. <?php
  2. namespace ProjectBiz\DatabaseBundle\Database;
  3. use DateTime;
  4. use Doctrine\DBAL\Connection;
  5. use Doctrine\DBAL\Result;
  6. use Doctrine\DBAL\Query\QueryBuilder;
  7. use Exception;
  8. use ProjectBiz\UserBundle\Service\SecurityContextWrapper;
  9. use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaBuilderInterface;
  10. use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaComparison;
  11. use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaComposite;
  12. use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaConstant;
  13. use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaHasRights;
  14. use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaUnmappedColumn;
  15. use ProjectBiz\DatabaseBundle\Database\Platform\MySqlPlatform;
  16. use ProjectBiz\DatabaseBundle\Database\Platform\SqlServerPlatform;
  17. use ProjectBiz\DatabaseBundle\Exceptions\GenericRepositoryDeleteException;
  18. use ProjectBiz\DatabaseBundle\Exceptions\RequiredColumnNotReadableException;
  19. use ProjectBiz\DatabaseBundle\Exceptions\RequiredColumnNotWriteableException;
  20. use ProjectBiz\PortalBundle\Exceptions\UnaccessibleObjectException;
  21. use ProjectBiz\MailerBundle\Notification\MailNotifier;
  22. use ProjectBiz\PortalBundle\Exceptions\PortalException;
  23. use ProjectBiz\PortalBundle\Portal\Messages;
  24. use ProjectBiz\DatabaseBundle\Database\GenericRepositoryFactory;
  25. use Psr\Log\LoggerInterface;
  26. use Symfony\Component\EventDispatcher\EventDispatcher;
  27. use Symfony\Component\DependencyInjection\ContainerInterface;
  28. use Symfony\Component\HttpFoundation\RequestStack;
  29. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  30. use ProjectBiz\PortalBundle\Service\StandardLogHeader;
  31. class GenericRepository extends EventDispatcher
  32. {
  33.     const msgUnsupportedPlatform 'Die Datenbank-Plattform "%platform%" wird nicht unterstützt.';
  34.     const msgAncestorNotFound 'Urahn konnte nicht gefunden werden.';
  35.     const msgLockingColumnsNotWriteable 'Die Spalten zum Sperren des Objekts dürfen nicht geschrieben werden.';
  36.     const msgErrorClone 'Das Klonen des Objekts %id% in der Tabelle "%table%" ist fehlgeschlagen.';
  37.     const msgNoAccess 'Auf das Objekt konnte nicht zugegriffen werden.';
  38.     const msgUnreadableRequired '%column% wird benötigt, ist aber nicht lesbar.';
  39.     const msgMissingColDefs 'Fehlende Spaltendefinitionen';
  40.     const msgMissingColDefFor 'Fehlende Spaltendefinition für %column%';
  41.     const msgMissingParentReleaseParameter 'Eigenschaft parent_release_parameter fehlt.';
  42.     const msgAdminRightsDenied 'Die Zuweisung von ausgewählten Rechten ist nicht erlaubt.';
  43.     const msgMissingWritePermissions 'Sie haben nicht die erforderliche Berechtigung um diesen Datensatz zu speichern.';
  44.     private static $parameterIndex 1;
  45.     private        $db;
  46.     private        $user;
  47.     private        $securityContextWrapper;
  48.     private        $tableInfo;
  49.     private        $options;
  50.     private        $prefix;
  51.     private        $tableHelper;
  52.     private        $logger;
  53.     private        $platform;
  54.     private        $dateTimeFactory;
  55.     private        $mailNotifier;
  56.     private        $repoFactory;
  57.     private        $dateFormat;
  58.     private        $dateTimeFormat;
  59.     private        $schemaCache;
  60.     private        $request;
  61.     private        $container;
  62.     /**
  63.      * Construct.
  64.      *
  65.      * @param Connection $db
  66.      * @param TableInfoInterface $tableInfo
  67.      * @param GenericRepositoryOptions $options
  68.      * @param string $prefix
  69.      * @param TableHelper $tableHelper
  70.      * @param LoggerInterface $logger
  71.      *
  72.      * @throws PortalException
  73.      */
  74.     public function __construct(
  75.         Connection $db,
  76.         SecurityContextWrapper $securityContextWrapper,
  77.         TableInfoInterface $tableInfo,
  78.         GenericRepositoryOptions $options,
  79.         string $prefix,
  80.         TableHelper $tableHelper,
  81.         LoggerInterface $logger,
  82.         DateTimeFactory $dateTimeFactory,
  83.         MailNotifier $mailNotifier,
  84.         GenericRepositoryFactory $repoFactory,
  85.         $dateFormat,
  86.         $dateTimeFormat,
  87.         DatabaseSchemaCache $schemaCache,
  88.         RequestStack $requestStack,
  89.         ContainerInterface $container
  90.     )
  91.     {
  92.         $this->db $db;
  93.         $this->securityContextWrapper $securityContextWrapper;
  94.         $this->user $securityContextWrapper->getUser();
  95.         $this->tableInfo $tableInfo;
  96.         $this->options $options;
  97.         $this->prefix $prefix;
  98.         $this->tableHelper $tableHelper;
  99.         $this->logger $logger;
  100.         $this->dateTimeFactory $dateTimeFactory;
  101.         $this->mailNotifier $mailNotifier;
  102.         $this->repoFactory $repoFactory;
  103.         $this->dateFormat $dateFormat;
  104.         $this->dateTimeFormat $dateTimeFormat;
  105.         $this->schemaCache $schemaCache;
  106.         $this->request $requestStack->getCurrentRequest();
  107.         $this->container $container;
  108.     }
  109.     /**
  110.      * @return Connection The database connection used by this repository
  111.      */
  112.     public function getConnection()
  113.     {
  114.         return $this->db;
  115.     }
  116.     public function findAll(
  117.         array $sort null,
  118.         array $view null,
  119.         $hide_deleted true,
  120.         $group_by_version true,
  121.         $join_refs true,
  122.         $limit null,
  123.         $offset null
  124.     )
  125.     {
  126.         $processedView $this->processView($view);
  127.         $builder $this->findByQuery(
  128.             $columnMap,
  129.             null,
  130.             $sort,
  131.             $processedView,
  132.             $limit,
  133.             $offset,
  134.             $hide_deleted,
  135.             $group_by_version,
  136.             $join_refs
  137.         );
  138.         $select_columns $this->buildSelectColumns($columnMap$processedView);
  139.         if (count($select_columns) > 0) {
  140.             return $builder->select($select_columns)
  141.                 ->execute();
  142.         }
  143.         return null;
  144.     }
  145.     public function findSecondaryBy(
  146.         CriteriaBuilderInterface $filter,
  147.         array $sort null,
  148.         array $view null,
  149.         $limit null,
  150.         $offset null,
  151.         $hide_deleted true,
  152.         $group_by_version true,
  153.         $join_refs true
  154.     )
  155.     {
  156.         /* @todo:   this method should not be in this class; it currently is because of the tight coupling
  157.          *          with the column lookup.
  158.          */
  159.         $processedView $this->processView($view);
  160.         $builder $this->findByQuery(
  161.             $columnMap,
  162.             $filter,
  163.             $sort,
  164.             $processedView,
  165.             $limit,
  166.             $offset,
  167.             $hide_deleted,
  168.             $group_by_version,
  169.             $join_refs
  170.         );
  171.         $builder->resetQueryPart('orderBy'); // MSSQL doesn't like COUNT() with ORDER BY
  172.         $select_columns $this->buildSecondarySelectColumns($processedView);
  173.         if (count($select_columns) > 0) {
  174.             return $builder->select($select_columns)
  175.                 ->execute();
  176.         }
  177.         return null;
  178.     }
  179.     private function processView($view)
  180.     {
  181.         $unprocessedView $this->useAllOrGiven($view);
  182.         $unprocessedView $this->addRequiredColumnsAndPrimaryKey($unprocessedView);
  183.         $processedView = [];
  184.         foreach ($unprocessedView as $column) {
  185.             $splitColumn explode(':'$column);
  186.             if ($this->getTableInfo()
  187.                 ->isReadable($splitColumn[0], $this->getUserRights())
  188.             ) {
  189.                 if (!array_key_exists($splitColumn[0], $processedView)) {
  190.                     $processedView[$splitColumn[0]] = [];
  191.                 }
  192.                 if (count($splitColumn) === 2) {
  193.                     $processedView[$splitColumn[0]][] = $splitColumn[1];
  194.                 } else {
  195.                     if (count($splitColumn) > 2) {
  196.                         // @todo: create meaningful Exception
  197.                         throw new Exception();
  198.                     }
  199.                 }
  200.             } else {
  201.                 if ($this->isRequired($splitColumn[0])) {
  202.                     throw new PortalException(
  203.                         self::msgUnreadableRequired,
  204.                         ['%column%' => $splitColumn[0]]
  205.                     );
  206.                 }
  207.             }
  208.         }
  209.         return $processedView;
  210.     }
  211.     private function useAllOrGiven($view)
  212.     {
  213.         if ((null === $view) || !is_array($view)) {
  214.             return $this->tableInfo->getColumns();
  215.         }
  216.         return $view;
  217.     }
  218.     private function addRequiredColumnsAndPrimaryKey($view)
  219.     {
  220.         if ((null === $view) || !is_array($view)) {
  221.             $view = [];
  222.         }
  223.         $requiredColumns $this->getRequiredColumns();
  224.         foreach ($requiredColumns as $column) {
  225.             $view[] = $column;
  226.         }
  227.         $view[] = $this->getPrimaryKey();
  228.         return array_unique($view);
  229.     }
  230.     private function getRequiredColumns()
  231.     {
  232.         $requiredColumns $this->getOptions()
  233.             ->getOption('required_columns');
  234.         if (null !== $requiredColumns) {
  235.             return $requiredColumns;
  236.         }
  237.         return [];
  238.     }
  239.     /**
  240.      * @return GenericRepositoryOptions
  241.      *
  242.      * Mainly used (in 3 locations to get the system-columns)
  243.      */
  244.     public function getOptions()
  245.     {
  246.         return $this->options;
  247.     }
  248.     /**
  249.      * Get the primary key for the repository
  250.      *
  251.      * @return string The primary key for the repository
  252.      *
  253.      */
  254.     public function getPrimaryKey()
  255.     {
  256.         return $this->getOptions()
  257.             ->getOption('primary_key');
  258.     }
  259.     public function getTableInfo()
  260.     {
  261.         return $this->tableInfo;
  262.     }
  263.     /**
  264.      * @return string
  265.      */
  266.     private function getUserRights()
  267.     {
  268.         return $this->securityContextWrapper->getUserRights();
  269.     }
  270.     private function isRequired($column)
  271.     {
  272.         $requiredColumns $this->getOptions()
  273.             ->getOption('required_columns');
  274.         if (!is_array($requiredColumns)) {
  275.             return false;
  276.         }
  277.         return in_array($column$requiredColumns);
  278.     }
  279.     /**
  280.      * @param $columnMap
  281.      * @param CriteriaBuilderInterface|null $filter
  282.      * @param array|null $sort
  283.      * @param null $processedView
  284.      * @param null $limit
  285.      * @param null $offset
  286.      * @param bool|true $hide_deleted
  287.      * @param bool|true $group_by_version
  288.      * @param bool|true $join_refs
  289.      * @param bool|true $hide_archived
  290.      * @param string $mt_name
  291.      * @return QueryBuilder
  292.      * @throws Exception
  293.      */
  294.     public function findByQuery(
  295.         &$columnMap,
  296.         CriteriaBuilderInterface $filter null,
  297.         array $sort null,
  298.         $processedView null,
  299.         $limit null,
  300.         $offset null,
  301.         $hide_deleted true,
  302.         $group_by_version true,
  303.         $join_refs true,
  304.         $hide_archived true,
  305.         $mt_name 'mt'
  306.     )
  307.     {
  308.         if (null === $processedView) {
  309.             $processedView $this->processView(null);
  310.         }
  311.         $tablename $this->tableInfo->getInternalTablename();
  312.         $builder $this->db->createQueryBuilder();
  313.         $this->tableInfo->buildFrom($builder$mt_name);
  314.         $context = [
  315.             'builder' => $builder,
  316.             'where_used' => false
  317.         ];
  318.         if (null !== $limit) {
  319.             $builder->setMaxResults($limit);
  320.         }
  321.         if (null !== $offset) {
  322.             $builder->setFirstResult($offset);
  323.         }
  324.         $columnMap $this->buildColumnMapForJoins($builder$processedView$join_refs$mt_name);
  325.         /*
  326.          * Users can only see a document if they have the rights to see the _type_ of the document.
  327.          */
  328.         if ($tablename == $this->prefix 'Document' && !$this->getUser()->isAdmin()) {
  329.             $builder
  330.                 ->innerJoin(
  331.                     $mt_name,
  332.                     $this->prefix 'DocumentType',
  333.                     'doctype',
  334.                     '('.$mt_name.'.Document_Link_DocumentType_ID = doctype.DocumentType_ID'
  335.                     ' and (doctype.DocumentType_PermissionRead & ' $this->getUserRights() . ') > 0)'
  336.                 );
  337.         }
  338.         if ($group_by_version && $this->options->useVersioning()) {
  339.             $ancestorColumn $this->options->getAncestorColumn();
  340.             $versionColumn $this->options->getVersionColumn();
  341.             $builder
  342.                 ->leftJoin(
  343.                     $mt_name,
  344.                     $tablename,
  345.                     't2',
  346.                     '(' $mt_name '.'
  347.                     $ancestorColumn
  348.                     " = t2."
  349.                     $ancestorColumn
  350.                     ' AND ' $mt_name '.'
  351.                     $versionColumn
  352.                     ' < t2.'
  353.                     $versionColumn
  354.                     ')'
  355.                 )
  356.                 ->where('t2.' $this->getPrimaryKey() . ' IS NULL');
  357.             $context['where_used'] = true;
  358.             if ($hide_deleted && $this->options->useDeleted()) {
  359.                 $builder->andWhere($mt_name '.' $this->options->getDeletedColumn() . " <> 1");
  360.             }
  361.             if ($hide_archived && $this->options->useArchived()) {
  362.                 $builder->andWhere($mt_name '.' $this->options->getArchivedColumn() . " <> 1");
  363.             }
  364.             /*
  365.             Add filter and sorting
  366.             */
  367.             if (null !== $filter) {
  368.                 $criteria $filter->buildCriteria($builder$columnMap$processedView$mt_name);
  369.                 if ($criteria) {
  370.                     $builder->andWhere($criteria);
  371.                 }
  372.             }
  373.         } else {
  374.             $useAndWhere false;
  375.             if ($hide_deleted && $this->options->useDeleted()) {
  376.                 $useAndWhere true;
  377.                 $context['where_used'] = true;
  378.                 $builder->where($mt_name '.' $this->options->getDeletedColumn() . " <> 1");
  379.             }
  380.             if ($hide_archived && $this->options->useArchived()) {
  381.                 $useAndWhere true;
  382.                 $context['where_used'] = true;
  383.                 $builder->andWhere($mt_name '.' $this->options->getArchivedColumn() . " <> 1");
  384.             }
  385.             /*
  386.             Add filter and sorting
  387.             */
  388.             if (null !== $filter) {
  389.                 $criteria $filter->buildCriteria($builder$columnMap$processedView$mt_name);
  390.                 if ($useAndWhere) {
  391.                     if ($criteria) {
  392.                         $builder->andWhere($criteria);
  393.                     }
  394.                 } else {
  395.                     if ($criteria) {
  396.                         $builder->where($criteria);
  397.                         $context['where_used'] = true;
  398.                     }
  399.                 }
  400.             }
  401.         }
  402.         /*
  403.          * Consider the record rights.
  404.          *
  405.          * A user can see a record if he is the owner or the role matches.
  406.          */
  407.          $user $this->getUser();
  408.          if (isset($user)) {
  409.               if (($this->options->useReadRecordRights() || $this->options->useOwner()) && !$this->getUser()->isAdmin()) {
  410.                   $rightsCriterias = [];
  411.                   if ($this->options->useReadRecordRights()) {
  412.                       $readRecordRightsCriteria = new CriteriaHasRights($this->options->getReadRecordRightsColumn(), $this->getUser()->getRights()->getReadRecordRights());
  413.                       $user=   $this->getUser()->getRights()->getReadRecordRights();
  414.                       $rightsCriterias[] = $readRecordRightsCriteria->buildCriteria($builder$columnMap$processedView);
  415.                   }
  416.                   if ($this->options->useOwner()) {
  417.                       $ownerCriteria = new CriteriaComparison('=', new CriteriaUnmappedColumn($this->options->getOwnerColumn()), new CriteriaConstant($this->getUser()->getId()));
  418.                       $rightsCriterias[] = $ownerCriteria->buildCriteria($builder$columnMap$processedView);
  419.                   }
  420.                   $rightsCriteriaExpression call_user_func_array([$builder->expr(), 'orX'], $rightsCriterias);
  421.                   if ($context['where_used']) {
  422.                       $builder->andWhere($rightsCriteriaExpression);
  423.                   } else {
  424.                       $builder->where($rightsCriteriaExpression);
  425.                       $context['where_used'] = true;
  426.                   }
  427.               }
  428.         }
  429.         $defaultSorting $this->options->getOption('default_sorting');
  430.         $this->applySorting($builder$sort$defaultSorting$columnMap$processedView);
  431.         $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::FIND_BEFORE_EXECUTE);
  432.         return $builder;
  433.     }
  434.     private function buildColumnMapForJoins(QueryBuilder $builder$processedView$join_refs true$mt_name 'mt')
  435.     {
  436.         $columnMap = [];
  437.         if ($join_refs) {
  438.             $columnDefinitions $this->getColumnDefinitions();
  439.             $viewKeys array_keys($processedView);
  440.             foreach ($viewKeys as $column) {
  441.                 $col = new Column($column'view'$mt_name);
  442.                 $selectName $col->getSelectName();
  443.                 $defName $col->getDefinitionName();
  444.                 if (isset($columnDefinitions[$defName])) {
  445.                     $colDef $columnDefinitions[$defName];
  446.                     $source $colDef->getSource();
  447.                     if (null !== $source) {
  448.                         /*
  449.                          * @todo: Include joins of Syn-Tables!
  450.                          */
  451.                         $tname self::getNextParameterName();
  452.                         $sourceTableInfo $this->tableHelper->getTableInfo($source['table']);
  453.                         $sourceView = (null === $source['view'])
  454.                             ?[]
  455.                             :$source['view'];
  456.                         $columnMap[$column] = [
  457.                             'table' => $tname,
  458.                             'tablename' => $sourceTableInfo->getTablename(),
  459.                             'display' => $source['display_column'],
  460.                             'view' => array_unique(array_merge($sourceView$processedView[$column]))
  461.                         ];
  462.                         if ($colDef->isVirtual()) {
  463.                             $this->getPlatform()
  464.                                 ->refsBuildColumnMapForJoins(
  465.                                     $colDef->getType(),
  466.                                     $builder,
  467.                                     $this->tableInfo->getTablename(),
  468.                                     $this->tableInfo->getPrimaryKey(),
  469.                                     $source['table'],
  470.                                     $column,
  471.                                     $tname,
  472.                                     $colDef->getReverseProperty()
  473.                                 );
  474.                         } else {
  475.                             $condition sprintf(
  476.                                 "%s = %s.%s",
  477.                                 $selectName,
  478.                                 $tname,
  479.                                 $source['foreign_key']
  480.                             );
  481.                             $builder->leftJoin(
  482.                                 $mt_name,
  483.                                 $this->prefix $sourceTableInfo->getTablename(),
  484.                                 $tname,
  485.                                 $condition
  486.                             );
  487.                         }
  488.                     }
  489.                 }
  490.             }
  491.         }
  492.         return $columnMap;
  493.     }
  494.     /**
  495.      * @return \ProjectBiz\DatabaseBundle\Entity\ColumnDefinition[]
  496.      */
  497.     public function getColumnDefinitions()
  498.     {
  499.         return $this->getTableInfo()
  500.             ->getColumnDefinitions();
  501.     }
  502.     public static function getNextParameterName()
  503.     {
  504.         return 'p' . (++self::$parameterIndex);
  505.     }
  506.     public function getPlatform()
  507.     {
  508.         if (!isset($this->platform)) {
  509.             $platform $this->db->getDatabasePlatform();
  510.             if (is_a($platform'Doctrine\DBAL\Platforms\MySqlPlatform')) {
  511.                 $this->platform = new MySqlPlatform($this->prefix);
  512.             } else {
  513.                 if (is_a($platform'Doctrine\DBAL\Platforms\SqlServerPlatform')) {
  514.                     $this->platform = new SqlServerPlatform($this->prefix);
  515.                 } else {
  516.                     throw new PortalException(
  517.                         self::msgUnsupportedPlatform,
  518.                         ['%platform%' => $platform->getName()]
  519.                     );
  520.                 }
  521.             }
  522.         }
  523.         return $this->platform;
  524.     }
  525.     public function applySorting(
  526.         QueryBuilder $builder,
  527.         array $sort null,
  528.         array $defaultSorting null,
  529.         array $columnMap null,
  530.         $processedView null
  531.     )
  532.     {
  533.         if ((null === $sort) && (null === $defaultSorting) && !$this->getOptions()
  534.                 ->useManSort()
  535.         ) {
  536.             return;
  537.         }
  538.         $totalSort array_merge(
  539.             $sort
  540.                 $sort
  541.                 : [],
  542.             $this->getOptions()->useManSort()
  543.                 ? [
  544.                     [
  545.                         'column' => $this->getOptions()->getManSortColumn(),
  546.                         'mapped' => false
  547.                     ]
  548.                 ]
  549.                 : [],
  550.             $defaultSorting
  551.                 $defaultSorting
  552.                 : []
  553.         );
  554.         $usedColumnNames = [];
  555.         foreach ($totalSort as $sortEntry) {
  556.             $columnName $sortEntry['column'];
  557.             /*
  558.              * Make sure there are no duplicated column names for orderby clause.
  559.              * Especially sorting by column "_manSort" leads to sorting duplication.
  560.              */
  561.             {
  562.                 if (in_array($columnName$usedColumnNames)) {
  563.                     continue;
  564.                 }
  565.                 $usedColumnNames[] = $columnName;
  566.             }
  567.             $direction = isset($sortEntry['direction'])
  568.                 ?$sortEntry['direction']
  569.                 :'ASC';
  570.             $mapped = isset($sortEntry['mapped'])
  571.                 ?$sortEntry['mapped']
  572.                 :true;
  573.             if ($this->isVirtualForPropertyPath($columnName)) {
  574.                 return;
  575.             }
  576.             $parts explode(':'$columnName);
  577.             $numParts count($parts);
  578.             switch ($numParts) {
  579.                 case 1:
  580.                     $column = new Column($parts[0], 'view');
  581.                     $mappedColumn $column->getSelectName();
  582.                     if ((null !== $columnMap) && ($mapped)) {
  583.                         $mappedColumn = isset($columnMap[$parts[0]])
  584.                             ?($columnMap[$parts[0]]['table'] . '.' $columnMap[$parts[0]]['display'])
  585.                             :($column->getSelectName());
  586.                     }
  587.                     $builder->addOrderBy($mappedColumn$direction);
  588.                     break;
  589.                 case 2:
  590.                     $mappedColumn $columnMap[$parts[0]]['table'] . '.' $parts[1];
  591.                     $builder->addOrderBy($mappedColumn$direction);
  592.                     break;
  593.             }
  594.         }
  595.     }
  596.     protected function buildSecondarySelectColumns($processedView)
  597.     {
  598.         $columnDefinitions $this->getColumnDefinitions();
  599.         $selectColumns = [];
  600.         foreach ($processedView as $column => $sourceView) {
  601.             $col = new Column($column'view');
  602.             $selectName $col->getSelectName();
  603.             $defName $col->getDefinitionName();
  604.             $colDef $columnDefinitions[$defName];
  605.             $secHeader $colDef->getSecondaryHeader();
  606.             if (!empty($secHeader)) {
  607.                 $expr '';
  608.                 switch ($secHeader) {
  609.                     case 'sum':
  610.                         $expr 'SUM(' $selectName ')';
  611.                         break;
  612.                     case 'avg':
  613.                         $expr 'AVG(' $selectName ')';
  614.                         break;
  615.                     case 'min':
  616.                         $expr 'MIN(' $selectName ')';
  617.                         break;
  618.                     case 'max':
  619.                         $expr 'MAX(' $selectName ')';
  620.                         break;
  621.                     case 'count':
  622.                         $expr 'Count(' $selectName ')';
  623.                         break;
  624.                 }
  625.                 $selectColumns[] = $expr ' AS ' $column;
  626.             }
  627.         }
  628.         return $selectColumns;
  629.     }
  630.     public function countAll(
  631.         array $sort null,
  632.         array $view null,
  633.         $hide_deleted true,
  634.         $group_by_version true,
  635.         $join_refs true
  636.     )
  637.     {
  638.         $processedView $this->processView($view);
  639.         $builder $this->findByQuery(
  640.             $columnMap,
  641.             null,
  642.             $sort,
  643.             $processedView,
  644.             null,
  645.             null,
  646.             $hide_deleted,
  647.             $group_by_version,
  648.             $join_refs
  649.         );
  650.         $select_columns $this->buildSelectColumns($columnMap$processedView);
  651.         if (count($select_columns) > 0) {
  652.             return $builder->select('COUNT(*) AS COUNT')
  653.                 ->execute();
  654.         }
  655.         return 0;
  656.     }
  657.     /**
  658.      * Get the result of a specific column by sql aggregate function.
  659.      *
  660.      * @param type $column
  661.      * @return type
  662.      */
  663.     public function aggregate($column$table$aggregateFunction 'max') {
  664.         // @todo: support multiple columns at once to improve performance
  665.         $result $this->db->createQueryBuilder()
  666.             ->select($aggregateFunction '(' $column ') as aggregate_result')
  667.             ->from($this->prefix $table)
  668.             ->execute()->fetchAll();
  669.         if(count($result)>0) {
  670.             return $result[0]['aggregate_result'];
  671.         }
  672.         // if the result is empty, there are no records in the table.
  673.         return null;
  674.     }
  675.     protected function buildSelectColumns(array $columnMap$processedView)
  676.     {
  677.         $selectColumns = [];
  678.         foreach ($processedView as $viewName => $sourceView) {
  679.             $col = new Column($viewName'view');
  680.             $selectName $col->getSelectName();
  681.             $viewRefName $col->getViewRefName();
  682.             $defName $col->getDefinitionName();
  683.             if (isset($columnMap[$viewName])) {
  684.                 if ($this->getColumnDefinitions()[$defName]->isVirtual()) {
  685.                     $selectColumns[] = $this->getPlatform()
  686.                         ->refsBuildSelectColumnsData(
  687.                             $this->getColumnDefinitions()[$defName]->getType(),
  688.                             $this->tableInfo->getTablename(),
  689.                             $this->tableInfo->getPrimaryKey(),
  690.                             $columnMap[$viewName]['tablename'],
  691.                             $viewName,
  692.                             $columnMap[$viewName]['table'],
  693.                             $this->getColumnDefinitions()[$defName]->getReverseProperty()
  694.                         );
  695.                     $selectColumns[] = $this->getPlatform()
  696.                         ->refsBuildSelectColumnsDisplay(
  697.                             $this->getColumnDefinitions()[$defName]->getType(),
  698.                             $this->tableInfo->getTablename(),
  699.                             $this->tableInfo->getPrimaryKey(),
  700.                             $columnMap[$viewName]['tablename'],
  701.                             $viewName,
  702.                             $columnMap[$viewName]['table'],
  703.                             $this->getColumnDefinitions()[$defName]->getReverseProperty()
  704.                         );
  705.                 } else {
  706.                     // 1. Select original column as 'itself'
  707.                     $selectColumns[] = $selectName ' AS ' $viewName;
  708.                     // 2. Select view-columns
  709.                     foreach ($columnMap[$viewName]['view'] as $viewColumn) {
  710.                         $refTableInfo $this->tableHelper->getTableInfo($columnMap[$viewName]['tablename']);
  711.                         $refColumn = new Column($viewColumn'view');
  712.                         $refDefName $refColumn->getDefinitionName();
  713.                         if (!$refTableInfo->getColumnDefinitions()[$refDefName]->isVirtual()) {
  714.                             $selectColumns[] = $columnMap[$viewName]['table']
  715.                                 . '.'
  716.                                 $viewColumn
  717.                                 ' AS '
  718.                                 $viewRefName
  719.                                 '___'
  720.                                 $viewColumn;
  721.                         }
  722.                         else {
  723.                             $selectColumns[] = '\'nv\' AS '
  724.                                 $viewRefName
  725.                                 '___'
  726.                                 $viewColumn;
  727.                         }
  728.                     }
  729.                     // 3. Select display-column
  730.                     $selectColumns[] = $columnMap[$viewName]['table']
  731.                         . '.'
  732.                         $columnMap[$viewName]['display']
  733.                         . ' AS '
  734.                         $viewRefName
  735.                         '___display';
  736.                 }
  737.             } else {
  738.                 if (!$this->getColumnDefinitions()[$defName]->isVirtual()) {
  739.                     $selectColumns[] = $selectName ' AS ' $viewName;
  740.                 }
  741.             }
  742.         }
  743.         return $selectColumns;
  744.     }
  745.     public function countHistoryBy(
  746.         $id,
  747.         CriteriaBuilderInterface $filter null,
  748.         array $view null
  749.     )
  750.     {
  751.         if (!$this->options->useVersioning()) {
  752.             return false;
  753.         }
  754.         $processedView $this->processView($view);
  755.         $builder $this->historyByQuery(
  756.             $id,
  757.             $columnMap,
  758.             $filter,
  759.             $processedView,
  760.             null,
  761.             null
  762.         );
  763.         $builder->resetQueryPart('orderBy'); // MSSQL doesn't like COUNT() with ORDER BY
  764.         $select_columns $this->buildSelectColumns($columnMap$processedView);
  765.         if (count($select_columns) > 0) {
  766.             return $builder->select('COUNT(*) AS COUNT')
  767.                 ->execute();
  768.         }
  769.         return false;
  770.     }
  771.     public function countBy(
  772.         CriteriaBuilderInterface $filter null,
  773.         array $view null,
  774.         $limit null,
  775.         $offset null,
  776.         $hide_deleted true,
  777.         $group_by_version true,
  778.         $join_refs true
  779.     )
  780.     {
  781.         $processedView $this->processView($view);
  782.         $builder $this->findByQuery(
  783.             $columnMap,
  784.             $filter,
  785.             null,
  786.             $processedView,
  787.             $limit,
  788.             $offset,
  789.             $hide_deleted,
  790.             $group_by_version,
  791.             $join_refs
  792.         );
  793.         $builder->resetQueryPart('orderBy'); // MSSQL doesn't like COUNT() with ORDER BY
  794.         $select_columns $this->buildSelectColumns($columnMap$processedView);
  795.         if (count($select_columns) > 0) {
  796.             return $builder->select('COUNT(*) AS COUNT')
  797.                 ->execute();
  798.         }
  799.         return false;
  800.     }
  801.     public function fetchAll(Result $result null)
  802.     {
  803.         if (null === $result) {
  804.             return null;
  805.         }
  806.         $rows $result->fetchAll();
  807.         $ret = [];
  808.         foreach ($rows as $row) {
  809.             $ret[] = $this->handleRow($row);
  810.         }
  811.         return $ret;
  812.     }
  813.     protected function handleRow($row)
  814.     {
  815.         $columnDefinitions $this->getColumnDefinitions();
  816.         $ret = [];
  817.         foreach ($row as $key => $value) {
  818.             $keyParts explode('___'$key);
  819.             if (count($keyParts) > 1) {
  820.                 $vKey $keyParts[0];
  821.                 $sourceKey $keyParts[1];
  822.                 if (!isset($ret[$vKey])) {
  823.                     $ret[$vKey] = [];
  824.                 }
  825.                 $column = new Column($vKey'view');
  826.                 $baseKey substr($column->getDefinitionName(), 4);
  827.                 $sourceInfo $this->getTableInfo()->getSourceTableInfo($baseKey);
  828.                 $sourceColDefs $sourceInfo->getColumnDefinitions();
  829.                 $ret[$vKey][$sourceKey] = $this->getValueWithColDef($value$sourceKey$sourceColDefs);
  830.             } else {
  831.                 $ret[$key] = $this->getValueWithColDef($value$key$columnDefinitions);
  832.             }
  833.         }
  834.         return $ret;
  835.     }
  836.     private function getValueWithColDef($value$key$columnDefinitions) {
  837.         $column = new Column($key'view');
  838.         if (isset($columnDefinitions[$column->getDefinitionName()])) {
  839.             $colDef $columnDefinitions[$column->getDefinitionName()];
  840.             if ((($colDef->getType() == 'datetime') || ($colDef->getType() == 'date'))
  841.             && $value
  842.             ) {
  843.                 $date $this->dateTimeFactory->create(
  844.                     $this->db->getDatabasePlatform()
  845.                         ->getDateTimeFormatString(),
  846.                     $value
  847.                 );
  848.                 if ((\DateTime::getLastErrors()['warning_count'] ?? null) > 0) {
  849.                     $date false;
  850.                 }
  851.                 if ($date === false) {
  852.                     $date $this->dateTimeFactory->create(
  853.                         $this->db->getDatabasePlatform()
  854.                             ->getDateFormatString(),
  855.                         $value
  856.                     );
  857.                     if ((\DateTime::getLastErrors()['warning_count'] ?? null) > 0) {
  858.                         $date false;
  859.                     }
  860.                     if ($date !== false) {
  861.                         $date->setTime(00);
  862.                     }
  863.                 }
  864.                 if ($date === false) {
  865.                     return null;
  866.                 }
  867.                 return $date;
  868.             }
  869.             return $value;
  870.         }
  871.         return $value;
  872.     }
  873.     public function lock($id)
  874.     {
  875.         if (!$this->options->useLocking()) {
  876.             return false;
  877.         }
  878.         if (!$this->hasUser()) {
  879.             return false;
  880.         }
  881.         if (!$this->canAccessObject($id)) {
  882.             throw new PortalException(self::msgNoAccess);
  883.         }
  884.         $ancestor 0;
  885.         $tablename $this->tableInfo->getInternalTablename();
  886.         if ($this->options->useVersioning()) {
  887.             $ancestor $this->getAncestor($id);
  888.         }
  889.         $lockedAtColumn $this->options->getLockedAtColumn();
  890.         $lockedByColumn $this->options->getLockedByColumn();
  891.         if ($this->isWriteable($lockedAtColumn) && $this->isWriteable($lockedByColumn)) {
  892.             $builder $this->db->createQueryBuilder();
  893.             $builder
  894.                 ->update($this->tableInfo->getInternalTablename(), '')
  895.                 ->set(
  896.                     $lockedAtColumn,
  897.                     $builder->createNamedParameter(
  898.                         (new DateTime())->format(
  899.                             $this->db->getDatabasePlatform()
  900.                                 ->getDateTimeFormatString()
  901.                         )
  902.                     )
  903.                 )
  904.                 ->set(
  905.                     $lockedByColumn,
  906.                     $builder->createNamedParameter(
  907.                         $this->getUser()
  908.                             ->getId()
  909.                     )
  910.                 )
  911.                 ->where(
  912.                     $builder->expr()
  913.                         ->orX(
  914.                             $builder->expr()
  915.                                 ->eq(
  916.                                     $lockedByColumn,
  917.                                     $this->getUser()
  918.                                         ->getId()
  919.                                 ),
  920.                             $builder->expr()
  921.                                 ->eq($lockedByColumn0),
  922.                             $builder->expr()
  923.                                 ->isNull($lockedByColumn)
  924.                         )
  925.                 );
  926.             if ($this->options->useVersioning()) {
  927.                 $builder->andWhere($this->options->getAncestorColumn() . " = " $ancestor);
  928.             } else {
  929.                 $builder->andWhere($this->getPrimaryKey() . " = " $builder->createNamedParameter($id));
  930.             }
  931.             $context = ['builder' => $builder'where_used' => true];
  932.             $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::LOCK_BEFORE_EXECUTE);
  933.             $result $builder->execute();
  934.             if ($result == 0) {
  935.                 return false;
  936.             }
  937.         } else {
  938.             //throw new GenericRepositoryDeleteException(1810);
  939.             throw new Exception("Locking columns not writeable.");
  940.         }
  941.         return true;
  942.     }
  943.     /**
  944.      * @return bool
  945.      */
  946.     protected function hasUser()
  947.     {
  948.         return is_a($this->user'Symfony\Component\Security\Core\User\UserInterface');
  949.     }
  950.     public function canAccessObject($id$hide_deleted true$find_latest_version true)
  951.     {
  952.         $pk $this->tableInfo->getPrimaryKey();
  953.         $criteria = new CriteriaComparison('=', new CriteriaUnmappedColumn($pk), new CriteriaConstant($id));
  954.         return (false !== $this->findOneBy($criteria, [$pk], $hide_deleted$find_latest_versionfalse));
  955.     }
  956.     public function findOneBy(
  957.         CriteriaBuilderInterface $filter,
  958.         array $view null,
  959.         $hide_deleted true,
  960.         $group_by_version true,
  961.         $join_refs true
  962.     )
  963.     {
  964.         $processedView $this->processView($view);
  965.         $builder $this->findByQuery(
  966.             $columnMap,
  967.             $filter,
  968.             null,
  969.             $processedView,
  970.             1,
  971.             0,
  972.             $hide_deleted,
  973.             $group_by_version,
  974.             $join_refs
  975.         );
  976.         $select_columns $this->buildSelectColumns($columnMap$processedView);
  977.         if (count($select_columns) > 0) {
  978.             return $this->fetch(
  979.                 $builder->select($select_columns)
  980.                     ->execute()
  981.             );
  982.         }
  983.         return false;
  984.     }
  985.     public function fetch(Result $result)
  986.     {
  987.         if (null === $result) {
  988.             return null;
  989.         }
  990.         $row $result->fetch();
  991.         if (!$row) {
  992.             return $row;
  993.         }
  994.         // Map refs
  995.         return $this->handleRow($row);
  996.     }
  997.     private function getAncestor($id)
  998.     {
  999.         $tablename $this->tableInfo->getInternalTablename();
  1000.         $ancestorColumn $this->options->getAncestorColumn();
  1001.         $refObject $this->db->createQueryBuilder()
  1002.             ->select([$ancestorColumn])
  1003.             ->from($tablename'mt')
  1004.             ->where('mt.' $this->getPrimaryKey() . ' = :id')
  1005.             ->setParameter('id'$id)
  1006.             ->setMaxResults(1)
  1007.             ->execute()
  1008.             ->fetch();
  1009.         if (($refObject !== false) && isset($refObject[$ancestorColumn]) && ($refObject[$ancestorColumn] > 0)) {
  1010.             return $refObject[$ancestorColumn];
  1011.         } else {
  1012.             throw new PortalException(self::msgAncestorNotFound);
  1013.         }
  1014.     }
  1015.     public function isWriteable($column)
  1016.     {
  1017.         return $this->getTableInfo()
  1018.             ->isWriteable($column$this->getUserRights());
  1019.     }
  1020.     /**
  1021.      * @return mixed
  1022.      */
  1023.     public function getUser()
  1024.     {
  1025.         return $this->user;
  1026.     }
  1027.     public function unlock($id)
  1028.     {
  1029.         if (!$this->options->useLocking()) {
  1030.             return false;
  1031.         }
  1032.         if (!$this->hasUser()) {
  1033.             return false;
  1034.         }
  1035.         if (!$this->canAccessObject($id)) {
  1036.             throw new PortalException(self::msgNoAccess);
  1037.         }
  1038.         $ancestor 0;
  1039.         $tablename $this->tableInfo->getInternalTablename();
  1040.         if ($this->options->useVersioning()) {
  1041.             $ancestorColumn $this->options->getAncestorColumn();
  1042.             $refObject $this->db->createQueryBuilder()
  1043.                 ->select([$ancestorColumn])
  1044.                 ->from($tablename'mt')
  1045.                 ->where('mt.' $this->getPrimaryKey() . ' = :id')
  1046.                 ->setParameter('id'$id)
  1047.                 ->setMaxResults(1)
  1048.                 ->execute()
  1049.                 ->fetch();
  1050.             if (($refObject !== false) && isset($refObject[$ancestorColumn]) && ($refObject[$ancestorColumn] > 0)) {
  1051.                 $ancestor $refObject[$ancestorColumn];
  1052.             } else {
  1053.                 //throw new GenericRepositoryDeleteException(1801);
  1054.                 throw new PortalException(self::msgAncestorNotFound);
  1055.             }
  1056.         }
  1057.         $lockedAtColumn $this->options->getLockedAtColumn();
  1058.         $lockedByColumn $this->options->getLockedByColumn();
  1059.         if ($this->isWriteable($lockedAtColumn) && $this->isWriteable($lockedByColumn)) {
  1060.             $builder $this->db->createQueryBuilder();
  1061.             $builder
  1062.                 ->update($this->tableInfo->getInternalTablename(), '')
  1063.                 ->set($this->options->getLockedByColumn(), $builder->createNamedParameter(0));
  1064.             $where_used false;
  1065.             if (!$this->getUser()
  1066.                 ->isAdmin()
  1067.             ) {
  1068.                 $builder->where(
  1069.                     $builder->expr()
  1070.                         ->eq(
  1071.                             $this->options->getLockedByColumn(),
  1072.                             $this->getUser()
  1073.                                 ->getId()
  1074.                         )
  1075.                 );
  1076.                 $where_used true;
  1077.             }
  1078.             if ($this->options->useVersioning()) {
  1079.                 if ($where_used) {
  1080.                     $builder->andWhere($this->options->getAncestorColumn() . " = " $ancestor);
  1081.                 } else {
  1082.                     $builder->where($this->options->getAncestorColumn() . " = " $ancestor);
  1083.                     $where_used true;
  1084.                 }
  1085.             } else {
  1086.                 if ($where_used) {
  1087.                     $builder->andWhere($this->getPrimaryKey() . " = " $builder->createNamedParameter($id));
  1088.                 } else {
  1089.                     $builder->where($this->getPrimaryKey() . " = " $builder->createNamedParameter($id));
  1090.                     $where_used true;
  1091.                 }
  1092.             }
  1093.             $context = ['builder' => $builder'where_used' => $where_used];
  1094.             $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::UNLOCK_BEFORE_EXECUTE);
  1095.             $result $builder->execute();
  1096.             if ($result == 0) {
  1097.                 throw new PortalException(Messages::msgErrorUnlock);
  1098.             }
  1099.         } else {
  1100.             throw new PortalException(self::msgLockingColumnsNotWriteable);
  1101.         }
  1102.         return true;
  1103.     }
  1104.     public function deleteBy($criteria, &$allAffectedWatchDogNotificationRows null)
  1105.     {
  1106.         // @todo: Implement batch delete
  1107.         $group_by_version $this->options->useDeleted();
  1108.         $objects $this->findBy(
  1109.             $criteria,
  1110.             null,
  1111.             [$this->getPrimaryKey()],
  1112.             null,
  1113.             null,
  1114.             true,
  1115.             $group_by_version
  1116.         );
  1117.         $objects $objects->fetchAll(\PDO::FETCH_ASSOC);
  1118.         if (null !== $objects) {
  1119.             $affectedWatchDogNotificationRows 0;
  1120.             foreach ($objects as $obj) {
  1121.                 $this->delete($obj[$this->getPrimaryKey()], $affectedWatchDogNotificationRows);
  1122.                 if ($allAffectedWatchDogNotificationRows) {
  1123.                     $allAffectedWatchDogNotificationRows += $affectedWatchDogNotificationRows;
  1124.                 }
  1125.             }
  1126.         }
  1127.     }
  1128.     public function findBy(
  1129.         CriteriaBuilderInterface $filter null,
  1130.         array $sort null,
  1131.         array $view null,
  1132.         $limit null,
  1133.         $offset null,
  1134.         $hide_deleted true,
  1135.         $group_by_version true,
  1136.         $join_refs true
  1137.     )
  1138.     {
  1139.         $processedView $this->processView($view);
  1140.         $builder $this->findByQuery(
  1141.             $columnMap,
  1142.             $filter,
  1143.             $sort,
  1144.             $processedView,
  1145.             $limit,
  1146.             $offset,
  1147.             $hide_deleted,
  1148.             $group_by_version,
  1149.             $join_refs
  1150.         );
  1151.         $select_columns $this->buildSelectColumns($columnMap$processedView);
  1152.         if (count($select_columns) > 0) {
  1153.             return $builder->select($select_columns)
  1154.                 ->execute();
  1155.         }
  1156.         return null;
  1157.     }
  1158.     public function historyBy(
  1159.         $id,
  1160.         CriteriaBuilderInterface $filter,
  1161.         array $view null,
  1162.         $limit null,
  1163.         $offset null,
  1164.         $sort null
  1165.     ) {
  1166.         if (!$this->options->useVersioning()) {
  1167.             return null;
  1168.         }
  1169.         $processedView $this->processView($view);
  1170.         $builder $this->historyByQuery(
  1171.             $id,
  1172.             $columnMap,
  1173.             $filter,
  1174.             $processedView,
  1175.             $limit,
  1176.             $offset,
  1177.             $sort
  1178.         );
  1179.         $selectColumns $this->buildSelectColumns($columnMap$processedView);
  1180.         if (count($selectColumns) > 0) {
  1181.             return $builder->select($selectColumns)
  1182.                 ->execute();
  1183.         }
  1184.         return null;
  1185.     }
  1186.     public function historyByQuery(
  1187.         $id,
  1188.         &$columnMap,
  1189.         CriteriaBuilderInterface $filter null,
  1190.         $processedView null,
  1191.         $limit null,
  1192.         $offset null,
  1193.         $sort null,
  1194.         $mt_name 'mt'
  1195.     )
  1196.     {
  1197.         if (null === $processedView) {
  1198.             $processedView $this->processView(null);
  1199.         }
  1200.         $tablename $this->tableInfo->getInternalTablename();
  1201.         $builder $this->db->createQueryBuilder();
  1202.         $this->tableInfo->buildFrom($builder$mt_name);
  1203.         $context = [
  1204.             'builder' => $builder,
  1205.             'where_used' => false
  1206.         ];
  1207.         if (null !== $limit) {
  1208.             $builder->setMaxResults($limit);
  1209.         }
  1210.         if (null !== $offset) {
  1211.             $builder->setFirstResult($offset);
  1212.         }
  1213.         $columnMap $this->buildColumnMapForJoins($builder$processedViewtrue$mt_name);
  1214.         $ancestorColumn $this->options->getAncestorColumn();
  1215.         $versionColumn $this->options->getVersionColumn();
  1216.         $refObject $this->db->createQueryBuilder()
  1217.             ->select([$ancestorColumn])
  1218.             ->from($tablename$mt_name)
  1219.             ->where($mt_name '.' $this->getPrimaryKey() . ' = :id')
  1220.             ->setParameter('id'$id)
  1221.             ->setMaxResults(1)
  1222.             ->execute()
  1223.             ->fetch();
  1224.         if ($refObject !== false) {
  1225.             $ancestor $refObject[$ancestorColumn];
  1226.         } else {
  1227.             throw new \Exception('Missing refObject.');
  1228.         }
  1229.         $builder->andWhere($mt_name '.' $ancestorColumn ' = :ancestor');
  1230.         $context['where_used'] = true;
  1231.         $builder->setParameter('ancestor'$ancestor);
  1232.         /*
  1233.         Add filter and sorting
  1234.         */
  1235.         if (null !== $filter) {
  1236.             $criteria $filter->buildCriteria($builder$columnMap$processedView$mt_name);
  1237.             if ($criteria) {
  1238.                 $builder->andWhere($criteria);
  1239.             }
  1240.         }
  1241.         $this->applySorting($builder$sort, [['column' => $versionColumn'direction' => 'DESC']], $columnMap$processedView);
  1242.         $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::FIND_BEFORE_EXECUTE);
  1243.         return $builder;
  1244.     }
  1245.     public function delete($id, &$affectedWatchDogNotificationRows null)
  1246.     {
  1247.         $ancestor 0;
  1248.         $tablename $this->tableInfo->getInternalTablename();
  1249.         if (!$this->canAccessObject($id)) {
  1250.             throw new PortalException(self::msgNoAccess);
  1251.         }
  1252.         if ($this->options->useVersioning()) {
  1253.             $ancestorColumn $this->options->getAncestorColumn();
  1254.             $refObject $this->db->createQueryBuilder()
  1255.                 ->select([$ancestorColumn])
  1256.                 ->from($tablename'mt')
  1257.                 ->where('mt.' $this->getPrimaryKey() . ' = :id')
  1258.                 ->setParameter('id'$id)
  1259.                 ->setMaxResults(1)
  1260.                 ->execute()
  1261.                 ->fetch();
  1262.             if ($refObject !== false) {
  1263.                 $ancestor $refObject[$ancestorColumn];
  1264.             } else {
  1265.                 throw new GenericRepositoryDeleteException(1801);
  1266.             }
  1267.         }
  1268.         if ($this->options->useDeleted()) {
  1269.             if ($this->isWriteable($this->options->getDeletedColumn())) {
  1270.                 $obj = [
  1271.                     $this->options->getDeletedColumn() => 1,
  1272.                     $this->getPrimaryKey() => $id
  1273.                 ];
  1274.                 try {
  1275.                     $newId $this->persist($objtrue);
  1276.                     $this->incrementReleaseColumns($newId);
  1277.                     /*
  1278.                      *  Consider encryption if applicable
  1279.                      */
  1280.                     if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
  1281.                         $dsgvo $this->container->get('projectbiz.dsgvo');
  1282.                         if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
  1283.                             $dsgvo->encryptRecord($id$this->tableInfo->getTablename());
  1284.                         }
  1285.                     }
  1286.                 } catch (Exception $ex) {
  1287.                     throw new GenericRepositoryDeleteException(1802);
  1288.                 }
  1289.             } else {
  1290.                 throw new GenericRepositoryDeleteException(1810);
  1291.             }
  1292.         } else {
  1293.             $this->db->beginTransaction();
  1294.             try {
  1295.                 // @todo: delete Refs
  1296.                 if ($this->options->useManSort()) {
  1297.                     // Get the manSort-Value of the Object
  1298.                     $delObject $this->db->createQueryBuilder()
  1299.                         ->select([$this->options->getManSortColumn()])
  1300.                         ->from($tablename'mt')
  1301.                         ->where('mt.' $this->getPrimaryKey() . ' = :id')
  1302.                         ->setParameter('id'$id)
  1303.                         ->setMaxResults(1)
  1304.                         ->execute()
  1305.                         ->fetch();
  1306.                     $manSortDelete $delObject[$this->options->getManSortColumn()];
  1307.                     if (null !== $manSortDelete) {
  1308.                         // Consider the set ...
  1309.                         $manSortSet $this->options->getManSortSetColumns();
  1310.                         $manSortBuilder $this->db->createQueryBuilder()
  1311.                             ->update($this->tableInfo->getInternalTablename(), '')
  1312.                             ->set($this->options->getManSortColumn(), $this->options->getManSortColumn() . ' - 1')
  1313.                             ->where($this->options->getManSortColumn() . ' > ' $manSortDelete);
  1314.                         for ($i 0$i count($manSortSet); $i++) {
  1315.                             $manSortBuilder->andWhere($manSortSet[$i] . ' = ' $delObject[$manSortSet[$i]]);
  1316.                         }
  1317.                         $manSortBuilder->execute();
  1318.                     }
  1319.                 }
  1320.                 if ($this->options->useVersioning()) {
  1321.                     $builder $this->db->createQueryBuilder()
  1322.                         ->delete($tablename)
  1323.                         ->where($this->options->getAncestorColumn() . ' = ' $ancestor);
  1324.                     $context = ['builder' => $builder'where_used' => true];
  1325.                     $this->dispatch( new RepositoryEvent($this$context), RepositoryEvents::DELETE_BEFORE_EXECUTE);
  1326.                     $num_rows $builder->execute();
  1327.                 } else {
  1328.                     $builder $this->db->createQueryBuilder()
  1329.                         ->delete($tablename)
  1330.                         ->where($this->getPrimaryKey() . ' = ' $id);
  1331.                     $context = ['builder' => $builder'where_used' => true];
  1332.                     $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::DELETE_BEFORE_EXECUTE);
  1333.                     $num_rows $builder->execute();
  1334.                 }
  1335.                 /*
  1336.                  *  Consider encryption if applicable
  1337.                  */
  1338.                 if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
  1339.                     $dsgvo $this->container->get('projectbiz.dsgvo');
  1340.                     if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
  1341.                         $useVersioning $this->options->useVersioning();
  1342.                         $dsgvo->deleteRecord($ancestor$this->tableInfo->getTablename(), $useVersioning$useVersioning);
  1343.                     }
  1344.                 }
  1345.             } catch (\Exception $ex) {
  1346.                 $this->db->rollBack();
  1347.                 throw($ex);
  1348.             }
  1349.             $this->db->commit();
  1350.             if ($num_rows == 0) {
  1351.                 throw new GenericRepositoryDeleteException(1803);
  1352.             }
  1353.         }
  1354.         /*
  1355.          * Delete corresponding watchdog-notifications.
  1356.          */
  1357.         {
  1358.             $queryBuilder $this->db->createQueryBuilder();
  1359.             $queryBuilder
  1360.                 ->delete($this->prefix 'WatchDogNotification')
  1361.                 ->where('WatchDogNotification_LINK_Target_ID = ' .
  1362.                     $queryBuilder->createPositionalParameter($this->options->useVersioning() ? $ancestor $id))
  1363.                 ->andWhere('WatchDogNotification_Table_Caption = ' .
  1364.                     $queryBuilder->createPositionalParameter($this->tableInfo->getCaption()));
  1365.             // This variable is a reference
  1366.             $affectedWatchDogNotificationRows $queryBuilder->execute();
  1367.         }
  1368.     }
  1369.     public function persist($persistData$forceSave false$copyAfterCreate null$updateKey null)
  1370.     {
  1371.         $data $persistData;
  1372.         // Remove REFs
  1373.         foreach ($data as $key => $value) {
  1374.             if (strpos($key'REF_') === 0) {
  1375.                 unset($data[$key]);
  1376.             }
  1377.         }
  1378.         $is_new true;
  1379.         $id null;
  1380.         if ($updateKey) {
  1381.             $is_new = !$this->objectExists($updateKey$data[$updateKey]);
  1382.         } elseif (array_key_exists($this->getPrimaryKey(), $data)) {
  1383.             if (null !== $data[$this->getPrimaryKey()]) {
  1384.                 $id $data[$this->getPrimaryKey()];
  1385.                 $is_new = !$this->objectExists($id);
  1386.             } else {
  1387.                 unset($data[$this->getPrimaryKey()]);
  1388.             }
  1389.         }
  1390.         $rights $this->getUserRights();
  1391.         // Strip join-table data - it needs to be written "by hand" in the controller
  1392.         // Strip data that is not writeable
  1393.         foreach (array_keys($data) as $key) {
  1394.             $column = new Column($key'view');
  1395.             if (!$this->tableInfo->isWriteable($key$rights) || ($column->getTableAlias() != 'mt')) {
  1396.                 unset($data[$key]);
  1397.             }
  1398.         }
  1399.         if ($is_new) {
  1400.             return $this->create($data$copyAfterCreate);
  1401.         } else {
  1402.             unset($data[$this->getPrimaryKey()]);
  1403.             return $this->update($id$data$forceSave);
  1404.         }
  1405.     }
  1406.     private function objectExists($value$keyColumn null)
  1407.     {
  1408.         $tablename $this->tableInfo->getInternalTablename();
  1409.         $builder $this->db->createQueryBuilder();
  1410.         $result $builder->select('COUNT(*) AS cnt')
  1411.             ->from($tablename)
  1412.             ->where(($keyColumn ?? $this->tableInfo->getPrimaryKey()) . '=' $builder->createNamedParameter($value))
  1413.             ->execute();
  1414.         $value $result->fetch();
  1415.         return !(($value === false) || ($value['cnt'] == 0));
  1416.     }
  1417.     protected function create($data$copyAfterCreate null)
  1418.     {
  1419.         $needPersist false;
  1420.         $write_data = [];
  1421.         $refs = [];
  1422.         // Join sent data with required special columns
  1423.         $columns array_unique(array_merge(array_keys($data), $this->options->getAutoColumns()));
  1424.         $colDefs $this->getColumnDefinitions();
  1425.         foreach ($columns as $key) {
  1426.             $value = isset($data[$key])
  1427.                 ?$data[$key]
  1428.                 :null;
  1429.             if (array_key_exists($key$colDefs)) {
  1430.                 if ($colDefs[$key]->isVirtual()) {
  1431.                     $refs[] = [
  1432.                         'key' => $key,
  1433.                         'data' => [
  1434.                             'value' => $this->valueOrAutoValue($key$valuenulltrue$data)['value']
  1435.                         ]
  1436.                     ];
  1437.                 } else {
  1438.                     $write_data[$key] = $this->valueOrAutoValue($key$valuenulltrue$data)['value'];
  1439.                 }
  1440.             }
  1441.             $needPersist true;
  1442.         }
  1443.         if ($needPersist) {
  1444.             $this->db->beginTransaction();
  1445.             try {
  1446.                 if ($this->tableInfo->getTablename() === 'UserRights') {
  1447.                     $submittedAdminRights gmp_intval(gmp_and(gmp_init($data['UserRights_UserRight']), gmp_init(1)));
  1448.                     if ($submittedAdminRights == && !$this->securityContextWrapper->isSuperAdmin()) {
  1449.                         throw new PortalException(self::msgAdminRightsDenied, [], 0nullnulltrue);
  1450.                     }
  1451.                 }
  1452.                 $this->db->insert($this->tableInfo->getInternalTablename(), $write_data);
  1453.                 $lastInsertId $this->db->lastInsertId();
  1454.                 foreach ($refs as $ref) {
  1455.                     $this->updateRefs($lastInsertId$ref['key'], $ref['data']);
  1456.                 }
  1457.                 if ($this->options->useVersioning()) {
  1458.                     // Set the ancestor to the newly created id
  1459.                     $this->db->createQueryBuilder()
  1460.                         ->update($this->tableInfo->getInternalTablename(), '')
  1461.                         ->set($this->options->getAncestorColumn(), $lastInsertId)
  1462.                         ->where($this->getPrimaryKey() . " = " $lastInsertId)
  1463.                         ->execute();
  1464.                 }
  1465.                 $this->incrementReleaseColumns($lastInsertId);
  1466.                 if ($this->options->useManSort() && !array_key_exists('SPM_manSort'$data)) {
  1467.                     // Set the manSort-Value of the newly created entry to the next available value
  1468.                     // Consider the set ...
  1469.                     $manSortSet $this->options->getManSortSetColumns();
  1470.                     $manSortBuilder $this->db->createQueryBuilder()
  1471.                         ->from($this->tableInfo->getInternalTablename(), 'mt')
  1472.                         ->select('MAX(mt.' $this->options->getManSortColumn() . ') + 1 AS value');
  1473.                     if (count($manSortSet) > 0) {
  1474.                         $manSortBuilder
  1475.                             ->leftJoin(
  1476.                                 'mt',
  1477.                                 $this->tableInfo->getInternalTablename(),
  1478.                                 'j',
  1479.                                 'j.' $this->getPrimaryKey() . " = " $lastInsertId
  1480.                             )
  1481.                             ->where('mt.' $manSortSet[0] . ' = j.' $manSortSet[0]);
  1482.                         for ($i 1$i count($manSortSet); $i++) {
  1483.                             $manSortBuilder->andWhere('mt.' $manSortSet[$i] . ' = j.' $manSortSet[$i]);
  1484.                         }
  1485.                     }
  1486.                     $manSortValue $manSortBuilder
  1487.                         ->execute()
  1488.                         ->fetch()['value'];
  1489.                     if ($manSortValue == null) {
  1490.                         $manSortValue '1';
  1491.                     }
  1492.                     $this->db->createQueryBuilder()
  1493.                         ->update($this->tableInfo->getInternalTablename(), '')
  1494.                         ->set($this->options->getManSortColumn(), $manSortValue)
  1495.                         ->where($this->getPrimaryKey() . " = " $lastInsertId)
  1496.                         ->execute();
  1497.                 }
  1498.                 if ($copyAfterCreate != null) {
  1499.                     $queryBuilder $this->db->createQueryBuilder()
  1500.                         ->update($this->tableInfo->getInternalTablename(), '')
  1501.                         ->where($this->getPrimaryKey() . " = " $lastInsertId);
  1502.                     foreach ($copyAfterCreate as $source => $target) {
  1503.                         $queryBuilder->set($target$source);
  1504.                     }
  1505.                     $queryBuilder->execute();
  1506.                 }
  1507.                 $object $this->find($lastInsertIdfalsefalsetrue);
  1508.                 $context = [
  1509.                     'id' => $lastInsertId,
  1510.                     'object' => $object,
  1511.                     'database' => $this->db
  1512.                 ];
  1513.                 /*
  1514.                  *  Consider encryption if applicable
  1515.                  */
  1516.                 if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
  1517.                     $dsgvo $this->container->get('projectbiz.dsgvo');
  1518.                     if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
  1519.                         $dsgvo->encryptRecord($lastInsertId$this->tableInfo->getTablename());
  1520.                     }
  1521.                 }
  1522.                 $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::CREATE_AFTER_EXECUTE);
  1523.                 $this->db->commit();
  1524.                 return $lastInsertId;
  1525.             } catch (Exception $ex) {
  1526.                 $this->db->rollBack();
  1527.                 throw($ex);
  1528.             }
  1529.         }
  1530.         return 0;
  1531.     }
  1532.     /**
  1533.      * Increment release columns.
  1534.      *
  1535.      * If a column with name *_LINK_*_ReleaseID is present it has a counterpart column with name *_ReleaseId
  1536.      * in a corresponding table. That column gets incremented. The SPM set will be cloned and the new versions
  1537.      * get this new id.
  1538.      *
  1539.      * @param type $lastInsertId
  1540.      * @return type
  1541.      * @throws Exception
  1542.      */
  1543.     public function incrementReleaseColumns($lastInsertId)
  1544.     {
  1545.         if (!$this->options->useReleaseLink()) {
  1546.             return;
  1547.         }
  1548.         $routeParams $this->request->attributes->get('_route_params');
  1549.         if(!isset($routeParams['parent_release_parameter'])) {
  1550.             throw new Exception(self::msgMissingParentReleaseParameter);
  1551.         }
  1552.         $colDefs =  $this->tableInfo->getColumnDefinitions();
  1553.         $releaseLinkColumn $this->options->getReleaseLinkColumn();
  1554.         $primaryKeyColumn $this->getPrimaryKey();
  1555.         $parentTableName $colDefs[$releaseLinkColumn]->getSource()['table'];
  1556.         $parentTableRepo $this->repoFactory->createGenericRepository($parentTableName);
  1557.         $parentTable_releaseColumnName $colDefs[$releaseLinkColumn]->getSource()['foreign_key'];
  1558.         $parentId $routeParams[$routeParams['parent_release_parameter']];
  1559.         $parentReleaseId $parentTableRepo->find($parentId)[$parentTable_releaseColumnName];
  1560.         $repo $this->repoFactory->createGenericRepository($this->tableInfo->getTablename());
  1561.         $incrementedReleaseId $this->aggregate($parentTable_releaseColumnName$parentTableName'max' ) + 1;
  1562.         /*
  1563.          * Get the whole SPM-set. A set has the same release-id.
  1564.          * Do not get the last inserted spm, since it is updated exclusively.
  1565.          */
  1566.         $wholeSpmSet $repo->findBy(
  1567.                 new CriteriaComposite(
  1568.                     'and',
  1569.                     [
  1570.                         new CriteriaComparison(
  1571.                                 '=', new CriteriaUnmappedColumn($releaseLinkColumn), new CriteriaConstant($parentReleaseId)
  1572.                         ),
  1573.                         new CriteriaComparison(
  1574.                                 '<>', new CriteriaUnmappedColumn($primaryKeyColumn), new CriteriaConstant($lastInsertId)
  1575.                         )
  1576.                     ]
  1577.                 ), null, ['SPM_ID'], nullnullnulltruefalse
  1578.             )->fetchAll();
  1579.         /*
  1580.          * Update the SPM-set with the incremented release id.
  1581.          * The funtion versiondUpdate() duplicates a row when updating what is desired.
  1582.          */
  1583.         foreach ($wholeSpmSet as $spm) {
  1584.             $this->versionedUpdate($spm[$this->getPrimaryKey()], [$releaseLinkColumn => $incrementedReleaseId], true);
  1585.         }
  1586.         /*
  1587.          * Update the release id of the last inserted record.
  1588.          */
  1589.         $this->db->createQueryBuilder()
  1590.             ->update($this->tableInfo->getInternalTablename(), '')
  1591.             ->set($releaseLinkColumn$incrementedReleaseId)
  1592.             ->where($this->getPrimaryKey() . " = " $lastInsertId)
  1593.             ->execute();
  1594.         /*
  1595.          * Update the releaseColumn of the MPM record.
  1596.          */
  1597.         $parentTableRepo->versionedUpdate($parentId, [$parentTable_releaseColumnName => $incrementedReleaseId], true);
  1598.     }
  1599.     protected function valueOrAutoValue($key$value$original_value$creation false$data)
  1600.     {
  1601.         $userId 0;
  1602.         if (is_a($this->user'ProjectBiz\UserBundle\Entity\BaseUserInterface')) {
  1603.             /* @var $userEntity \ProjectBiz\UserBundle\Entity\User */
  1604.             $userEntity $this->user;
  1605.             $userId $userEntity->getId();
  1606.         }
  1607.         $param_name self::getNextParameterName();
  1608.         /*
  1609.         if ($creation && ($key == $this->getPrimaryKey())) {
  1610.             return null;
  1611.         }
  1612.         */
  1613.         if ($this->options->useModificationTimestamp()) {
  1614.             if ($key == $this->options->getModifiedByColumn()) {
  1615.                 return $this->simpleValue($param_name$userId);
  1616.             } else {
  1617.                 if ($key == $this->options->getModifiedAtColumn()) {
  1618.                     return $this->simpleValue(
  1619.                         $param_name,
  1620.                         (new DateTime())->format(
  1621.                             $this->db->getDatabasePlatform()
  1622.                                 ->getDateTimeFormatString()
  1623.                         )
  1624.                     );
  1625.                 }
  1626.             }
  1627.         }
  1628.         if ($this->options->useCreationTimestamp()) {
  1629.             if ($creation) {
  1630.                 // @todo: check the "else" case. It might be possible to send in new creation date on modification
  1631.                 if ($key == $this->options->getCreatedByColumn()) {
  1632.                     return $this->simpleValue($param_name$userId);
  1633.                 } else {
  1634.                     if ($key == $this->options->getCreatedAtColumn()) {
  1635.                         return $this->simpleValue(
  1636.                             $param_name,
  1637.                             (new DateTime())->format(
  1638.                                 $this->db->getDatabasePlatform()
  1639.                                     ->getDateTimeFormatString()
  1640.                             )
  1641.                         );
  1642.                     }
  1643.                 }
  1644.             } else {
  1645.                 if ($key == $this->options->getCreatedByColumn()) {
  1646.                     return $this->simpleValue($param_name$original_value);
  1647.                 } else {
  1648.                     if ($key == $this->options->getCreatedAtColumn()) {
  1649.                         return $this->simpleValue($param_name$original_value);
  1650.                     }
  1651.                 }
  1652.             }
  1653.         }
  1654.         if ($this->options->useLog()) {
  1655.             $logPos array_search($key$this->options->getLogColumns());
  1656.             if ($logPos !== false) {
  1657.                 if (!empty($value)) {
  1658.                     if ($this->options->hasLogHeader($logPos)) {
  1659.                         $logHeader $this->options->getLogHeader($logPos);
  1660.                         $value $logHeader->render() . $value;
  1661.                     } else {
  1662.                   // Here must be the standard-log-header in the service-yml declared !!
  1663.                         if (null !== ($this->options->getOption('standard-log-header'))) {
  1664.                             /** @var $standardLogHeader \ProjectBiz\PortalBundle\Service\LogHeaderInterface */
  1665.                             $standardLogHeader $this->options->getOption('standard-log-header');
  1666.                             $value $standardLogHeader->render() . $value;
  1667.                         }
  1668.                         else {
  1669.                           //var_dump(render());
  1670.                           //var_dump($data);
  1671.                           //var_dump($key);
  1672.                           //exit;
  1673.                         }
  1674.                     }
  1675.                 }
  1676.                 // @todo: There is a problem with "write-only" logs:
  1677.                 // only the most recent log entry is written, because the original_value is not readable and thus empty.
  1678.                 // This could be fixed by CONCAT_WS, or something with ISNULL, IFNULL, but not without doing a doctrine
  1679.                 // extension as those are not supported:
  1680.                 if ($creation || empty($original_value)) {
  1681.                     return $this->simpleValue($param_name$value);
  1682.                 } else {
  1683.                     $value "\n\n" $value;
  1684.                     return [
  1685.                         'expr' => $this->db->getDatabasePlatform()
  1686.                             ->getConcatExpression(
  1687.                                 $key,
  1688.                                 $this->db->getDatabasePlatform()
  1689.                                     ->getConcatExpression('CHAR(13)'':' $param_name)
  1690.                             ),
  1691.                         'param' => $param_name,
  1692.                         'value' => $value
  1693.                     ];
  1694.                 }
  1695.             }
  1696.         }
  1697.         if ($this->options->useVersioning()) {
  1698.             if ($creation && ($key == $this->options->getVersionColumn())) {
  1699.                 // @todo: check the "else" case. It might be possible to send in new version on modification
  1700.                 if (null !== $value) {
  1701.                     return $this->simpleValue($param_name$value); // Start with given version ...
  1702.                 } else {
  1703.                     return $this->simpleValue($param_name1); // ... or with default version 1
  1704.                 }
  1705.             }
  1706.         }
  1707.         if ($this->options->useDeleted()) {
  1708.             if ($key == $this->options->getDeletedColumn()) {
  1709.                 if ($value !== null) {
  1710.                     return $this->simpleValue($param_name$value);
  1711.                 } else {
  1712.                     return $this->simpleValue($param_name0);
  1713.                 }
  1714.             }
  1715.         }
  1716.         return $this->simpleValue($param_name$value);
  1717.     }
  1718.     protected function simpleValue($param_name$value)
  1719.     {
  1720.         if (is_a($value'\DateTime')) {
  1721.             /** @var $dataTimeValue \DateTime * */
  1722.             $dataTimeValue $value;
  1723.             $dbValue $dataTimeValue->format(
  1724.                 $this->db->getDatabasePlatform()
  1725.                     ->getDateTimeFormatString()
  1726.             );
  1727.         } else {
  1728.             $dbValue $value;
  1729.         }
  1730.         return [
  1731.             'expr' => ':' $param_name,
  1732.             'param' => $param_name,
  1733.             'value' => $dbValue
  1734.         ];
  1735.     }
  1736.     protected function updateRefs($id$key$modifiedValue)
  1737.     {
  1738.         // Update refs by dropping and rebuilding
  1739.         $sourceTableColumn 'Refs_SourceTable';
  1740.         $sourceIdColumn 'Refs_Source_LINK_ID';
  1741.         $targetTableColumn 'Refs_TargetTable';
  1742.         $targetIdColumn 'Refs_Target_LINK_ID';
  1743.         $refsProperty $key;
  1744.         if ($this->getColumnDefinitions()[$key]->getType() == 'fer') {
  1745.             $sourceTableColumn 'Refs_TargetTable';
  1746.             $sourceIdColumn 'Refs_Target_LINK_ID';
  1747.             $targetTableColumn 'Refs_SourceTable';
  1748.             $targetIdColumn 'Refs_Source_LINK_ID';
  1749.             $refsProperty $this->getColumnDefinitions()[$key]->getReverseProperty();
  1750.         }
  1751.         $refsBuilder $this->db->createQueryBuilder();
  1752.         $refsBuilder
  1753.             ->delete($this->prefix 'Refs')
  1754.             ->where(
  1755.                 $sourceTableColumn .
  1756.                 ' = ' .
  1757.                 $refsBuilder->createNamedParameter(
  1758.                     $this->getTableInfo()
  1759.                         ->getTablename()
  1760.                 )
  1761.             )
  1762.             ->andWhere($sourceIdColumn ' = ' $refsBuilder->createNamedParameter($id))
  1763.             ->andWhere('Refs_Property = ' $refsBuilder->createNamedParameter($refsProperty));
  1764.         $refsBuilder
  1765.             ->execute();
  1766.         if (isset($modifiedValue['value']) && strlen($modifiedValue['value'] > 0)) {
  1767.             $targetTable $this->getColumnDefinitions()[$key]->getSource()['table'];
  1768.             $refs array_map(
  1769.                 function ($value) use (
  1770.                     $key,
  1771.                     $id,
  1772.                     $targetTable,
  1773.                     $sourceTableColumn,
  1774.                     $sourceIdColumn,
  1775.                     $targetTableColumn,
  1776.                     $targetIdColumn,
  1777.                     $refsProperty
  1778.                 ) {
  1779.                     return [
  1780.                         $sourceTableColumn => $this->getTableInfo()
  1781.                             ->getTablename(),
  1782.                         $sourceIdColumn => $id,
  1783.                         $targetTableColumn => $targetTable,
  1784.                         $targetIdColumn => $value,
  1785.                         'Refs_Property' => $refsProperty
  1786.                     ];
  1787.                 },
  1788.                 explode(','$modifiedValue['value'])
  1789.             );
  1790.             foreach ($refs as $ref) {
  1791.                 $this->db->insert(
  1792.                     $this->prefix 'Refs',
  1793.                     $ref
  1794.                 );
  1795.             }
  1796.         }
  1797.     }
  1798.     /**
  1799.      * @param $id
  1800.      * @param $data
  1801.      * @param $forceSave
  1802.      *
  1803.      * @return string
  1804.      *
  1805.      * @throws Exception
  1806.      */
  1807.     protected function update($id$data$forceSave)
  1808.     {
  1809.         if (!$this->canAccessObject($idfalsefalse)) {
  1810.             throw new PortalException(self::msgNoAccess);
  1811.         }
  1812.         $usespecialunversionDoc=false;
  1813.         If ($this->tableInfo->getInternalTablename()=='Tab_Document'){
  1814.           if(!(isset($data['Document_LINK_File_ID']))){
  1815.             $usespecialunversionDoc =true;
  1816.           }
  1817.           $usespecialunversionDoc =false;
  1818.         }
  1819.         if ($this->options->useVersioning() && !$usespecialunversionDoc ) {
  1820.             return $this->versionedUpdate($id$data$forceSave);
  1821.         } else {
  1822.             return $this->unversionedUpdate($id$datanullnull$forceSave);
  1823.         }
  1824.     }
  1825.     /**
  1826.      * @param $id
  1827.      * @param $data
  1828.      * @param $forceSave
  1829.      *
  1830.      * @return string
  1831.      *
  1832.      * @throws Exception
  1833.      * @throws \Doctrine\DBAL\ConnectionException
  1834.      */
  1835.     protected function versionedUpdate($id$data$forceSave$compareWithLatestVersion false)
  1836.     {
  1837.         $original_object $this->find($id$compareWithLatestVersionfalsetrue);
  1838.         $changed_columns $this->getChangedColumns($data$original_object);
  1839.         if ((count($changed_columns) > 0) || $forceSave) {
  1840.             // Get latest object
  1841.             // @todo: object based access rights might lead to no latest object, crashing the saving ... need FIX!
  1842.             $latest_object $this->find($id);
  1843.             // Clone latest object and update all changed columns
  1844.             $this->db->beginTransaction();
  1845.             try {
  1846.                 $cloned_id $this->cloneObject($latest_object[$this->getPrimaryKey()]);
  1847.                 $new_version $latest_object[$this->options->getVersionColumn()] + 1;
  1848.                 $this->db->createQueryBuilder()
  1849.                     ->update($this->tableInfo->getInternalTablename(), '')
  1850.                     ->set($this->options->getVersionColumn(), $new_version)
  1851.                     ->where($this->getPrimaryKey() . " = " $cloned_id)
  1852.                     ->execute();
  1853.                 // Prevent overwriting with old value:
  1854.                 $data[$this->options->getVersionColumn()] = $new_version;
  1855.                 // Prevent overwriting with null:
  1856.                 $data[$this->options->getAncestorColumn()] = $latest_object[$this->options->getAncestorColumn()];
  1857.                 /*
  1858.                  * Prevent overwriting with null for ancestor- and release-columns:
  1859.                  */
  1860.                 {
  1861.                     $data[$this->options->getAncestorColumn()] = $latest_object[$this->options->getAncestorColumn()];
  1862.                     foreach ($this->options->getReleaseColumns() as $releaseColumn) {
  1863.                         if (!isset($data[$releaseColumn])) {
  1864.                             $data[$releaseColumn] = $latest_object[$releaseColumn];
  1865.                         }
  1866.                     }
  1867.                 }
  1868.                 $this->unversionedUpdate($cloned_id$data$changed_columns$original_object$forceSavefalsefalse);
  1869.                 if (!$forceSave) {
  1870.                     $this->incrementReleaseColumns($cloned_id);
  1871.                 }
  1872.                 $new_object $this->find($cloned_idfalsefalsetrue);
  1873.                 $context = [
  1874.                     'id' => $cloned_id,
  1875.                     'prev_id' => $id,
  1876.                     'object' => $new_object,
  1877.                     'prev_object' => $latest_object,
  1878.                     'database' => $this->db
  1879.                 ];
  1880.                 $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::UPDATE_AFTER_EXECUTE);
  1881.                 $this->db->commit();
  1882.                 // Send notifications about changes in this record
  1883.                 $this->sendNotifications($changed_columns$original_object$new_object);
  1884.                 return $cloned_id;
  1885.             } catch (\Exception $ex) {
  1886.                 if ($this->db->isTransactionActive()) {
  1887.                     $this->db->rollBack();
  1888.                 }
  1889.                 throw($ex);
  1890.             }
  1891.         }
  1892.         return $id;
  1893.     }
  1894.     /**
  1895.      * Examine all changed columns by checking them against the conditions in Tab_MailTrigger.
  1896.      * Send a notification to the particular e-mail adress if the condition applies.
  1897.      *
  1898.      * @param $changedColumns
  1899.      * @param $originalObject
  1900.      * @param $newObject
  1901.      */
  1902.     protected function sendNotifications($changedColumns$originalObject$newObject)
  1903.     {
  1904.         if (!count($changedColumns)) {
  1905.             return;
  1906.         }
  1907.         $colDefs $this->tableInfo->getColumnDefinitions();
  1908.         $columnNames = [];
  1909.         $changedColumnNames = [];
  1910.         /*
  1911.          * Build an array with ColumnDefinition_ID and column-name mapping for both,
  1912.          * $columnNames and $changedColumnNames.
  1913.          * For example:
  1914.          * [
  1915.          *   7  => MPM_Projectname,
  1916.          *   28 => MPM_Text,
  1917.          *      ...
  1918.          * ]
  1919.          *
  1920.          * In $changedColumnNames ignore the objects which have the value 0 at column _MailTrigger_Active.
  1921.          * If _MailTrigger_Active column does not exist, it defaults to active.
  1922.          */
  1923.         {
  1924.             $mailTriggerActiveColumn $colDefs[$changedColumns[0]]->getTable() . '_MailTrigger_Active';
  1925.             foreach ($colDefs as $colName => $colDef) {
  1926.                 $columnNames[$colDef->getId()] = $colName;
  1927.             }
  1928.             foreach ($changedColumns as $column) {
  1929.                 if (isset($colDefs[$mailTriggerActiveColumn])) {
  1930.                     if ($newObject[$mailTriggerActiveColumn] == 0) {
  1931.                         continue;
  1932.                     }
  1933.                 }
  1934.                 $changedColumnNames[$colDefs[$column]->getId()] = $column;
  1935.             }
  1936.         }
  1937.         $allMailtriggers $this->repoFactory->createGenericRepository('MailTrigger')->findAll();
  1938.         if ($allMailtriggers) {
  1939.             $possibleMailTriggers $allMailtriggers->fetchAll();
  1940.         } else {
  1941.             $possibleMailTriggers = [];
  1942.         }
  1943.         $affectedMailTriggers = [];
  1944.         /*
  1945.          * Let's go further and check if their conditions apply.
  1946.          * Filter all applying mail-triggers and store them in $affectedMailTriggers[].
  1947.          */
  1948.         foreach ($possibleMailTriggers as $possibleMailTrigger) {
  1949.             foreach ($changedColumnNames as $changedColumnId => $changedColumnName) {
  1950.                 if ($possibleMailTrigger['MailTrigger_LINK_Column_ID'] != $changedColumnId && $possibleMailTrigger['MailTrigger_LINK_Right_Column_ID'] != $changedColumnId ) {
  1951.                     continue;
  1952.                 }
  1953.                 $comparisonOperator $possibleMailTrigger['REF_MailTrigger_LINK_MailTriggerType_ID___MailTriggerType_Definition'];
  1954.                 /*
  1955.                  * Make sure there is no compromising code injection.
  1956.                  */
  1957.                 if (!$possibleMailTrigger['MailTrigger_Trigger_Changes']) {
  1958.                     $allowedComparisonOperators = [
  1959.                         '==',
  1960.                         '!=',
  1961.                         '<>',
  1962.                         '<',
  1963.                         '>',
  1964.                         '<=',
  1965.                         '>=',
  1966.                     ];
  1967.                     if (!in_array($comparisonOperator$allowedComparisonOperators)) {
  1968.                         throw new PortalException(
  1969.                             'Comparison operator "'.$comparisonOperator.'" in column MailTriggerType_Definition is not allowed. Allowed operators are:'.PHP_EOL.
  1970.                             implode(', '$allowedComparisonOperators)
  1971.                         );
  1972.                     }
  1973.                 }
  1974.                 /*
  1975.                  * MailTrigger_Trigger_Changes is a checkbox to send notifications when the observed value has changed.
  1976.                  */
  1977.                 if ($possibleMailTrigger['MailTrigger_Trigger_Changes']) {
  1978.                     if ($possibleMailTrigger['MailTrigger_LINK_Column_ID'] == $changedColumnId && $newObject[$changedColumnName] !== $originalObject[$changedColumnName]) {
  1979.                         $affectedMailTriggers[] = $possibleMailTrigger;
  1980.                     }
  1981.                     // If MailTrigger_Trigger_Changes is active, the other possible comparison with MailTrigger_LINK_Right_Column_ID is skipped.
  1982.                     continue;
  1983.                 }
  1984.                 /*
  1985.                  * $leftValue is the left operand of the comparison.
  1986.                  */
  1987.                 $leftValueColumnName $possibleMailTrigger['REF_MailTrigger_LINK_Column_ID___ColDefinition_InternalColumnname'];
  1988.                 $leftValue $newObject[$leftValueColumnName];
  1989.                 $leftType $colDefs[$leftValueColumnName]->getType();
  1990.                 /*
  1991.                  * $rightValue is the right operand of the comparison.
  1992.                  * It can either be a constant (else part) or relate to a column-id (if-part).
  1993.                  */
  1994.                 if ($possibleMailTrigger['MailTrigger_LINK_Right_Column_ID']) {
  1995.                     $rightValueColumnName $possibleMailTrigger['REF_MailTrigger_LINK_Right_Column_ID___ColDefinition_InternalColumnname'];
  1996.                     $rightValue $newObject[$rightValueColumnName];
  1997.                     /*
  1998.                      * Some data types need a special handling for a comparison.
  1999.                      * The type of the right operand is not needed. The user should not create a mailtrigger
  2000.                      * with two columns having different data types.
  2001.                      */
  2002.                     if ($leftType == 'date' || $leftType == 'datetime') {
  2003.                         if(!$rightValue || !$leftValue) {
  2004.                             continue;
  2005.                         }
  2006.                         $leftValue $leftValue->getTimestamp();
  2007.                         $rightValue $rightValue->getTimestamp();
  2008.                     } elseif ($leftType == 'link') {
  2009.                         $leftValue $newObject['REF_' $leftValueColumnName]['display'];
  2010.                         $rightValue $newObject['REF_' $rightValueColumnName]['display'];
  2011.                     }
  2012.                 } else {
  2013.                     $rightValue $possibleMailTrigger['MailTrigger_Value'];
  2014.                     /*
  2015.                      * Some data types need a special handling for a comparison.
  2016.                      */
  2017.                     if ($leftType == 'date' || $leftType == 'datetime') {
  2018.                         $rightValue \DateTime::createFromFormat(
  2019.                                 $leftType == 'date' $this->dateFormat $this->dateTimeFormat,
  2020.                                 $rightValue
  2021.                             );
  2022.                         if ($rightValue) {
  2023.                             $rightValue $rightValue->getTimestamp();
  2024.                         }
  2025.                         $leftValue $leftValue->getTimestamp();
  2026.                     } elseif ($leftType == 'link') {
  2027.                         $leftValue $newObject['REF_' $leftValueColumnName]['display'];
  2028.                     } elseif ($leftType == 'percent') {
  2029.                         $rightValue /= 100;
  2030.                     }
  2031.                 }
  2032.                 /*
  2033.                  * MailTrigger_Deviance is a percental variance applicable for columns with type »euro«.
  2034.                  * The deviance can be positive or negative.
  2035.                  */
  2036.                 if ($leftType == 'euro' && $possibleMailTrigger['MailTrigger_Deviance']) {
  2037.                     // Remove whitespaces. '- 10' will be converted to '-10';
  2038.                     $deviance str_replace(' '''$possibleMailTrigger['MailTrigger_Deviance']);
  2039.                     $leftValue += ($leftValue $deviance/100);
  2040.                 }
  2041.                 /*
  2042.                  *  Does the condition of a trigger apply?
  2043.                  *  The eval statement adds up to a comparison, for example:
  2044.                  *  »return '160'>'150';«
  2045.                  *  or
  2046.                  *  »return 'string1'=='string2';«
  2047.                  */
  2048.                 if ($rightValue !== false && eval('return \'' $leftValue '\'' $comparisonOperator '\'' $rightValue '\';')) {
  2049.                     $affectedMailTriggers[] = $possibleMailTrigger;
  2050.                 }
  2051.             }
  2052.         }
  2053.         $userRepo false;
  2054.         /*
  2055.          * Send an email for every trigger that took effect
  2056.          */
  2057.         foreach ($affectedMailTriggers as $affectedMailTrigger) {
  2058.             /*
  2059.              * create object only once, to consider performance.
  2060.              */
  2061.             if(!$userRepo) {
  2062.                 $userRepo $this->repoFactory->createGenericRepository('User');
  2063.             }
  2064.             $columnName $columnNames[$affectedMailTrigger['MailTrigger_LINK_Column_ID']];
  2065.             $infoColumns = [];
  2066.             /*
  2067.              * There are 6 slots which optionally contain columns that are available in the mail template as further info about the changed record.
  2068.              * They can be included in the mail template like: {{infoColumns[1]}}
  2069.              */
  2070.             for ($i 1$i <= 6$i++) {
  2071.                 if($affectedMailTrigger['MailTrigger_LINK_Info' $i '_Column_ID']) {
  2072.                     $infoColumnName $columnNames[$affectedMailTrigger['MailTrigger_LINK_Info' $i '_Column_ID']];
  2073.                     /*
  2074.                      * If the info-column is of type link, grab the target value to avoid displaying an id.
  2075.                      */
  2076.                     if ($colDefs[$infoColumnName]->getType() == 'link') {
  2077.                         $infoColumns[$i] = $newObject['REF_' .$infoColumnName]['display'];
  2078.                     } else {
  2079.                         $infoColumns[$i] = $newObject[$infoColumnName];
  2080.                     }
  2081.                 } else {
  2082.                     $infoColumns[$i] = null;
  2083.                 }
  2084.             }
  2085.             $recipients = [];
  2086.             /*
  2087.              * The e-mail field "MailTrigger_Send_To_Address" can have multiple addresses separated by ';'.
  2088.              * Send an e-mail for each e-mail address seperately, since recipients should not see each others e-mail addresses.
  2089.              */
  2090.             if ($affectedMailTrigger['MailTrigger_Send_To_Address']) {
  2091.                 $recipients explode(';'$affectedMailTrigger['MailTrigger_Send_To_Address']);
  2092.             }
  2093.             /*
  2094.              * There is one more recipient field "MailTrigger_Send_To" that lets the user choose a column containing an user-id. For example "MPM_Owner_LINK_User_ID".
  2095.              * Thereby the user does not need to know the e-mail address of this recipient at all.
  2096.              */
  2097.             if ($affectedMailTrigger['REF_MailTrigger_Send_To___ColDefinition_InternalColumnname'] && $newObject[$affectedMailTrigger['REF_MailTrigger_Send_To___ColDefinition_InternalColumnname']]) {
  2098.                 $recipientUserId $newObject[$affectedMailTrigger['REF_MailTrigger_Send_To___ColDefinition_InternalColumnname']];
  2099.                 $recipients[] = $userRepo->find($recipientUserId)['User_Mail'];
  2100.             }
  2101.             $recipients array_unique($recipients);
  2102.             $logMailTriggerRepo $this->repoFactory->createGenericRepository('LogMailTrigger');
  2103.             /*
  2104.              * Send e-mails and write logs to LogMailTrigger whether or not the e-mail was sent sucessfully.
  2105.              */
  2106.             foreach ($recipients as $recipient) {
  2107.                 $success true;
  2108.                 try {
  2109.                     $this->mailNotifier->notify(
  2110.                         [
  2111.                             'template' => $affectedMailTrigger['MailTrigger_LINK_MailTemplate_ID'],
  2112.                             'to' => $recipient,
  2113.                         ],
  2114.                         [
  2115.                             'columnCaption' => $colDefs[$columnName]->getCaption(),
  2116.                             'infoColumns' => $infoColumns,
  2117.                             'oldValue' => $originalObject[$columnName],
  2118.                             'newValue' => $newObject[$columnName]
  2119.                         ]
  2120.                     );
  2121.                 } catch(\Exception $e) {
  2122.                     $success false;
  2123.                 }
  2124.                 $logMailTriggerRepo->persist(
  2125.                     [
  2126.                         'LogMailTrigger_LINK_MailTrigger_ID' => $affectedMailTrigger['MailTrigger_ID'],
  2127.                         'LogMailTrigger_Sent_To' => $recipient,
  2128.                         'LogMailTrigger_Success' => $success
  2129.                     ]
  2130.                 );
  2131.             }
  2132.         }
  2133.     }
  2134.     public function find($id$find_latest_version true$hide_deleted true$join_refs false)
  2135.     {
  2136.         if ($this->options->useVersioning() && $find_latest_version) {
  2137.             return $this->findLatestById($id$hide_deleted$join_refs);
  2138.         }
  2139.         $criteria = new CriteriaComparison(
  2140.             '=',
  2141.             new CriteriaUnmappedColumn($this->tableInfo->getPrimaryKey()),
  2142.             new CriteriaConstant($id)
  2143.         );
  2144.         return $this->findOneBy($criterianull$hide_deleted$find_latest_version$join_refs);
  2145.     }
  2146.     public function findLatestById($id$hide_deleted true$join_refs false)
  2147.     {
  2148.         if (!$this->options->useVersioning()) {
  2149.             return $this->find($idfalse$hide_deleted);
  2150.         }
  2151.         $ancestor $this->getAncestor($id);
  2152.         $criteria = new CriteriaComparison(
  2153.             '=',
  2154.             new CriteriaUnmappedColumn($this->options->getAncestorColumn()),
  2155.             new CriteriaConstant($ancestor)
  2156.         );
  2157.         return $this->findOneBy($criterianull$hide_deletedtrue$join_refs);
  2158.     }
  2159.     protected function getChangedColumns($data$original_object)
  2160.     {
  2161.         // @todo: check
  2162.         $columns array_diff(
  2163.             $this->getWriteableColumns(),
  2164.             $this->options->getAutoColumns()
  2165.         );
  2166.         // Get Original (unmodified) object from database and calculate changed columns
  2167.         $changed_columns = [];
  2168.         foreach ($data as $key => $value) {
  2169.             if (!$this->options->isAutoColumn($key) && in_array($key$columns)) {
  2170.                 // Special handling of Log-Columns, because every non-empty input changes the column
  2171.                 if ($this->options->useLog() && ($this->options->isLogColumn($key))) {
  2172.                     if (!empty($value)) {
  2173.                         $changed_columns[] = $key;
  2174.                     }
  2175.                 } else { // Special handling of Password-Columns, because an empty input does not changes the column
  2176.                     if ($this->options->usePassword() && ($this->options->isPasswordColumn($key))) {
  2177.                         if (!empty($value)) {
  2178.                             $changed_columns[] = $key;
  2179.                         }
  2180.                     } else {
  2181.                         if (!isset($original_object[$key]) || ($original_object[$key] != $value)) {
  2182.                             $changed_columns[] = $key;
  2183.                         }
  2184.                     }
  2185.                 }
  2186.             }
  2187.         }
  2188.         return $changed_columns;
  2189.     }
  2190.     public function getWriteableColumns()
  2191.     {
  2192.         return $this->getTableInfo()
  2193.             ->getWriteableColumns($this->getUserRights());
  2194.     }
  2195.     /**
  2196.      * @param string $id The new objects id
  2197.      *
  2198.      * @return string
  2199.      *
  2200.      * @throws \Doctrine\DBAL\ConnectionException
  2201.      */
  2202.     protected function cloneObject($id$cloneWholeBunch false)
  2203.     {
  2204.         // Existing Columns get cloned, so no data is trashed when there are missing definitions
  2205.         $colDefs $this->tableInfo->getColumnDefinitions();
  2206.         $columns array_filter(
  2207.             array_diff($this->tableInfo->getColumns(), [$this->getPrimaryKey()]),
  2208.             function ($value) use ($colDefs) {
  2209.                 $col = new Column($value'view');
  2210.                 return !$colDefs[$col->getDefinitionName()]->isVirtual();
  2211.             }
  2212.         );
  2213.         $selectColumns array_map(
  2214.             function ($viewColumnName) {
  2215.                 return (new Column($viewColumnName'view'))->getSelectName();
  2216.             },
  2217.             $columns
  2218.         );
  2219.         $builder $this->db->createQueryBuilder()
  2220.             ->select($selectColumns)
  2221.             ->from($this->tableInfo->getInternalTablename(), 'mt');
  2222.         if($cloneWholeBunch) {
  2223.             $builder->where('mt.' $this->options->getReleaseLinkColumn() . ' = ' $builder->createNamedParameter($id));
  2224.         } else {
  2225.             $builder->where('mt.' $this->getPrimaryKey() . ' = ' $builder->createNamedParameter($id));
  2226.         }
  2227.         $sourceObjects $builder->execute()->fetchAll();
  2228.         $clonedIds = [];
  2229.         $this->db->beginTransaction();
  2230.         foreach ($sourceObjects as  $sourceObject)  {
  2231.             unset($sourceObject['doctrine_rownum']); // @todo: MSSQL-fix. Check if this is the best solution
  2232.             try {
  2233.                 $this->db->insert($this->tableInfo->getInternalTablename(), $sourceObject);
  2234.                 $clonedIds[] = $clonedId $this->db->lastInsertId();
  2235.                 // Clone all refs FROM this object
  2236.                 $this->cloneRefs('Refs_SourceTable'$this->tableInfo->getTablename(), 'Refs_Source_LINK_ID'$id$clonedId);
  2237.                 // Clone all refs TO this object
  2238.                 $this->cloneRefs('Refs_TargetTable'$this->tableInfo->getTablename(), 'Refs_Target_LINK_ID'$id$clonedId);
  2239.             } catch (\Exception $ex) {
  2240.                 if($this->db->isTransactionActive()) {
  2241.                     $this->db->rollBack();
  2242.                 }
  2243.                 throw new PortalException(
  2244.                     self::msgErrorClone,
  2245.                     ['%id%' => $id'%table%' => $this->tableInfo->getInternalTablename()],
  2246.                     0,
  2247.                     $ex
  2248.                 );
  2249.             }
  2250.         }
  2251.         $this->db->commit();
  2252.         if ($cloneWholeBunch) {
  2253.             return $clonedIds;
  2254.         } else {
  2255.             return $clonedId;
  2256.         }
  2257.     }
  2258.     private function cloneRefs($tableColumn$tablename$idColumn$id$clonedId)
  2259.     {
  2260.         $refsBuilder $this->db->createQueryBuilder();
  2261.         $refsBuilder
  2262.             ->select('*')
  2263.             ->from($this->prefix 'Refs''')
  2264.             ->where(
  2265.                 $tableColumn ' = ' $refsBuilder->createNamedParameter($tablename)
  2266.             )
  2267.             ->andWhere($idColumn ' = ' $refsBuilder->createNamedParameter($id));
  2268.         $refs $refsBuilder->execute()
  2269.             ->fetchAll();
  2270.         if ($refs) {
  2271.             $refs array_map(
  2272.                 function ($value) use ($clonedId$idColumn) {
  2273.                     unset($value['Refs_ID']);
  2274.                     $value[$idColumn] = $clonedId;
  2275.                     return $value;
  2276.                 },
  2277.                 $refs
  2278.             );
  2279.             foreach ($refs as $ref) {
  2280.                 $this->db->insert($this->prefix 'Refs'$ref);
  2281.             }
  2282.         }
  2283.     }
  2284.     protected function unversionedUpdate(
  2285.         $id,
  2286.         $data,
  2287.         $changed_columns null,
  2288.         $original_object null,
  2289.         $forceSave false,
  2290.         $finalSave true,
  2291.         $sendNotifications true
  2292.     )
  2293.     {
  2294.         if (!isset($original_object)) {
  2295.             $original_object $this->find($idfalsefalsetrue);
  2296.         }
  2297.         if ($this->options->useWriteRecordRights() && !$this->securityContextWrapper->checkRecordRights($original_object[$this->options->getWriteRecordRightsColumn()], 1)) {
  2298.             throw new UnaccessibleObjectException(self::msgMissingWritePermissions);
  2299.         }
  2300.         if (!isset($changed_columns)) {
  2301.             $changed_columns $this->getChangedColumns($data$original_object);
  2302.         }
  2303.         if ((count($changed_columns) > 0) || $forceSave) {
  2304.             // Add special columns to changed columns to ensure correct automated setting of values
  2305.             $this->db->beginTransaction();
  2306.             try {
  2307.                 if ($this->tableInfo->getTablename() === 'UserRights') {
  2308.                     $originalAdminRights  gmp_and(gmp_init($original_object['UserRights_UserRight']), gmp_init(1));
  2309.                     $submittedAdminRights gmp_and(gmp_init($data['UserRights_UserRight']), gmp_init(1));
  2310.                     if(gmp_cmp($submittedAdminRights$originalAdminRights) > && !$this->securityContextWrapper->isSuperAdmin()) {
  2311.                         throw new PortalException(self::msgAdminRightsDenied, [], 0nullnulltrue);
  2312.                     }
  2313.                 }
  2314.                 $changed_columns array_merge($changed_columns$this->options->getAutoColumns());
  2315.                 $queryBuilder $this->db->createQueryBuilder()
  2316.                     ->update($this->tableInfo->getInternalTablename(), '');
  2317.                 $queryBuilder
  2318.                     ->where($this->getPrimaryKey() . " = " $queryBuilder->createNamedParameter($id));
  2319.                 $needPersist $forceSave;
  2320.                 foreach ($changed_columns as $key) {
  2321.                     $value = isset($data[$key])
  2322.                         ?$data[$key]
  2323.                         :null;
  2324.                     $modifiedValue $this->valueOrAutoValue(
  2325.                         $key,
  2326.                         $value,
  2327.                         isset($original_object[$key])
  2328.                             ?$original_object[$key]
  2329.                             :null,
  2330.                         null,
  2331.                         $data
  2332.                     );
  2333.                     $colDefs $this->getColumnDefinitions();
  2334.                     if ($colDefs[$key]->getType() == 'html') {
  2335.                         $htmlPurifierConfig \HTMLPurifier_Config::createDefault();
  2336.                         $htmlPurifierConfig->set('Attr.AllowedFrameTargets', ['_blank''_top''_self''_parent']);
  2337.                         $purifier = new \HTMLPurifier($htmlPurifierConfig);
  2338.                         $modifiedValue['value'] = $purifier->purify($modifiedValue['value']);
  2339.                     }
  2340.                     if ($colDefs[$key]->isVirtual()) {
  2341.                         $this->updateRefs($id$key$modifiedValue);
  2342.                     } else {
  2343.                         // Standard update
  2344.                         $queryBuilder->set($key$modifiedValue['expr']);
  2345.                         $queryBuilder->setParameter($modifiedValue['param'], $modifiedValue['value']);
  2346.                         $needPersist true;
  2347.                     }
  2348.                 }
  2349.                 if ($needPersist) {
  2350.                     $queryBuilder->execute();
  2351.                     /*
  2352.                      *  Consider encryption if applicable
  2353.                      */
  2354.                     if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
  2355.                         $dsgvo $this->container->get('projectbiz.dsgvo');
  2356.                         if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
  2357.                             $dsgvo->encryptRecord($id$this->tableInfo->getTablename(), true$this->options->useVersioning());
  2358.                         }
  2359.                     }
  2360.                     if ($finalSave) {
  2361.                         $new_object $this->find($idfalsefalsetrue);
  2362.                         $this->logChangesOfUserTable($original_object$new_object);
  2363.                         $context = [
  2364.                             'id' => $id,
  2365.                             'object' => $new_object,
  2366.                             'prev_object' => $original_object,
  2367.                             'database' => $this->db
  2368.                         ];
  2369.                         $this->dispatch(new RepositoryEvent($this$context), RepositoryEvents::UPDATE_AFTER_EXECUTE);
  2370.                     }
  2371.                 }
  2372.             } catch (\Exception $ex) {
  2373.                 $this->db->rollBack();
  2374.                 throw($ex);
  2375.             }
  2376.             $this->db->commit();
  2377.             if ($sendNotifications) {
  2378.                 $this->sendNotifications($changed_columns$original_object$new_object);
  2379.             }
  2380.         }
  2381.         return $id;
  2382.     }
  2383.     /**
  2384.      * Log the changes that have been made to the »User«-table into table »UserLog«.
  2385.      *
  2386.      * @param array $original_object
  2387.      * @param array $new_object
  2388.      */
  2389.     protected function logChangesOfUserTable($original_object$new_object)
  2390.     {
  2391.         if ($this->tableInfo->getTablename() != 'User') {
  2392.             return;
  2393.         }
  2394.         $changedValues false;
  2395.         /*
  2396.          * Do not log changes within the following columns.
  2397.          */
  2398.         $blacklist = ['User_Last_Change''User_Failure_Count''User_SessionID''User_Logged_In''User_Log''User_Previous_login'];
  2399.         foreach ($new_object as $key => $value) {
  2400.             if ($new_object[$key] != $original_object[$key] && !in_array($key$blacklist)) {
  2401.                 $changedValues .= $key ': »' print_r($original_object[$key], true) . '« => »' print_r($new_object[$key], true) . '«' PHP_EOL;
  2402.             }
  2403.         }
  2404.         if ($changedValues) {
  2405.             $this->db->insert(
  2406.                 $this->prefix 'UserLog',
  2407.                 [
  2408.                     'UserLog_LINK_User_ID'          => $this->securityContextWrapper->getUserId(),
  2409.                     'UserLog_Affected_LINK_User_ID' => $original_object['User_ID'],
  2410.                     'UserLog_initialized_Date'      => (new \DateTime())->format($this->db->getDatabasePlatform()->getDateTimeFormatString()),
  2411.                     'UserLog_Changes'               => $changedValues,
  2412.                 ]
  2413.             );
  2414.         }
  2415.     }
  2416.     public function archive($id)
  2417.     {
  2418.         $ancestor 0;
  2419.         $tablename $this->tableInfo->getInternalTablename();
  2420.         if (!$this->canAccessObject($id)) {
  2421.             throw new PortalException(self::msgNoAccess);
  2422.         }
  2423.         if ($this->options->useVersioning()) {
  2424.             $ancestorColumn $this->options->getAncestorColumn();
  2425.             $refObject $this->db->createQueryBuilder()
  2426.                 ->select([$ancestorColumn])
  2427.                 ->from($tablename'mt')
  2428.                 ->where('mt.' $this->getPrimaryKey() . ' = :id')
  2429.                 ->setParameter('id'$id)
  2430.                 ->setMaxResults(1)
  2431.                 ->execute()
  2432.                 ->fetch();
  2433.             if ($refObject !== false) {
  2434.                 $ancestor $refObject[$ancestorColumn];
  2435.             } else {
  2436.                 throw new GenericRepositoryArchiveException(1901);
  2437.             }
  2438.         }
  2439.         if ($this->options->useArchived()) {
  2440.             if ($this->isWriteable($this->options->getArchivedColumn())) {
  2441.                 $obj = [
  2442.                     $this->options->getArchivedColumn() => 1,
  2443.                     $this->getPrimaryKey() => $id
  2444.                 ];
  2445.                 try {
  2446.                     $this->persist($objtrue);
  2447.                 } catch (Exception $ex) {
  2448.                     throw new GenericRepositoryArchiveException(1902);
  2449.                 }
  2450.             } else {
  2451.                 throw new GenericRepositoryArchiveException(1910);
  2452.             }
  2453.         }
  2454.     }
  2455.     public function duplicate($id$data null)
  2456.     {
  2457.         $colDefs $this->getColumnDefinitions();
  2458.         $criteria = new CriteriaComparison(
  2459.             '=',
  2460.             new CriteriaUnmappedColumn($this->getPrimaryKey()),
  2461.             new CriteriaConstant($id)
  2462.         );
  2463.         $object $this->findOneBy($criteria);
  2464.         if (null !== $data) {
  2465.             foreach (array_keys($object) as $key) {
  2466.                 if (array_key_exists($key$data)) {
  2467.                     $object[$key] = $data[$key];
  2468.                 }
  2469.             }
  2470.         }
  2471.         foreach (array_keys($object) as $column) {
  2472.             /*
  2473.              * If »ColDefinition_ExcludeFromCloning« is true for a column and the user did not change the columns value,
  2474.              * overwrite the data with the default value (ColDefinition_StandardValue).
  2475.              * This prevents the duplication of some values, e.g. unique project numbers.
  2476.              */
  2477.             if (isset($colDefs[$column]) && $colDefs[$column]->getExcludeFromCloning() && (!isset($data[$column]) || $data[$column] === $object[$column])) {
  2478.                 $object[$column] = $colDefs[$column]->getDefault();
  2479.             }
  2480.         }
  2481.         if ($object !== false) {
  2482.             unset($object[$this->getPrimaryKey()]);
  2483.             if ($this->getOptions()
  2484.                 ->useVersioning()
  2485.             ) {
  2486.                 unset($object[$this->getOptions()
  2487.                         ->getVersionColumn()]);
  2488.             }
  2489.             return $this->create($object);
  2490.         }
  2491.         return null;
  2492.     }
  2493.     /**
  2494.      * Restores a version by getting the specified id and saving it as a new version.
  2495.      *
  2496.      * @param int $id primary key id of the version to restore.
  2497.      * @throws Exception
  2498.      */
  2499.     public function restoreVersion($id)
  2500.     {
  2501.         if (!$this->getOptions()->useVersioning()) {
  2502.             throw new \Exception('Cannot restore a version in an unversioned table.');
  2503.         }
  2504.         $targetVersion $this->find($idfalsefalsetrue);
  2505.         unset($targetVersion[$this->getPrimaryKey()]);
  2506.         unset($targetVersion[$this->options->getVersionColumn()]);
  2507.         $this->versionedUpdate($id$targetVersionfalsetrue);
  2508.     }
  2509.     /**
  2510.      * @param array $columns The columns that are required to be readable
  2511.      *
  2512.      * @return GenericRepository
  2513.      *
  2514.      * @throws RequiredColumnNotReadableException
  2515.      */
  2516.     public function requireReadableColumns(array $columns)
  2517.     {
  2518.         foreach ($columns as $column) {
  2519.             if (!$this->isReadable($column)) {
  2520.                 throw new RequiredColumnNotReadableException($column);
  2521.             }
  2522.         }
  2523.         return $this;
  2524.     }
  2525.     public function isReadable($column)
  2526.     {
  2527.         return $this->getTableInfo()
  2528.             ->isReadable($column$this->getUserRights());
  2529.     }
  2530.     /**
  2531.      * @param array $columns The columns that are required to be writeable
  2532.      *
  2533.      * @return GenericRepository
  2534.      *
  2535.      * @throws RequiredColumnNotWriteableException
  2536.      */
  2537.     public function requireWriteableColumns(array $columns)
  2538.     {
  2539.         foreach ($columns as $column) {
  2540.             if (!$this->isReadable($column)) {
  2541.                 throw new RequiredColumnNotWriteableException($column);
  2542.             }
  2543.         }
  2544.         return $this;
  2545.     }
  2546.     /**
  2547.      * @param array $view
  2548.      * @param bool $with_required
  2549.      *
  2550.      * @return array
  2551.      */
  2552.     public function getReadableColumnsInView($view null$with_required true)
  2553.     {
  2554.         $viewColumnsOrAll $this->getColumnsInView($view);
  2555.         $viewAndRequired $viewColumnsOrAll;
  2556.         if ($with_required) {
  2557.             $requiredColumns $this->getRequiredColumns();
  2558.             foreach ($requiredColumns as $column) {
  2559.                 $viewAndRequired[] = $column;
  2560.             }
  2561.             $viewAndRequired[] = $this->getPrimaryKey();
  2562.         }
  2563.         $readableColumns $this->getReadableColumns();
  2564.         return array_unique(array_intersect($viewAndRequired$readableColumns));
  2565.     }
  2566.     public function getColumnsInView($view null)
  2567.     {
  2568.         if (null === $view) {
  2569.             return $this->tableInfo->getColumns();
  2570.         }
  2571.         return array_intersect($view$this->tableInfo->getColumns());
  2572.     }
  2573.     public function getReadableColumns()
  2574.     {
  2575.         return $this->getTableInfo()
  2576.             ->getReadableColumns($this->getUserRights());
  2577.     }
  2578.     public function defaultObject()
  2579.     {
  2580.         $result = [];
  2581.         $columnDefinitions $this->getColumnDefinitions();
  2582.         $readableColumns $this->getReadableColumns();
  2583.         foreach ($readableColumns as $column) {
  2584.             $col = new Column($column'view');
  2585.             $colDef $columnDefinitions[$col->getDefinitionName()];
  2586.             $defaultValue $colDef->getDefault();
  2587.             if ($defaultValue !== null) {
  2588.                 $result[$column] = $defaultValue;
  2589.             }
  2590.         }
  2591.         unset($result[$this->getPrimaryKey()]);
  2592.         return $result;
  2593.     }
  2594.     public function isSystemColumn($column)
  2595.     {
  2596.         return $this->options->isSystemColumn($column);
  2597.     }
  2598.     public function isAutoColumn($column)
  2599.     {
  2600.         return $this->options->isAutoColumn($column);
  2601.     }
  2602.     /**
  2603.      * Return type for special columns
  2604.      *
  2605.      * Returns 'hidden' for primary key columns and 'log' for log-columns.
  2606.      *
  2607.      * @todo    Try to get rid of this function. It is called in RepositoryBlock, TableViewController,
  2608.      *          TableDataViewController and GenericTableType. Try to replace it with something better
  2609.      *
  2610.      * @deprecated
  2611.      *
  2612.      * @param string $columnName
  2613.      *
  2614.      * @return null|string
  2615.      */
  2616.     public function getSpecialColumnType($columnName)
  2617.     {
  2618.         if ($this->getPrimaryKey() == $columnName) {
  2619.             return 'hidden';
  2620.         }
  2621.         if ($this->options->useLog()) {
  2622.             if (in_array($columnName$this->options->getLogColumns())) {
  2623.                 return 'log';
  2624.             }
  2625.         }
  2626.         return null;
  2627.     }
  2628.     public function getLabelForPropertyPath($propertyPath) {
  2629.         return $this->getLabelForPropertyPathWithInfo(explode(':'$propertyPath), $this->tableInfo);
  2630.     }
  2631.     private function getLabelForPropertyPathWithInfo($propertyPath$info) {
  2632.         if (count($propertyPath) == 1) {
  2633.             return $this->getLabelForProperty($propertyPath[0], $info);
  2634.         }
  2635.         else if (count($propertyPath) > 1) {
  2636.             $part0 array_shift($propertyPath);
  2637.             $column = new Column($part0'view');
  2638.             $sourceInfo $this->tableInfo->getSourceTableInfo($column->getDefinitionName());
  2639.             return $this->getLabelForProperty($part0$info) . ': ' $this->getLabelForPropertyPathWithInfo($propertyPath$sourceInfo);
  2640.         }
  2641.     }
  2642.     public function isVirtualForPropertyPath($propertyPath) {
  2643.         return $this->isVirtualForPropertyPathWithInfo(explode(':'$propertyPath), $this->tableInfo);
  2644.     }
  2645.     private function isVirtualForPropertyPathWithInfo($propertyPathTableInfoInterface $info) {
  2646.         if (count($propertyPath) == 1) {
  2647.             $colDefs $info->getColumnDefinitions();
  2648.             $column = new Column($propertyPath[0], 'view');
  2649.             $defName $column->getDefinitionName();
  2650.             if (!array_key_exists($defName$colDefs)) {
  2651.                 throw new \Exception('Missing Column Definition: ' $defName);
  2652.             }
  2653.             return $colDefs[$defName]->isVirtual();
  2654.         }
  2655.         else if (count($propertyPath) > 1) {
  2656.             $part0 array_shift($propertyPath);
  2657.             $column = new Column($part0'view');
  2658.             $sourceInfo $this->tableInfo->getSourceTableInfo($column->getDefinitionName());
  2659.             return $this->isVirtualForPropertyPathWithInfo($propertyPath$sourceInfo);
  2660.         }
  2661.     }
  2662.     private function getLabelForProperty($property$info) {
  2663.         return $info->getLabelForViewColumn($property);
  2664.     }
  2665. }