<?php
namespace ProjectBiz\DatabaseBundle\Database;
use DateTime;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Query\QueryBuilder;
use Exception;
use ProjectBiz\UserBundle\Service\SecurityContextWrapper;
use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaBuilderInterface;
use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaComparison;
use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaComposite;
use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaConstant;
use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaHasRights;
use ProjectBiz\DatabaseBundle\Database\Criteria\CriteriaUnmappedColumn;
use ProjectBiz\DatabaseBundle\Database\Platform\MySqlPlatform;
use ProjectBiz\DatabaseBundle\Database\Platform\SqlServerPlatform;
use ProjectBiz\DatabaseBundle\Exceptions\GenericRepositoryDeleteException;
use ProjectBiz\DatabaseBundle\Exceptions\RequiredColumnNotReadableException;
use ProjectBiz\DatabaseBundle\Exceptions\RequiredColumnNotWriteableException;
use ProjectBiz\PortalBundle\Exceptions\UnaccessibleObjectException;
use ProjectBiz\MailerBundle\Notification\MailNotifier;
use ProjectBiz\PortalBundle\Exceptions\PortalException;
use ProjectBiz\PortalBundle\Portal\Messages;
use ProjectBiz\DatabaseBundle\Database\GenericRepositoryFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use ProjectBiz\PortalBundle\Service\StandardLogHeader;
class GenericRepository extends EventDispatcher
{
const msgUnsupportedPlatform = 'Die Datenbank-Plattform "%platform%" wird nicht unterstützt.';
const msgAncestorNotFound = 'Urahn konnte nicht gefunden werden.';
const msgLockingColumnsNotWriteable = 'Die Spalten zum Sperren des Objekts dürfen nicht geschrieben werden.';
const msgErrorClone = 'Das Klonen des Objekts %id% in der Tabelle "%table%" ist fehlgeschlagen.';
const msgNoAccess = 'Auf das Objekt konnte nicht zugegriffen werden.';
const msgUnreadableRequired = '%column% wird benötigt, ist aber nicht lesbar.';
const msgMissingColDefs = 'Fehlende Spaltendefinitionen';
const msgMissingColDefFor = 'Fehlende Spaltendefinition für %column%';
const msgMissingParentReleaseParameter = 'Eigenschaft parent_release_parameter fehlt.';
const msgAdminRightsDenied = 'Die Zuweisung von ausgewählten Rechten ist nicht erlaubt.';
const msgMissingWritePermissions = 'Sie haben nicht die erforderliche Berechtigung um diesen Datensatz zu speichern.';
private static $parameterIndex = 1;
private $db;
private $user;
private $securityContextWrapper;
private $tableInfo;
private $options;
private $prefix;
private $tableHelper;
private $logger;
private $platform;
private $dateTimeFactory;
private $mailNotifier;
private $repoFactory;
private $dateFormat;
private $dateTimeFormat;
private $schemaCache;
private $request;
private $container;
/**
* Construct.
*
* @param Connection $db
* @param TableInfoInterface $tableInfo
* @param GenericRepositoryOptions $options
* @param string $prefix
* @param TableHelper $tableHelper
* @param LoggerInterface $logger
*
* @throws PortalException
*/
public function __construct(
Connection $db,
SecurityContextWrapper $securityContextWrapper,
TableInfoInterface $tableInfo,
GenericRepositoryOptions $options,
string $prefix,
TableHelper $tableHelper,
LoggerInterface $logger,
DateTimeFactory $dateTimeFactory,
MailNotifier $mailNotifier,
GenericRepositoryFactory $repoFactory,
$dateFormat,
$dateTimeFormat,
DatabaseSchemaCache $schemaCache,
RequestStack $requestStack,
ContainerInterface $container
)
{
$this->db = $db;
$this->securityContextWrapper = $securityContextWrapper;
$this->user = $securityContextWrapper->getUser();
$this->tableInfo = $tableInfo;
$this->options = $options;
$this->prefix = $prefix;
$this->tableHelper = $tableHelper;
$this->logger = $logger;
$this->dateTimeFactory = $dateTimeFactory;
$this->mailNotifier = $mailNotifier;
$this->repoFactory = $repoFactory;
$this->dateFormat = $dateFormat;
$this->dateTimeFormat = $dateTimeFormat;
$this->schemaCache = $schemaCache;
$this->request = $requestStack->getCurrentRequest();
$this->container = $container;
}
/**
* @return Connection The database connection used by this repository
*/
public function getConnection()
{
return $this->db;
}
public function findAll(
array $sort = null,
array $view = null,
$hide_deleted = true,
$group_by_version = true,
$join_refs = true,
$limit = null,
$offset = null
)
{
$processedView = $this->processView($view);
$builder = $this->findByQuery(
$columnMap,
null,
$sort,
$processedView,
$limit,
$offset,
$hide_deleted,
$group_by_version,
$join_refs
);
$select_columns = $this->buildSelectColumns($columnMap, $processedView);
if (count($select_columns) > 0) {
return $builder->select($select_columns)
->execute();
}
return null;
}
public function findSecondaryBy(
CriteriaBuilderInterface $filter,
array $sort = null,
array $view = null,
$limit = null,
$offset = null,
$hide_deleted = true,
$group_by_version = true,
$join_refs = true
)
{
/* @todo: this method should not be in this class; it currently is because of the tight coupling
* with the column lookup.
*/
$processedView = $this->processView($view);
$builder = $this->findByQuery(
$columnMap,
$filter,
$sort,
$processedView,
$limit,
$offset,
$hide_deleted,
$group_by_version,
$join_refs
);
$builder->resetQueryPart('orderBy'); // MSSQL doesn't like COUNT() with ORDER BY
$select_columns = $this->buildSecondarySelectColumns($processedView);
if (count($select_columns) > 0) {
return $builder->select($select_columns)
->execute();
}
return null;
}
private function processView($view)
{
$unprocessedView = $this->useAllOrGiven($view);
$unprocessedView = $this->addRequiredColumnsAndPrimaryKey($unprocessedView);
$processedView = [];
foreach ($unprocessedView as $column) {
$splitColumn = explode(':', $column);
if ($this->getTableInfo()
->isReadable($splitColumn[0], $this->getUserRights())
) {
if (!array_key_exists($splitColumn[0], $processedView)) {
$processedView[$splitColumn[0]] = [];
}
if (count($splitColumn) === 2) {
$processedView[$splitColumn[0]][] = $splitColumn[1];
} else {
if (count($splitColumn) > 2) {
// @todo: create meaningful Exception
throw new Exception();
}
}
} else {
if ($this->isRequired($splitColumn[0])) {
throw new PortalException(
self::msgUnreadableRequired,
['%column%' => $splitColumn[0]]
);
}
}
}
return $processedView;
}
private function useAllOrGiven($view)
{
if ((null === $view) || !is_array($view)) {
return $this->tableInfo->getColumns();
}
return $view;
}
private function addRequiredColumnsAndPrimaryKey($view)
{
if ((null === $view) || !is_array($view)) {
$view = [];
}
$requiredColumns = $this->getRequiredColumns();
foreach ($requiredColumns as $column) {
$view[] = $column;
}
$view[] = $this->getPrimaryKey();
return array_unique($view);
}
private function getRequiredColumns()
{
$requiredColumns = $this->getOptions()
->getOption('required_columns');
if (null !== $requiredColumns) {
return $requiredColumns;
}
return [];
}
/**
* @return GenericRepositoryOptions
*
* Mainly used (in 3 locations to get the system-columns)
*/
public function getOptions()
{
return $this->options;
}
/**
* Get the primary key for the repository
*
* @return string The primary key for the repository
*
*/
public function getPrimaryKey()
{
return $this->getOptions()
->getOption('primary_key');
}
public function getTableInfo()
{
return $this->tableInfo;
}
/**
* @return string
*/
private function getUserRights()
{
return $this->securityContextWrapper->getUserRights();
}
private function isRequired($column)
{
$requiredColumns = $this->getOptions()
->getOption('required_columns');
if (!is_array($requiredColumns)) {
return false;
}
return in_array($column, $requiredColumns);
}
/**
* @param $columnMap
* @param CriteriaBuilderInterface|null $filter
* @param array|null $sort
* @param null $processedView
* @param null $limit
* @param null $offset
* @param bool|true $hide_deleted
* @param bool|true $group_by_version
* @param bool|true $join_refs
* @param bool|true $hide_archived
* @param string $mt_name
* @return QueryBuilder
* @throws Exception
*/
public function findByQuery(
&$columnMap,
CriteriaBuilderInterface $filter = null,
array $sort = null,
$processedView = null,
$limit = null,
$offset = null,
$hide_deleted = true,
$group_by_version = true,
$join_refs = true,
$hide_archived = true,
$mt_name = 'mt'
)
{
if (null === $processedView) {
$processedView = $this->processView(null);
}
$tablename = $this->tableInfo->getInternalTablename();
$builder = $this->db->createQueryBuilder();
$this->tableInfo->buildFrom($builder, $mt_name);
$context = [
'builder' => $builder,
'where_used' => false
];
if (null !== $limit) {
$builder->setMaxResults($limit);
}
if (null !== $offset) {
$builder->setFirstResult($offset);
}
$columnMap = $this->buildColumnMapForJoins($builder, $processedView, $join_refs, $mt_name);
/*
* Users can only see a document if they have the rights to see the _type_ of the document.
*/
if ($tablename == $this->prefix . 'Document' && !$this->getUser()->isAdmin()) {
$builder
->innerJoin(
$mt_name,
$this->prefix . 'DocumentType',
'doctype',
'('.$mt_name.'.Document_Link_DocumentType_ID = doctype.DocumentType_ID'
. ' and (doctype.DocumentType_PermissionRead & ' . $this->getUserRights() . ') > 0)'
);
}
if ($group_by_version && $this->options->useVersioning()) {
$ancestorColumn = $this->options->getAncestorColumn();
$versionColumn = $this->options->getVersionColumn();
$builder
->leftJoin(
$mt_name,
$tablename,
't2',
'(' . $mt_name . '.'
. $ancestorColumn
. " = t2."
. $ancestorColumn
. ' AND ' . $mt_name . '.'
. $versionColumn
. ' < t2.'
. $versionColumn
. ')'
)
->where('t2.' . $this->getPrimaryKey() . ' IS NULL');
$context['where_used'] = true;
if ($hide_deleted && $this->options->useDeleted()) {
$builder->andWhere($mt_name . '.' . $this->options->getDeletedColumn() . " <> 1");
}
if ($hide_archived && $this->options->useArchived()) {
$builder->andWhere($mt_name . '.' . $this->options->getArchivedColumn() . " <> 1");
}
/*
Add filter and sorting
*/
if (null !== $filter) {
$criteria = $filter->buildCriteria($builder, $columnMap, $processedView, $mt_name);
if ($criteria) {
$builder->andWhere($criteria);
}
}
} else {
$useAndWhere = false;
if ($hide_deleted && $this->options->useDeleted()) {
$useAndWhere = true;
$context['where_used'] = true;
$builder->where($mt_name . '.' . $this->options->getDeletedColumn() . " <> 1");
}
if ($hide_archived && $this->options->useArchived()) {
$useAndWhere = true;
$context['where_used'] = true;
$builder->andWhere($mt_name . '.' . $this->options->getArchivedColumn() . " <> 1");
}
/*
Add filter and sorting
*/
if (null !== $filter) {
$criteria = $filter->buildCriteria($builder, $columnMap, $processedView, $mt_name);
if ($useAndWhere) {
if ($criteria) {
$builder->andWhere($criteria);
}
} else {
if ($criteria) {
$builder->where($criteria);
$context['where_used'] = true;
}
}
}
}
/*
* Consider the record rights.
*
* A user can see a record if he is the owner or the role matches.
*/
$user = $this->getUser();
if (isset($user)) {
if (($this->options->useReadRecordRights() || $this->options->useOwner()) && !$this->getUser()->isAdmin()) {
$rightsCriterias = [];
if ($this->options->useReadRecordRights()) {
$readRecordRightsCriteria = new CriteriaHasRights($this->options->getReadRecordRightsColumn(), $this->getUser()->getRights()->getReadRecordRights());
$user= $this->getUser()->getRights()->getReadRecordRights();
$rightsCriterias[] = $readRecordRightsCriteria->buildCriteria($builder, $columnMap, $processedView);
}
if ($this->options->useOwner()) {
$ownerCriteria = new CriteriaComparison('=', new CriteriaUnmappedColumn($this->options->getOwnerColumn()), new CriteriaConstant($this->getUser()->getId()));
$rightsCriterias[] = $ownerCriteria->buildCriteria($builder, $columnMap, $processedView);
}
$rightsCriteriaExpression = call_user_func_array([$builder->expr(), 'orX'], $rightsCriterias);
if ($context['where_used']) {
$builder->andWhere($rightsCriteriaExpression);
} else {
$builder->where($rightsCriteriaExpression);
$context['where_used'] = true;
}
}
}
$defaultSorting = $this->options->getOption('default_sorting');
$this->applySorting($builder, $sort, $defaultSorting, $columnMap, $processedView);
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::FIND_BEFORE_EXECUTE);
return $builder;
}
private function buildColumnMapForJoins(QueryBuilder $builder, $processedView, $join_refs = true, $mt_name = 'mt')
{
$columnMap = [];
if ($join_refs) {
$columnDefinitions = $this->getColumnDefinitions();
$viewKeys = array_keys($processedView);
foreach ($viewKeys as $column) {
$col = new Column($column, 'view', $mt_name);
$selectName = $col->getSelectName();
$defName = $col->getDefinitionName();
if (isset($columnDefinitions[$defName])) {
$colDef = $columnDefinitions[$defName];
$source = $colDef->getSource();
if (null !== $source) {
/*
* @todo: Include joins of Syn-Tables!
*/
$tname = self::getNextParameterName();
$sourceTableInfo = $this->tableHelper->getTableInfo($source['table']);
$sourceView = (null === $source['view'])
?[]
:$source['view'];
$columnMap[$column] = [
'table' => $tname,
'tablename' => $sourceTableInfo->getTablename(),
'display' => $source['display_column'],
'view' => array_unique(array_merge($sourceView, $processedView[$column]))
];
if ($colDef->isVirtual()) {
$this->getPlatform()
->refsBuildColumnMapForJoins(
$colDef->getType(),
$builder,
$this->tableInfo->getTablename(),
$this->tableInfo->getPrimaryKey(),
$source['table'],
$column,
$tname,
$colDef->getReverseProperty()
);
} else {
$condition = sprintf(
"%s = %s.%s",
$selectName,
$tname,
$source['foreign_key']
);
$builder->leftJoin(
$mt_name,
$this->prefix . $sourceTableInfo->getTablename(),
$tname,
$condition
);
}
}
}
}
}
return $columnMap;
}
/**
* @return \ProjectBiz\DatabaseBundle\Entity\ColumnDefinition[]
*/
public function getColumnDefinitions()
{
return $this->getTableInfo()
->getColumnDefinitions();
}
public static function getNextParameterName()
{
return 'p' . (++self::$parameterIndex);
}
public function getPlatform()
{
if (!isset($this->platform)) {
$platform = $this->db->getDatabasePlatform();
if (is_a($platform, 'Doctrine\DBAL\Platforms\MySqlPlatform')) {
$this->platform = new MySqlPlatform($this->prefix);
} else {
if (is_a($platform, 'Doctrine\DBAL\Platforms\SqlServerPlatform')) {
$this->platform = new SqlServerPlatform($this->prefix);
} else {
throw new PortalException(
self::msgUnsupportedPlatform,
['%platform%' => $platform->getName()]
);
}
}
}
return $this->platform;
}
public function applySorting(
QueryBuilder $builder,
array $sort = null,
array $defaultSorting = null,
array $columnMap = null,
$processedView = null
)
{
if ((null === $sort) && (null === $defaultSorting) && !$this->getOptions()
->useManSort()
) {
return;
}
$totalSort = array_merge(
$sort
? $sort
: [],
$this->getOptions()->useManSort()
? [
[
'column' => $this->getOptions()->getManSortColumn(),
'mapped' => false
]
]
: [],
$defaultSorting
? $defaultSorting
: []
);
$usedColumnNames = [];
foreach ($totalSort as $sortEntry) {
$columnName = $sortEntry['column'];
/*
* Make sure there are no duplicated column names for orderby clause.
* Especially sorting by column "_manSort" leads to sorting duplication.
*/
{
if (in_array($columnName, $usedColumnNames)) {
continue;
}
$usedColumnNames[] = $columnName;
}
$direction = isset($sortEntry['direction'])
?$sortEntry['direction']
:'ASC';
$mapped = isset($sortEntry['mapped'])
?$sortEntry['mapped']
:true;
if ($this->isVirtualForPropertyPath($columnName)) {
return;
}
$parts = explode(':', $columnName);
$numParts = count($parts);
switch ($numParts) {
case 1:
$column = new Column($parts[0], 'view');
$mappedColumn = $column->getSelectName();
if ((null !== $columnMap) && ($mapped)) {
$mappedColumn = isset($columnMap[$parts[0]])
?($columnMap[$parts[0]]['table'] . '.' . $columnMap[$parts[0]]['display'])
:($column->getSelectName());
}
$builder->addOrderBy($mappedColumn, $direction);
break;
case 2:
$mappedColumn = $columnMap[$parts[0]]['table'] . '.' . $parts[1];
$builder->addOrderBy($mappedColumn, $direction);
break;
}
}
}
protected function buildSecondarySelectColumns($processedView)
{
$columnDefinitions = $this->getColumnDefinitions();
$selectColumns = [];
foreach ($processedView as $column => $sourceView) {
$col = new Column($column, 'view');
$selectName = $col->getSelectName();
$defName = $col->getDefinitionName();
$colDef = $columnDefinitions[$defName];
$secHeader = $colDef->getSecondaryHeader();
if (!empty($secHeader)) {
$expr = '';
switch ($secHeader) {
case 'sum':
$expr = 'SUM(' . $selectName . ')';
break;
case 'avg':
$expr = 'AVG(' . $selectName . ')';
break;
case 'min':
$expr = 'MIN(' . $selectName . ')';
break;
case 'max':
$expr = 'MAX(' . $selectName . ')';
break;
case 'count':
$expr = 'Count(' . $selectName . ')';
break;
}
$selectColumns[] = $expr . ' AS ' . $column;
}
}
return $selectColumns;
}
public function countAll(
array $sort = null,
array $view = null,
$hide_deleted = true,
$group_by_version = true,
$join_refs = true
)
{
$processedView = $this->processView($view);
$builder = $this->findByQuery(
$columnMap,
null,
$sort,
$processedView,
null,
null,
$hide_deleted,
$group_by_version,
$join_refs
);
$select_columns = $this->buildSelectColumns($columnMap, $processedView);
if (count($select_columns) > 0) {
return $builder->select('COUNT(*) AS COUNT')
->execute();
}
return 0;
}
/**
* Get the result of a specific column by sql aggregate function.
*
* @param type $column
* @return type
*/
public function aggregate($column, $table, $aggregateFunction = 'max') {
// @todo: support multiple columns at once to improve performance
$result = $this->db->createQueryBuilder()
->select($aggregateFunction . '(' . $column . ') as aggregate_result')
->from($this->prefix . $table)
->execute()->fetchAll();
if(count($result)>0) {
return $result[0]['aggregate_result'];
}
// if the result is empty, there are no records in the table.
return null;
}
protected function buildSelectColumns(array $columnMap, $processedView)
{
$selectColumns = [];
foreach ($processedView as $viewName => $sourceView) {
$col = new Column($viewName, 'view');
$selectName = $col->getSelectName();
$viewRefName = $col->getViewRefName();
$defName = $col->getDefinitionName();
if (isset($columnMap[$viewName])) {
if ($this->getColumnDefinitions()[$defName]->isVirtual()) {
$selectColumns[] = $this->getPlatform()
->refsBuildSelectColumnsData(
$this->getColumnDefinitions()[$defName]->getType(),
$this->tableInfo->getTablename(),
$this->tableInfo->getPrimaryKey(),
$columnMap[$viewName]['tablename'],
$viewName,
$columnMap[$viewName]['table'],
$this->getColumnDefinitions()[$defName]->getReverseProperty()
);
$selectColumns[] = $this->getPlatform()
->refsBuildSelectColumnsDisplay(
$this->getColumnDefinitions()[$defName]->getType(),
$this->tableInfo->getTablename(),
$this->tableInfo->getPrimaryKey(),
$columnMap[$viewName]['tablename'],
$viewName,
$columnMap[$viewName]['table'],
$this->getColumnDefinitions()[$defName]->getReverseProperty()
);
} else {
// 1. Select original column as 'itself'
$selectColumns[] = $selectName . ' AS ' . $viewName;
// 2. Select view-columns
foreach ($columnMap[$viewName]['view'] as $viewColumn) {
$refTableInfo = $this->tableHelper->getTableInfo($columnMap[$viewName]['tablename']);
$refColumn = new Column($viewColumn, 'view');
$refDefName = $refColumn->getDefinitionName();
if (!$refTableInfo->getColumnDefinitions()[$refDefName]->isVirtual()) {
$selectColumns[] = $columnMap[$viewName]['table']
. '.'
. $viewColumn
. ' AS '
. $viewRefName
. '___'
. $viewColumn;
}
else {
$selectColumns[] = '\'nv\' AS '
. $viewRefName
. '___'
. $viewColumn;
}
}
// 3. Select display-column
$selectColumns[] = $columnMap[$viewName]['table']
. '.'
. $columnMap[$viewName]['display']
. ' AS '
. $viewRefName
. '___display';
}
} else {
if (!$this->getColumnDefinitions()[$defName]->isVirtual()) {
$selectColumns[] = $selectName . ' AS ' . $viewName;
}
}
}
return $selectColumns;
}
public function countHistoryBy(
$id,
CriteriaBuilderInterface $filter = null,
array $view = null
)
{
if (!$this->options->useVersioning()) {
return false;
}
$processedView = $this->processView($view);
$builder = $this->historyByQuery(
$id,
$columnMap,
$filter,
$processedView,
null,
null
);
$builder->resetQueryPart('orderBy'); // MSSQL doesn't like COUNT() with ORDER BY
$select_columns = $this->buildSelectColumns($columnMap, $processedView);
if (count($select_columns) > 0) {
return $builder->select('COUNT(*) AS COUNT')
->execute();
}
return false;
}
public function countBy(
CriteriaBuilderInterface $filter = null,
array $view = null,
$limit = null,
$offset = null,
$hide_deleted = true,
$group_by_version = true,
$join_refs = true
)
{
$processedView = $this->processView($view);
$builder = $this->findByQuery(
$columnMap,
$filter,
null,
$processedView,
$limit,
$offset,
$hide_deleted,
$group_by_version,
$join_refs
);
$builder->resetQueryPart('orderBy'); // MSSQL doesn't like COUNT() with ORDER BY
$select_columns = $this->buildSelectColumns($columnMap, $processedView);
if (count($select_columns) > 0) {
return $builder->select('COUNT(*) AS COUNT')
->execute();
}
return false;
}
public function fetchAll(Result $result = null)
{
if (null === $result) {
return null;
}
$rows = $result->fetchAll();
$ret = [];
foreach ($rows as $row) {
$ret[] = $this->handleRow($row);
}
return $ret;
}
protected function handleRow($row)
{
$columnDefinitions = $this->getColumnDefinitions();
$ret = [];
foreach ($row as $key => $value) {
$keyParts = explode('___', $key);
if (count($keyParts) > 1) {
$vKey = $keyParts[0];
$sourceKey = $keyParts[1];
if (!isset($ret[$vKey])) {
$ret[$vKey] = [];
}
$column = new Column($vKey, 'view');
$baseKey = substr($column->getDefinitionName(), 4);
$sourceInfo = $this->getTableInfo()->getSourceTableInfo($baseKey);
$sourceColDefs = $sourceInfo->getColumnDefinitions();
$ret[$vKey][$sourceKey] = $this->getValueWithColDef($value, $sourceKey, $sourceColDefs);
} else {
$ret[$key] = $this->getValueWithColDef($value, $key, $columnDefinitions);
}
}
return $ret;
}
private function getValueWithColDef($value, $key, $columnDefinitions) {
$column = new Column($key, 'view');
if (isset($columnDefinitions[$column->getDefinitionName()])) {
$colDef = $columnDefinitions[$column->getDefinitionName()];
if ((($colDef->getType() == 'datetime') || ($colDef->getType() == 'date'))
&& $value
) {
$date = $this->dateTimeFactory->create(
$this->db->getDatabasePlatform()
->getDateTimeFormatString(),
$value
);
if ((\DateTime::getLastErrors()['warning_count'] ?? null) > 0) {
$date = false;
}
if ($date === false) {
$date = $this->dateTimeFactory->create(
$this->db->getDatabasePlatform()
->getDateFormatString(),
$value
);
if ((\DateTime::getLastErrors()['warning_count'] ?? null) > 0) {
$date = false;
}
if ($date !== false) {
$date->setTime(0, 0);
}
}
/* dump([
'key' => $key,
'raw' => $value,
'colDefType' => $colDef->getType(),
'dbPlatformDateTimeFormat' => $this->db->getDatabasePlatform()->getDateTimeFormatString(),
'dbPlatformDateFormat' => $this->db->getDatabasePlatform()->getDateFormatString(),
]);*/
if ($date === false) {
return null;
}
return $date;
}
if (in_array($key, ['MPM_initialized_Date', 'MPM_Last_Change'])) {
}
return $value;
}
return $value;
}
public function lock($id)
{
if (!$this->options->useLocking()) {
return false;
}
if (!$this->hasUser()) {
return false;
}
if (!$this->canAccessObject($id)) {
throw new PortalException(self::msgNoAccess);
}
$ancestor = 0;
$tablename = $this->tableInfo->getInternalTablename();
if ($this->options->useVersioning()) {
$ancestor = $this->getAncestor($id);
}
$lockedAtColumn = $this->options->getLockedAtColumn();
$lockedByColumn = $this->options->getLockedByColumn();
if ($this->isWriteable($lockedAtColumn) && $this->isWriteable($lockedByColumn)) {
$builder = $this->db->createQueryBuilder();
$builder
->update($this->tableInfo->getInternalTablename(), '')
->set(
$lockedAtColumn,
$builder->createNamedParameter(
(new DateTime())->format(
$this->db->getDatabasePlatform()
->getDateTimeFormatString()
)
)
)
->set(
$lockedByColumn,
$builder->createNamedParameter(
$this->getUser()
->getId()
)
)
->where(
$builder->expr()
->orX(
$builder->expr()
->eq(
$lockedByColumn,
$this->getUser()
->getId()
),
$builder->expr()
->eq($lockedByColumn, 0),
$builder->expr()
->isNull($lockedByColumn)
)
);
if ($this->options->useVersioning()) {
$builder->andWhere($this->options->getAncestorColumn() . " = " . $ancestor);
} else {
$builder->andWhere($this->getPrimaryKey() . " = " . $builder->createNamedParameter($id));
}
$context = ['builder' => $builder, 'where_used' => true];
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::LOCK_BEFORE_EXECUTE);
$result = $builder->execute();
if ($result == 0) {
return false;
}
} else {
//throw new GenericRepositoryDeleteException(1810);
throw new Exception("Locking columns not writeable.");
}
return true;
}
/**
* @return bool
*/
protected function hasUser()
{
return is_a($this->user, 'Symfony\Component\Security\Core\User\UserInterface');
}
public function canAccessObject($id, $hide_deleted = true, $find_latest_version = true)
{
$pk = $this->tableInfo->getPrimaryKey();
$criteria = new CriteriaComparison('=', new CriteriaUnmappedColumn($pk), new CriteriaConstant($id));
return (false !== $this->findOneBy($criteria, [$pk], $hide_deleted, $find_latest_version, false));
}
public function findOneBy(
CriteriaBuilderInterface $filter,
array $view = null,
$hide_deleted = true,
$group_by_version = true,
$join_refs = true
)
{
$processedView = $this->processView($view);
$builder = $this->findByQuery(
$columnMap,
$filter,
null,
$processedView,
1,
0,
$hide_deleted,
$group_by_version,
$join_refs
);
$select_columns = $this->buildSelectColumns($columnMap, $processedView);
if (count($select_columns) > 0) {
return $this->fetch(
$builder->select($select_columns)
->execute()
);
}
return false;
}
public function fetch(Result $result)
{
if (null === $result) {
return null;
}
$row = $result->fetch();
if (!$row) {
return $row;
}
// Map refs
return $this->handleRow($row);
}
private function getAncestor($id)
{
$tablename = $this->tableInfo->getInternalTablename();
$ancestorColumn = $this->options->getAncestorColumn();
$refObject = $this->db->createQueryBuilder()
->select([$ancestorColumn])
->from($tablename, 'mt')
->where('mt.' . $this->getPrimaryKey() . ' = :id')
->setParameter('id', $id)
->setMaxResults(1)
->execute()
->fetch();
if (($refObject !== false) && isset($refObject[$ancestorColumn]) && ($refObject[$ancestorColumn] > 0)) {
return $refObject[$ancestorColumn];
} else {
throw new PortalException(self::msgAncestorNotFound);
}
}
public function isWriteable($column)
{
return $this->getTableInfo()
->isWriteable($column, $this->getUserRights());
}
/**
* @return mixed
*/
public function getUser()
{
return $this->user;
}
public function unlock($id)
{
if (!$this->options->useLocking()) {
return false;
}
if (!$this->hasUser()) {
return false;
}
if (!$this->canAccessObject($id)) {
throw new PortalException(self::msgNoAccess);
}
$ancestor = 0;
$tablename = $this->tableInfo->getInternalTablename();
if ($this->options->useVersioning()) {
$ancestorColumn = $this->options->getAncestorColumn();
$refObject = $this->db->createQueryBuilder()
->select([$ancestorColumn])
->from($tablename, 'mt')
->where('mt.' . $this->getPrimaryKey() . ' = :id')
->setParameter('id', $id)
->setMaxResults(1)
->execute()
->fetch();
if (($refObject !== false) && isset($refObject[$ancestorColumn]) && ($refObject[$ancestorColumn] > 0)) {
$ancestor = $refObject[$ancestorColumn];
} else {
//throw new GenericRepositoryDeleteException(1801);
throw new PortalException(self::msgAncestorNotFound);
}
}
$lockedAtColumn = $this->options->getLockedAtColumn();
$lockedByColumn = $this->options->getLockedByColumn();
if ($this->isWriteable($lockedAtColumn) && $this->isWriteable($lockedByColumn)) {
$builder = $this->db->createQueryBuilder();
$builder
->update($this->tableInfo->getInternalTablename(), '')
->set($this->options->getLockedByColumn(), $builder->createNamedParameter(0));
$where_used = false;
if (!$this->getUser()
->isAdmin()
) {
$builder->where(
$builder->expr()
->eq(
$this->options->getLockedByColumn(),
$this->getUser()
->getId()
)
);
$where_used = true;
}
if ($this->options->useVersioning()) {
if ($where_used) {
$builder->andWhere($this->options->getAncestorColumn() . " = " . $ancestor);
} else {
$builder->where($this->options->getAncestorColumn() . " = " . $ancestor);
$where_used = true;
}
} else {
if ($where_used) {
$builder->andWhere($this->getPrimaryKey() . " = " . $builder->createNamedParameter($id));
} else {
$builder->where($this->getPrimaryKey() . " = " . $builder->createNamedParameter($id));
$where_used = true;
}
}
$context = ['builder' => $builder, 'where_used' => $where_used];
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::UNLOCK_BEFORE_EXECUTE);
$result = $builder->execute();
if ($result == 0) {
throw new PortalException(Messages::msgErrorUnlock);
}
} else {
throw new PortalException(self::msgLockingColumnsNotWriteable);
}
return true;
}
public function deleteBy($criteria, &$allAffectedWatchDogNotificationRows = null)
{
// @todo: Implement batch delete
$group_by_version = $this->options->useDeleted();
$objects = $this->findBy(
$criteria,
null,
[$this->getPrimaryKey()],
null,
null,
true,
$group_by_version
);
$objects = $objects->fetchAll(\PDO::FETCH_ASSOC);
if (null !== $objects) {
$affectedWatchDogNotificationRows = 0;
foreach ($objects as $obj) {
$this->delete($obj[$this->getPrimaryKey()], $affectedWatchDogNotificationRows);
if ($allAffectedWatchDogNotificationRows) {
$allAffectedWatchDogNotificationRows += $affectedWatchDogNotificationRows;
}
}
}
}
public function findBy(
CriteriaBuilderInterface $filter = null,
array $sort = null,
array $view = null,
$limit = null,
$offset = null,
$hide_deleted = true,
$group_by_version = true,
$join_refs = true
)
{
$processedView = $this->processView($view);
$builder = $this->findByQuery(
$columnMap,
$filter,
$sort,
$processedView,
$limit,
$offset,
$hide_deleted,
$group_by_version,
$join_refs
);
$select_columns = $this->buildSelectColumns($columnMap, $processedView);
if (count($select_columns) > 0) {
return $builder->select($select_columns)
->execute();
}
return null;
}
public function historyBy(
$id,
CriteriaBuilderInterface $filter,
array $view = null,
$limit = null,
$offset = null,
$sort = null
) {
if (!$this->options->useVersioning()) {
return null;
}
$processedView = $this->processView($view);
$builder = $this->historyByQuery(
$id,
$columnMap,
$filter,
$processedView,
$limit,
$offset,
$sort
);
$selectColumns = $this->buildSelectColumns($columnMap, $processedView);
if (count($selectColumns) > 0) {
return $builder->select($selectColumns)
->execute();
}
return null;
}
public function historyByQuery(
$id,
&$columnMap,
CriteriaBuilderInterface $filter = null,
$processedView = null,
$limit = null,
$offset = null,
$sort = null,
$mt_name = 'mt'
)
{
if (null === $processedView) {
$processedView = $this->processView(null);
}
$tablename = $this->tableInfo->getInternalTablename();
$builder = $this->db->createQueryBuilder();
$this->tableInfo->buildFrom($builder, $mt_name);
$context = [
'builder' => $builder,
'where_used' => false
];
if (null !== $limit) {
$builder->setMaxResults($limit);
}
if (null !== $offset) {
$builder->setFirstResult($offset);
}
$columnMap = $this->buildColumnMapForJoins($builder, $processedView, true, $mt_name);
$ancestorColumn = $this->options->getAncestorColumn();
$versionColumn = $this->options->getVersionColumn();
$refObject = $this->db->createQueryBuilder()
->select([$ancestorColumn])
->from($tablename, $mt_name)
->where($mt_name . '.' . $this->getPrimaryKey() . ' = :id')
->setParameter('id', $id)
->setMaxResults(1)
->execute()
->fetch();
if ($refObject !== false) {
$ancestor = $refObject[$ancestorColumn];
} else {
throw new \Exception('Missing refObject.');
}
$builder->andWhere($mt_name . '.' . $ancestorColumn . ' = :ancestor');
$context['where_used'] = true;
$builder->setParameter('ancestor', $ancestor);
/*
Add filter and sorting
*/
if (null !== $filter) {
$criteria = $filter->buildCriteria($builder, $columnMap, $processedView, $mt_name);
if ($criteria) {
$builder->andWhere($criteria);
}
}
$this->applySorting($builder, $sort, [['column' => $versionColumn, 'direction' => 'DESC']], $columnMap, $processedView);
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::FIND_BEFORE_EXECUTE);
return $builder;
}
public function delete($id, &$affectedWatchDogNotificationRows = null)
{
$ancestor = 0;
$tablename = $this->tableInfo->getInternalTablename();
if (!$this->canAccessObject($id)) {
throw new PortalException(self::msgNoAccess);
}
if ($this->options->useVersioning()) {
$ancestorColumn = $this->options->getAncestorColumn();
$refObject = $this->db->createQueryBuilder()
->select([$ancestorColumn])
->from($tablename, 'mt')
->where('mt.' . $this->getPrimaryKey() . ' = :id')
->setParameter('id', $id)
->setMaxResults(1)
->execute()
->fetch();
if ($refObject !== false) {
$ancestor = $refObject[$ancestorColumn];
} else {
throw new GenericRepositoryDeleteException(1801);
}
}
if ($this->options->useDeleted()) {
if ($this->isWriteable($this->options->getDeletedColumn())) {
$obj = [
$this->options->getDeletedColumn() => 1,
$this->getPrimaryKey() => $id
];
try {
$newId = $this->persist($obj, true);
$this->incrementReleaseColumns($newId);
/*
* Consider encryption if applicable
*/
if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
$dsgvo = $this->container->get('projectbiz.dsgvo');
if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
$dsgvo->encryptRecord($id, $this->tableInfo->getTablename());
}
}
} catch (Exception $ex) {
throw new GenericRepositoryDeleteException(1802);
}
} else {
throw new GenericRepositoryDeleteException(1810);
}
} else {
$this->db->beginTransaction();
try {
// @todo: delete Refs
if ($this->options->useManSort()) {
// Get the manSort-Value of the Object
$delObject = $this->db->createQueryBuilder()
->select([$this->options->getManSortColumn()])
->from($tablename, 'mt')
->where('mt.' . $this->getPrimaryKey() . ' = :id')
->setParameter('id', $id)
->setMaxResults(1)
->execute()
->fetch();
$manSortDelete = $delObject[$this->options->getManSortColumn()];
if (null !== $manSortDelete) {
// Consider the set ...
$manSortSet = $this->options->getManSortSetColumns();
$manSortBuilder = $this->db->createQueryBuilder()
->update($this->tableInfo->getInternalTablename(), '')
->set($this->options->getManSortColumn(), $this->options->getManSortColumn() . ' - 1')
->where($this->options->getManSortColumn() . ' > ' . $manSortDelete);
for ($i = 0; $i < count($manSortSet); $i++) {
$manSortBuilder->andWhere($manSortSet[$i] . ' = ' . $delObject[$manSortSet[$i]]);
}
$manSortBuilder->execute();
}
}
if ($this->options->useVersioning()) {
$builder = $this->db->createQueryBuilder()
->delete($tablename)
->where($this->options->getAncestorColumn() . ' = ' . $ancestor);
$context = ['builder' => $builder, 'where_used' => true];
$this->dispatch( new RepositoryEvent($this, $context), RepositoryEvents::DELETE_BEFORE_EXECUTE);
$num_rows = $builder->execute();
} else {
$builder = $this->db->createQueryBuilder()
->delete($tablename)
->where($this->getPrimaryKey() . ' = ' . $id);
$context = ['builder' => $builder, 'where_used' => true];
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::DELETE_BEFORE_EXECUTE);
$num_rows = $builder->execute();
}
/*
* Consider encryption if applicable
*/
if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
$dsgvo = $this->container->get('projectbiz.dsgvo');
if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
$useVersioning = $this->options->useVersioning();
$dsgvo->deleteRecord($ancestor, $this->tableInfo->getTablename(), $useVersioning, $useVersioning);
}
}
} catch (\Exception $ex) {
$this->db->rollBack();
throw($ex);
}
$this->db->commit();
if ($num_rows == 0) {
throw new GenericRepositoryDeleteException(1803);
}
}
/*
* Delete corresponding watchdog-notifications.
*/
{
$queryBuilder = $this->db->createQueryBuilder();
$queryBuilder
->delete($this->prefix . 'WatchDogNotification')
->where('WatchDogNotification_LINK_Target_ID = ' .
$queryBuilder->createPositionalParameter($this->options->useVersioning() ? $ancestor : $id))
->andWhere('WatchDogNotification_Table_Caption = ' .
$queryBuilder->createPositionalParameter($this->tableInfo->getCaption()));
// This variable is a reference
$affectedWatchDogNotificationRows = $queryBuilder->execute();
}
}
public function persist($persistData, $forceSave = false, $copyAfterCreate = null, $updateKey = null)
{
$data = $persistData;
// Remove REFs
foreach ($data as $key => $value) {
if (strpos($key, 'REF_') === 0) {
unset($data[$key]);
}
}
$is_new = true;
$id = null;
if ($updateKey) {
$is_new = !$this->objectExists($updateKey, $data[$updateKey]);
} elseif (array_key_exists($this->getPrimaryKey(), $data)) {
if (null !== $data[$this->getPrimaryKey()]) {
$id = $data[$this->getPrimaryKey()];
$is_new = !$this->objectExists($id);
} else {
unset($data[$this->getPrimaryKey()]);
}
}
$rights = $this->getUserRights();
// Strip join-table data - it needs to be written "by hand" in the controller
// Strip data that is not writeable
foreach (array_keys($data) as $key) {
$column = new Column($key, 'view');
if (!$this->tableInfo->isWriteable($key, $rights) || ($column->getTableAlias() != 'mt')) {
unset($data[$key]);
}
}
if ($is_new) {
return $this->create($data, $copyAfterCreate);
} else {
unset($data[$this->getPrimaryKey()]);
return $this->update($id, $data, $forceSave);
}
}
private function objectExists($value, $keyColumn = null)
{
$tablename = $this->tableInfo->getInternalTablename();
$builder = $this->db->createQueryBuilder();
$result = $builder->select('COUNT(*) AS cnt')
->from($tablename)
->where(($keyColumn ?? $this->tableInfo->getPrimaryKey()) . '=' . $builder->createNamedParameter($value))
->execute();
$value = $result->fetch();
return !(($value === false) || ($value['cnt'] == 0));
}
protected function create($data, $copyAfterCreate = null)
{
$needPersist = false;
$write_data = [];
$refs = [];
// Join sent data with required special columns
$columns = array_unique(array_merge(array_keys($data), $this->options->getAutoColumns()));
$colDefs = $this->getColumnDefinitions();
foreach ($columns as $key) {
$value = isset($data[$key])
?$data[$key]
:null;
if (array_key_exists($key, $colDefs)) {
if ($colDefs[$key]->isVirtual()) {
$refs[] = [
'key' => $key,
'data' => [
'value' => $this->valueOrAutoValue($key, $value, null, true, $data)['value']
]
];
} else {
$write_data[$key] = $this->valueOrAutoValue($key, $value, null, true, $data)['value'];
}
}
$needPersist = true;
}
if ($needPersist) {
$this->db->beginTransaction();
try {
if ($this->tableInfo->getTablename() === 'UserRights') {
$submittedAdminRights = gmp_intval(gmp_and(gmp_init($data['UserRights_UserRight']), gmp_init(1)));
if ($submittedAdminRights == 1 && !$this->securityContextWrapper->isSuperAdmin()) {
throw new PortalException(self::msgAdminRightsDenied, [], 0, null, null, true);
}
}
$this->db->insert($this->tableInfo->getInternalTablename(), $write_data);
$lastInsertId = $this->db->lastInsertId();
foreach ($refs as $ref) {
$this->updateRefs($lastInsertId, $ref['key'], $ref['data']);
}
if ($this->options->useVersioning()) {
// Set the ancestor to the newly created id
$this->db->createQueryBuilder()
->update($this->tableInfo->getInternalTablename(), '')
->set($this->options->getAncestorColumn(), $lastInsertId)
->where($this->getPrimaryKey() . " = " . $lastInsertId)
->execute();
}
$this->incrementReleaseColumns($lastInsertId);
if ($this->options->useManSort() && !array_key_exists('SPM_manSort', $data)) {
// Set the manSort-Value of the newly created entry to the next available value
// Consider the set ...
$manSortSet = $this->options->getManSortSetColumns();
$manSortBuilder = $this->db->createQueryBuilder()
->from($this->tableInfo->getInternalTablename(), 'mt')
->select('MAX(mt.' . $this->options->getManSortColumn() . ') + 1 AS value');
if (count($manSortSet) > 0) {
$manSortBuilder
->leftJoin(
'mt',
$this->tableInfo->getInternalTablename(),
'j',
'j.' . $this->getPrimaryKey() . " = " . $lastInsertId
)
->where('mt.' . $manSortSet[0] . ' = j.' . $manSortSet[0]);
for ($i = 1; $i < count($manSortSet); $i++) {
$manSortBuilder->andWhere('mt.' . $manSortSet[$i] . ' = j.' . $manSortSet[$i]);
}
}
$manSortValue = $manSortBuilder
->execute()
->fetch()['value'];
if ($manSortValue == null) {
$manSortValue = '1';
}
$this->db->createQueryBuilder()
->update($this->tableInfo->getInternalTablename(), '')
->set($this->options->getManSortColumn(), $manSortValue)
->where($this->getPrimaryKey() . " = " . $lastInsertId)
->execute();
}
if ($copyAfterCreate != null) {
$queryBuilder = $this->db->createQueryBuilder()
->update($this->tableInfo->getInternalTablename(), '')
->where($this->getPrimaryKey() . " = " . $lastInsertId);
foreach ($copyAfterCreate as $source => $target) {
$queryBuilder->set($target, $source);
}
$queryBuilder->execute();
}
$object = $this->find($lastInsertId, false, false, true);
$context = [
'id' => $lastInsertId,
'object' => $object,
'database' => $this->db
];
/*
* Consider encryption if applicable
*/
if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
$dsgvo = $this->container->get('projectbiz.dsgvo');
if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
$dsgvo->encryptRecord($lastInsertId, $this->tableInfo->getTablename());
}
}
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::CREATE_AFTER_EXECUTE);
$this->db->commit();
return $lastInsertId;
} catch (Exception $ex) {
$this->db->rollBack();
throw($ex);
}
}
return 0;
}
/**
* Increment release columns.
*
* If a column with name *_LINK_*_ReleaseID is present it has a counterpart column with name *_ReleaseId
* in a corresponding table. That column gets incremented. The SPM set will be cloned and the new versions
* get this new id.
*
* @param type $lastInsertId
* @return type
* @throws Exception
*/
public function incrementReleaseColumns($lastInsertId)
{
if (!$this->options->useReleaseLink()) {
return;
}
$routeParams = $this->request->attributes->get('_route_params');
if(!isset($routeParams['parent_release_parameter'])) {
throw new Exception(self::msgMissingParentReleaseParameter);
}
$colDefs = $this->tableInfo->getColumnDefinitions();
$releaseLinkColumn = $this->options->getReleaseLinkColumn();
$primaryKeyColumn = $this->getPrimaryKey();
$parentTableName = $colDefs[$releaseLinkColumn]->getSource()['table'];
$parentTableRepo = $this->repoFactory->createGenericRepository($parentTableName);
$parentTable_releaseColumnName = $colDefs[$releaseLinkColumn]->getSource()['foreign_key'];
$parentId = $routeParams[$routeParams['parent_release_parameter']];
$parentReleaseId = $parentTableRepo->find($parentId)[$parentTable_releaseColumnName];
$repo = $this->repoFactory->createGenericRepository($this->tableInfo->getTablename());
$incrementedReleaseId = $this->aggregate($parentTable_releaseColumnName, $parentTableName, 'max' ) + 1;
/*
* Get the whole SPM-set. A set has the same release-id.
* Do not get the last inserted spm, since it is updated exclusively.
*/
$wholeSpmSet = $repo->findBy(
new CriteriaComposite(
'and',
[
new CriteriaComparison(
'=', new CriteriaUnmappedColumn($releaseLinkColumn), new CriteriaConstant($parentReleaseId)
),
new CriteriaComparison(
'<>', new CriteriaUnmappedColumn($primaryKeyColumn), new CriteriaConstant($lastInsertId)
)
]
), null, ['SPM_ID'], null, null, null, true, false
)->fetchAll();
/*
* Update the SPM-set with the incremented release id.
* The funtion versiondUpdate() duplicates a row when updating what is desired.
*/
foreach ($wholeSpmSet as $spm) {
$this->versionedUpdate($spm[$this->getPrimaryKey()], [$releaseLinkColumn => $incrementedReleaseId], true);
}
/*
* Update the release id of the last inserted record.
*/
$this->db->createQueryBuilder()
->update($this->tableInfo->getInternalTablename(), '')
->set($releaseLinkColumn, $incrementedReleaseId)
->where($this->getPrimaryKey() . " = " . $lastInsertId)
->execute();
/*
* Update the releaseColumn of the MPM record.
*/
$parentTableRepo->versionedUpdate($parentId, [$parentTable_releaseColumnName => $incrementedReleaseId], true);
}
protected function valueOrAutoValue($key, $value, $original_value, $creation = false, $data)
{
$userId = 0;
if (is_a($this->user, 'ProjectBiz\UserBundle\Entity\BaseUserInterface')) {
/* @var $userEntity \ProjectBiz\UserBundle\Entity\User */
$userEntity = $this->user;
$userId = $userEntity->getId();
}
$param_name = self::getNextParameterName();
/*
if ($creation && ($key == $this->getPrimaryKey())) {
return null;
}
*/
if ($this->options->useModificationTimestamp()) {
if ($key == $this->options->getModifiedByColumn()) {
return $this->simpleValue($param_name, $userId);
} else {
if ($key == $this->options->getModifiedAtColumn()) {
return $this->simpleValue(
$param_name,
(new DateTime())->format(
$this->db->getDatabasePlatform()
->getDateTimeFormatString()
)
);
}
}
}
if ($this->options->useCreationTimestamp()) {
if ($creation) {
// @todo: check the "else" case. It might be possible to send in new creation date on modification
if ($key == $this->options->getCreatedByColumn()) {
return $this->simpleValue($param_name, $userId);
} else {
if ($key == $this->options->getCreatedAtColumn()) {
return $this->simpleValue(
$param_name,
(new DateTime())->format(
$this->db->getDatabasePlatform()
->getDateTimeFormatString()
)
);
}
}
} else {
if ($key == $this->options->getCreatedByColumn()) {
return $this->simpleValue($param_name, $original_value);
} else {
if ($key == $this->options->getCreatedAtColumn()) {
return $this->simpleValue($param_name, $original_value);
}
}
}
}
if ($this->options->useLog()) {
$logPos = array_search($key, $this->options->getLogColumns());
if ($logPos !== false) {
if (!empty($value)) {
if ($this->options->hasLogHeader($logPos)) {
$logHeader = $this->options->getLogHeader($logPos);
$value = $logHeader->render() . $value;
} else {
// Here must be the standard-log-header in the service-yml declared !!
if (null !== ($this->options->getOption('standard-log-header'))) {
/** @var $standardLogHeader \ProjectBiz\PortalBundle\Service\LogHeaderInterface */
$standardLogHeader = $this->options->getOption('standard-log-header');
$value = $standardLogHeader->render() . $value;
}
else {
//var_dump(render());
//var_dump($data);
//var_dump($key);
//exit;
}
}
}
// @todo: There is a problem with "write-only" logs:
// only the most recent log entry is written, because the original_value is not readable and thus empty.
// This could be fixed by CONCAT_WS, or something with ISNULL, IFNULL, but not without doing a doctrine
// extension as those are not supported:
if ($creation || empty($original_value)) {
return $this->simpleValue($param_name, $value);
} else {
$value = "\n\n" . $value;
return [
'expr' => $this->db->getDatabasePlatform()
->getConcatExpression(
$key,
$this->db->getDatabasePlatform()
->getConcatExpression('CHAR(13)', ':' . $param_name)
),
'param' => $param_name,
'value' => $value
];
}
}
}
if ($this->options->useVersioning()) {
if ($creation && ($key == $this->options->getVersionColumn())) {
// @todo: check the "else" case. It might be possible to send in new version on modification
if (null !== $value) {
return $this->simpleValue($param_name, $value); // Start with given version ...
} else {
return $this->simpleValue($param_name, 1); // ... or with default version 1
}
}
}
if ($this->options->useDeleted()) {
if ($key == $this->options->getDeletedColumn()) {
if ($value !== null) {
return $this->simpleValue($param_name, $value);
} else {
return $this->simpleValue($param_name, 0);
}
}
}
return $this->simpleValue($param_name, $value);
}
protected function simpleValue($param_name, $value)
{
if (is_a($value, '\DateTime')) {
/** @var $dataTimeValue \DateTime * */
$dataTimeValue = $value;
$dbValue = $dataTimeValue->format(
$this->db->getDatabasePlatform()
->getDateTimeFormatString()
);
} else {
$dbValue = $value;
}
return [
'expr' => ':' . $param_name,
'param' => $param_name,
'value' => $dbValue
];
}
protected function updateRefs($id, $key, $modifiedValue)
{
// Update refs by dropping and rebuilding
$sourceTableColumn = 'Refs_SourceTable';
$sourceIdColumn = 'Refs_Source_LINK_ID';
$targetTableColumn = 'Refs_TargetTable';
$targetIdColumn = 'Refs_Target_LINK_ID';
$refsProperty = $key;
if ($this->getColumnDefinitions()[$key]->getType() == 'fer') {
$sourceTableColumn = 'Refs_TargetTable';
$sourceIdColumn = 'Refs_Target_LINK_ID';
$targetTableColumn = 'Refs_SourceTable';
$targetIdColumn = 'Refs_Source_LINK_ID';
$refsProperty = $this->getColumnDefinitions()[$key]->getReverseProperty();
}
$refsBuilder = $this->db->createQueryBuilder();
$refsBuilder
->delete($this->prefix . 'Refs')
->where(
$sourceTableColumn .
' = ' .
$refsBuilder->createNamedParameter(
$this->getTableInfo()
->getTablename()
)
)
->andWhere($sourceIdColumn . ' = ' . $refsBuilder->createNamedParameter($id))
->andWhere('Refs_Property = ' . $refsBuilder->createNamedParameter($refsProperty));
$refsBuilder
->execute();
if (isset($modifiedValue['value']) && strlen($modifiedValue['value'] > 0)) {
$targetTable = $this->getColumnDefinitions()[$key]->getSource()['table'];
$refs = array_map(
function ($value) use (
$key,
$id,
$targetTable,
$sourceTableColumn,
$sourceIdColumn,
$targetTableColumn,
$targetIdColumn,
$refsProperty
) {
return [
$sourceTableColumn => $this->getTableInfo()
->getTablename(),
$sourceIdColumn => $id,
$targetTableColumn => $targetTable,
$targetIdColumn => $value,
'Refs_Property' => $refsProperty
];
},
explode(',', $modifiedValue['value'])
);
foreach ($refs as $ref) {
$this->db->insert(
$this->prefix . 'Refs',
$ref
);
}
}
}
/**
* @param $id
* @param $data
* @param $forceSave
*
* @return string
*
* @throws Exception
*/
protected function update($id, $data, $forceSave)
{
if (!$this->canAccessObject($id, false, false)) {
throw new PortalException(self::msgNoAccess);
}
$usespecialunversionDoc=false;
If ($this->tableInfo->getInternalTablename()=='Tab_Document'){
if(!(isset($data['Document_LINK_File_ID']))){
$usespecialunversionDoc =true;
}
$usespecialunversionDoc =false;
}
if ($this->options->useVersioning() && !$usespecialunversionDoc ) {
return $this->versionedUpdate($id, $data, $forceSave);
} else {
return $this->unversionedUpdate($id, $data, null, null, $forceSave);
}
}
/**
* @param $id
* @param $data
* @param $forceSave
*
* @return string
*
* @throws Exception
* @throws \Doctrine\DBAL\ConnectionException
*/
protected function versionedUpdate($id, $data, $forceSave, $compareWithLatestVersion = false)
{
$original_object = $this->find($id, $compareWithLatestVersion, false, true);
$changed_columns = $this->getChangedColumns($data, $original_object);
if ((count($changed_columns) > 0) || $forceSave) {
// Get latest object
// @todo: object based access rights might lead to no latest object, crashing the saving ... need FIX!
$latest_object = $this->find($id);
// Clone latest object and update all changed columns
$this->db->beginTransaction();
try {
$cloned_id = $this->cloneObject($latest_object[$this->getPrimaryKey()]);
$new_version = $latest_object[$this->options->getVersionColumn()] + 1;
$this->db->createQueryBuilder()
->update($this->tableInfo->getInternalTablename(), '')
->set($this->options->getVersionColumn(), $new_version)
->where($this->getPrimaryKey() . " = " . $cloned_id)
->execute();
// Prevent overwriting with old value:
$data[$this->options->getVersionColumn()] = $new_version;
// Prevent overwriting with null:
$data[$this->options->getAncestorColumn()] = $latest_object[$this->options->getAncestorColumn()];
/*
* Prevent overwriting with null for ancestor- and release-columns:
*/
{
$data[$this->options->getAncestorColumn()] = $latest_object[$this->options->getAncestorColumn()];
foreach ($this->options->getReleaseColumns() as $releaseColumn) {
if (!isset($data[$releaseColumn])) {
$data[$releaseColumn] = $latest_object[$releaseColumn];
}
}
}
$this->unversionedUpdate($cloned_id, $data, $changed_columns, $original_object, $forceSave, false, false);
if (!$forceSave) {
$this->incrementReleaseColumns($cloned_id);
}
$new_object = $this->find($cloned_id, false, false, true);
$context = [
'id' => $cloned_id,
'prev_id' => $id,
'object' => $new_object,
'prev_object' => $latest_object,
'database' => $this->db
];
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::UPDATE_AFTER_EXECUTE);
$this->db->commit();
// Send notifications about changes in this record
$this->sendNotifications($changed_columns, $original_object, $new_object);
return $cloned_id;
} catch (\Exception $ex) {
if ($this->db->isTransactionActive()) {
$this->db->rollBack();
}
throw($ex);
}
}
return $id;
}
/**
* Examine all changed columns by checking them against the conditions in Tab_MailTrigger.
* Send a notification to the particular e-mail adress if the condition applies.
*
* @param $changedColumns
* @param $originalObject
* @param $newObject
*/
protected function sendNotifications($changedColumns, $originalObject, $newObject)
{
if (!count($changedColumns)) {
return;
}
$colDefs = $this->tableInfo->getColumnDefinitions();
$columnNames = [];
$changedColumnNames = [];
/*
* Build an array with ColumnDefinition_ID and column-name mapping for both,
* $columnNames and $changedColumnNames.
* For example:
* [
* 7 => MPM_Projectname,
* 28 => MPM_Text,
* ...
* ]
*
* In $changedColumnNames ignore the objects which have the value 0 at column _MailTrigger_Active.
* If _MailTrigger_Active column does not exist, it defaults to active.
*/
{
$mailTriggerActiveColumn = $colDefs[$changedColumns[0]]->getTable() . '_MailTrigger_Active';
foreach ($colDefs as $colName => $colDef) {
$columnNames[$colDef->getId()] = $colName;
}
foreach ($changedColumns as $column) {
if (isset($colDefs[$mailTriggerActiveColumn])) {
if ($newObject[$mailTriggerActiveColumn] == 0) {
continue;
}
}
$changedColumnNames[$colDefs[$column]->getId()] = $column;
}
}
$allMailtriggers = $this->repoFactory->createGenericRepository('MailTrigger')->findAll();
if ($allMailtriggers) {
$possibleMailTriggers = $allMailtriggers->fetchAll();
} else {
$possibleMailTriggers = [];
}
$affectedMailTriggers = [];
/*
* Let's go further and check if their conditions apply.
* Filter all applying mail-triggers and store them in $affectedMailTriggers[].
*/
foreach ($possibleMailTriggers as $possibleMailTrigger) {
foreach ($changedColumnNames as $changedColumnId => $changedColumnName) {
if ($possibleMailTrigger['MailTrigger_LINK_Column_ID'] != $changedColumnId && $possibleMailTrigger['MailTrigger_LINK_Right_Column_ID'] != $changedColumnId ) {
continue;
}
$comparisonOperator = $possibleMailTrigger['REF_MailTrigger_LINK_MailTriggerType_ID___MailTriggerType_Definition'];
/*
* Make sure there is no compromising code injection.
*/
if (!$possibleMailTrigger['MailTrigger_Trigger_Changes']) {
$allowedComparisonOperators = [
'==',
'!=',
'<>',
'<',
'>',
'<=',
'>=',
];
if (!in_array($comparisonOperator, $allowedComparisonOperators)) {
throw new PortalException(
'Comparison operator "'.$comparisonOperator.'" in column MailTriggerType_Definition is not allowed. Allowed operators are:'.PHP_EOL.
implode(', ', $allowedComparisonOperators)
);
}
}
/*
* MailTrigger_Trigger_Changes is a checkbox to send notifications when the observed value has changed.
*/
if ($possibleMailTrigger['MailTrigger_Trigger_Changes']) {
if ($possibleMailTrigger['MailTrigger_LINK_Column_ID'] == $changedColumnId && $newObject[$changedColumnName] !== $originalObject[$changedColumnName]) {
$affectedMailTriggers[] = $possibleMailTrigger;
}
// If MailTrigger_Trigger_Changes is active, the other possible comparison with MailTrigger_LINK_Right_Column_ID is skipped.
continue;
}
/*
* $leftValue is the left operand of the comparison.
*/
$leftValueColumnName = $possibleMailTrigger['REF_MailTrigger_LINK_Column_ID___ColDefinition_InternalColumnname'];
$leftValue = $newObject[$leftValueColumnName];
$leftType = $colDefs[$leftValueColumnName]->getType();
/*
* $rightValue is the right operand of the comparison.
* It can either be a constant (else part) or relate to a column-id (if-part).
*/
if ($possibleMailTrigger['MailTrigger_LINK_Right_Column_ID']) {
$rightValueColumnName = $possibleMailTrigger['REF_MailTrigger_LINK_Right_Column_ID___ColDefinition_InternalColumnname'];
$rightValue = $newObject[$rightValueColumnName];
/*
* Some data types need a special handling for a comparison.
* The type of the right operand is not needed. The user should not create a mailtrigger
* with two columns having different data types.
*/
if ($leftType == 'date' || $leftType == 'datetime') {
if(!$rightValue || !$leftValue) {
continue;
}
$leftValue = $leftValue->getTimestamp();
$rightValue = $rightValue->getTimestamp();
} elseif ($leftType == 'link') {
$leftValue = $newObject['REF_' . $leftValueColumnName]['display'];
$rightValue = $newObject['REF_' . $rightValueColumnName]['display'];
}
} else {
$rightValue = $possibleMailTrigger['MailTrigger_Value'];
/*
* Some data types need a special handling for a comparison.
*/
if ($leftType == 'date' || $leftType == 'datetime') {
$rightValue = \DateTime::createFromFormat(
$leftType == 'date' ? $this->dateFormat : $this->dateTimeFormat,
$rightValue
);
if ($rightValue) {
$rightValue = $rightValue->getTimestamp();
}
$leftValue = $leftValue->getTimestamp();
} elseif ($leftType == 'link') {
$leftValue = $newObject['REF_' . $leftValueColumnName]['display'];
} elseif ($leftType == 'percent') {
$rightValue /= 100;
}
}
/*
* MailTrigger_Deviance is a percental variance applicable for columns with type »euro«.
* The deviance can be positive or negative.
*/
if ($leftType == 'euro' && $possibleMailTrigger['MailTrigger_Deviance']) {
// Remove whitespaces. '- 10' will be converted to '-10';
$deviance = str_replace(' ', '', $possibleMailTrigger['MailTrigger_Deviance']);
$leftValue += ($leftValue * $deviance/100);
}
/*
* Does the condition of a trigger apply?
* The eval statement adds up to a comparison, for example:
* »return '160'>'150';«
* or
* »return 'string1'=='string2';«
*/
if ($rightValue !== false && eval('return \'' . $leftValue . '\'' . $comparisonOperator . '\'' . $rightValue . '\';')) {
$affectedMailTriggers[] = $possibleMailTrigger;
}
}
}
$userRepo = false;
/*
* Send an email for every trigger that took effect
*/
foreach ($affectedMailTriggers as $affectedMailTrigger) {
/*
* create object only once, to consider performance.
*/
if(!$userRepo) {
$userRepo = $this->repoFactory->createGenericRepository('User');
}
$columnName = $columnNames[$affectedMailTrigger['MailTrigger_LINK_Column_ID']];
$infoColumns = [];
/*
* There are 6 slots which optionally contain columns that are available in the mail template as further info about the changed record.
* They can be included in the mail template like: {{infoColumns[1]}}
*/
for ($i = 1; $i <= 6; $i++) {
if($affectedMailTrigger['MailTrigger_LINK_Info' . $i . '_Column_ID']) {
$infoColumnName = $columnNames[$affectedMailTrigger['MailTrigger_LINK_Info' . $i . '_Column_ID']];
/*
* If the info-column is of type link, grab the target value to avoid displaying an id.
*/
if ($colDefs[$infoColumnName]->getType() == 'link') {
$infoColumns[$i] = $newObject['REF_' .$infoColumnName]['display'];
} else {
$infoColumns[$i] = $newObject[$infoColumnName];
}
} else {
$infoColumns[$i] = null;
}
}
$recipients = [];
/*
* The e-mail field "MailTrigger_Send_To_Address" can have multiple addresses separated by ';'.
* Send an e-mail for each e-mail address seperately, since recipients should not see each others e-mail addresses.
*/
if ($affectedMailTrigger['MailTrigger_Send_To_Address']) {
$recipients = explode(';', $affectedMailTrigger['MailTrigger_Send_To_Address']);
}
/*
* 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".
* Thereby the user does not need to know the e-mail address of this recipient at all.
*/
if ($affectedMailTrigger['REF_MailTrigger_Send_To___ColDefinition_InternalColumnname'] && $newObject[$affectedMailTrigger['REF_MailTrigger_Send_To___ColDefinition_InternalColumnname']]) {
$recipientUserId = $newObject[$affectedMailTrigger['REF_MailTrigger_Send_To___ColDefinition_InternalColumnname']];
$recipients[] = $userRepo->find($recipientUserId)['User_Mail'];
}
$recipients = array_unique($recipients);
$logMailTriggerRepo = $this->repoFactory->createGenericRepository('LogMailTrigger');
/*
* Send e-mails and write logs to LogMailTrigger whether or not the e-mail was sent sucessfully.
*/
foreach ($recipients as $recipient) {
$success = true;
try {
$this->mailNotifier->notify(
[
'template' => $affectedMailTrigger['MailTrigger_LINK_MailTemplate_ID'],
'to' => $recipient,
],
[
'columnCaption' => $colDefs[$columnName]->getCaption(),
'infoColumns' => $infoColumns,
'oldValue' => $originalObject[$columnName],
'newValue' => $newObject[$columnName]
]
);
} catch(\Exception $e) {
$success = false;
}
$logMailTriggerRepo->persist(
[
'LogMailTrigger_LINK_MailTrigger_ID' => $affectedMailTrigger['MailTrigger_ID'],
'LogMailTrigger_Sent_To' => $recipient,
'LogMailTrigger_Success' => $success
]
);
}
}
}
public function find($id, $find_latest_version = true, $hide_deleted = true, $join_refs = false)
{
if ($this->options->useVersioning() && $find_latest_version) {
return $this->findLatestById($id, $hide_deleted, $join_refs);
}
$criteria = new CriteriaComparison(
'=',
new CriteriaUnmappedColumn($this->tableInfo->getPrimaryKey()),
new CriteriaConstant($id)
);
return $this->findOneBy($criteria, null, $hide_deleted, $find_latest_version, $join_refs);
}
public function findLatestById($id, $hide_deleted = true, $join_refs = false)
{
if (!$this->options->useVersioning()) {
return $this->find($id, false, $hide_deleted);
}
$ancestor = $this->getAncestor($id);
$criteria = new CriteriaComparison(
'=',
new CriteriaUnmappedColumn($this->options->getAncestorColumn()),
new CriteriaConstant($ancestor)
);
return $this->findOneBy($criteria, null, $hide_deleted, true, $join_refs);
}
protected function getChangedColumns($data, $original_object)
{
// @todo: check
$columns = array_diff(
$this->getWriteableColumns(),
$this->options->getAutoColumns()
);
// Get Original (unmodified) object from database and calculate changed columns
$changed_columns = [];
foreach ($data as $key => $value) {
if (!$this->options->isAutoColumn($key) && in_array($key, $columns)) {
// Special handling of Log-Columns, because every non-empty input changes the column
if ($this->options->useLog() && ($this->options->isLogColumn($key))) {
if (!empty($value)) {
$changed_columns[] = $key;
}
} else { // Special handling of Password-Columns, because an empty input does not changes the column
if ($this->options->usePassword() && ($this->options->isPasswordColumn($key))) {
if (!empty($value)) {
$changed_columns[] = $key;
}
} else {
if (!isset($original_object[$key]) || ($original_object[$key] != $value)) {
$changed_columns[] = $key;
}
}
}
}
}
return $changed_columns;
}
public function getWriteableColumns()
{
return $this->getTableInfo()
->getWriteableColumns($this->getUserRights());
}
/**
* @param string $id The new objects id
*
* @return string
*
* @throws \Doctrine\DBAL\ConnectionException
*/
protected function cloneObject($id, $cloneWholeBunch = false)
{
// Existing Columns get cloned, so no data is trashed when there are missing definitions
$colDefs = $this->tableInfo->getColumnDefinitions();
$columns = array_filter(
array_diff($this->tableInfo->getColumns(), [$this->getPrimaryKey()]),
function ($value) use ($colDefs) {
$col = new Column($value, 'view');
return !$colDefs[$col->getDefinitionName()]->isVirtual();
}
);
$selectColumns = array_map(
function ($viewColumnName) {
return (new Column($viewColumnName, 'view'))->getSelectName();
},
$columns
);
$builder = $this->db->createQueryBuilder()
->select($selectColumns)
->from($this->tableInfo->getInternalTablename(), 'mt');
if($cloneWholeBunch) {
$builder->where('mt.' . $this->options->getReleaseLinkColumn() . ' = ' . $builder->createNamedParameter($id));
} else {
$builder->where('mt.' . $this->getPrimaryKey() . ' = ' . $builder->createNamedParameter($id));
}
$sourceObjects = $builder->execute()->fetchAll();
$clonedIds = [];
$this->db->beginTransaction();
foreach ($sourceObjects as $sourceObject) {
unset($sourceObject['doctrine_rownum']); // @todo: MSSQL-fix. Check if this is the best solution
try {
$this->db->insert($this->tableInfo->getInternalTablename(), $sourceObject);
$clonedIds[] = $clonedId = $this->db->lastInsertId();
// Clone all refs FROM this object
$this->cloneRefs('Refs_SourceTable', $this->tableInfo->getTablename(), 'Refs_Source_LINK_ID', $id, $clonedId);
// Clone all refs TO this object
$this->cloneRefs('Refs_TargetTable', $this->tableInfo->getTablename(), 'Refs_Target_LINK_ID', $id, $clonedId);
} catch (\Exception $ex) {
if($this->db->isTransactionActive()) {
$this->db->rollBack();
}
throw new PortalException(
self::msgErrorClone,
['%id%' => $id, '%table%' => $this->tableInfo->getInternalTablename()],
0,
$ex
);
}
}
$this->db->commit();
if ($cloneWholeBunch) {
return $clonedIds;
} else {
return $clonedId;
}
}
private function cloneRefs($tableColumn, $tablename, $idColumn, $id, $clonedId)
{
$refsBuilder = $this->db->createQueryBuilder();
$refsBuilder
->select('*')
->from($this->prefix . 'Refs', '')
->where(
$tableColumn . ' = ' . $refsBuilder->createNamedParameter($tablename)
)
->andWhere($idColumn . ' = ' . $refsBuilder->createNamedParameter($id));
$refs = $refsBuilder->execute()
->fetchAll();
if ($refs) {
$refs = array_map(
function ($value) use ($clonedId, $idColumn) {
unset($value['Refs_ID']);
$value[$idColumn] = $clonedId;
return $value;
},
$refs
);
foreach ($refs as $ref) {
$this->db->insert($this->prefix . 'Refs', $ref);
}
}
}
protected function unversionedUpdate(
$id,
$data,
$changed_columns = null,
$original_object = null,
$forceSave = false,
$finalSave = true,
$sendNotifications = true
)
{
if (!isset($original_object)) {
$original_object = $this->find($id, false, false, true);
}
if ($this->options->useWriteRecordRights() && !$this->securityContextWrapper->checkRecordRights($original_object[$this->options->getWriteRecordRightsColumn()], 1)) {
throw new UnaccessibleObjectException(self::msgMissingWritePermissions);
}
if (!isset($changed_columns)) {
$changed_columns = $this->getChangedColumns($data, $original_object);
}
if ((count($changed_columns) > 0) || $forceSave) {
// Add special columns to changed columns to ensure correct automated setting of values
$this->db->beginTransaction();
try {
if ($this->tableInfo->getTablename() === 'UserRights') {
$originalAdminRights = gmp_and(gmp_init($original_object['UserRights_UserRight']), gmp_init(1));
$submittedAdminRights = gmp_and(gmp_init($data['UserRights_UserRight']), gmp_init(1));
if(gmp_cmp($submittedAdminRights, $originalAdminRights) > 0 && !$this->securityContextWrapper->isSuperAdmin()) {
throw new PortalException(self::msgAdminRightsDenied, [], 0, null, null, true);
}
}
$changed_columns = array_merge($changed_columns, $this->options->getAutoColumns());
$queryBuilder = $this->db->createQueryBuilder()
->update($this->tableInfo->getInternalTablename(), '');
$queryBuilder
->where($this->getPrimaryKey() . " = " . $queryBuilder->createNamedParameter($id));
$needPersist = $forceSave;
foreach ($changed_columns as $key) {
$value = isset($data[$key])
?$data[$key]
:null;
$modifiedValue = $this->valueOrAutoValue(
$key,
$value,
isset($original_object[$key])
?$original_object[$key]
:null,
null,
$data
);
$colDefs = $this->getColumnDefinitions();
if ($colDefs[$key]->getType() == 'html') {
$htmlPurifierConfig = \HTMLPurifier_Config::createDefault();
$htmlPurifierConfig->set('Attr.AllowedFrameTargets', ['_blank', '_top', '_self', '_parent']);
$purifier = new \HTMLPurifier($htmlPurifierConfig);
$modifiedValue['value'] = $purifier->purify($modifiedValue['value']);
}
if ($colDefs[$key]->isVirtual()) {
$this->updateRefs($id, $key, $modifiedValue);
} else {
// Standard update
$queryBuilder->set($key, $modifiedValue['expr']);
$queryBuilder->setParameter($modifiedValue['param'], $modifiedValue['value']);
$needPersist = true;
}
}
if ($needPersist) {
$queryBuilder->execute();
/*
* Consider encryption if applicable
*/
if ($this->container->getParameter('projectbiz.dsgvo.use_encryption')) {
$dsgvo = $this->container->get('projectbiz.dsgvo');
if ($dsgvo->getCorrespondingEncryptionTable($this->tableInfo->getTablename())) {
$dsgvo->encryptRecord($id, $this->tableInfo->getTablename(), true, $this->options->useVersioning());
}
}
if ($finalSave) {
$new_object = $this->find($id, false, false, true);
$this->logChangesOfUserTable($original_object, $new_object);
$context = [
'id' => $id,
'object' => $new_object,
'prev_object' => $original_object,
'database' => $this->db
];
$this->dispatch(new RepositoryEvent($this, $context), RepositoryEvents::UPDATE_AFTER_EXECUTE);
}
}
} catch (\Exception $ex) {
$this->db->rollBack();
throw($ex);
}
$this->db->commit();
if ($sendNotifications) {
$this->sendNotifications($changed_columns, $original_object, $new_object);
}
}
return $id;
}
/**
* Log the changes that have been made to the »User«-table into table »UserLog«.
*
* @param array $original_object
* @param array $new_object
*/
protected function logChangesOfUserTable($original_object, $new_object)
{
if ($this->tableInfo->getTablename() != 'User') {
return;
}
$changedValues = false;
/*
* Do not log changes within the following columns.
*/
$blacklist = ['User_Last_Change', 'User_Failure_Count', 'User_SessionID', 'User_Logged_In', 'User_Log', 'User_Previous_login'];
foreach ($new_object as $key => $value) {
if ($new_object[$key] != $original_object[$key] && !in_array($key, $blacklist)) {
$changedValues .= $key . ': »' . print_r($original_object[$key], true) . '« => »' . print_r($new_object[$key], true) . '«' . PHP_EOL;
}
}
if ($changedValues) {
$this->db->insert(
$this->prefix . 'UserLog',
[
'UserLog_LINK_User_ID' => $this->securityContextWrapper->getUserId(),
'UserLog_Affected_LINK_User_ID' => $original_object['User_ID'],
'UserLog_initialized_Date' => (new \DateTime())->format($this->db->getDatabasePlatform()->getDateTimeFormatString()),
'UserLog_Changes' => $changedValues,
]
);
}
}
public function archive($id)
{
$ancestor = 0;
$tablename = $this->tableInfo->getInternalTablename();
if (!$this->canAccessObject($id)) {
throw new PortalException(self::msgNoAccess);
}
if ($this->options->useVersioning()) {
$ancestorColumn = $this->options->getAncestorColumn();
$refObject = $this->db->createQueryBuilder()
->select([$ancestorColumn])
->from($tablename, 'mt')
->where('mt.' . $this->getPrimaryKey() . ' = :id')
->setParameter('id', $id)
->setMaxResults(1)
->execute()
->fetch();
if ($refObject !== false) {
$ancestor = $refObject[$ancestorColumn];
} else {
throw new GenericRepositoryArchiveException(1901);
}
}
if ($this->options->useArchived()) {
if ($this->isWriteable($this->options->getArchivedColumn())) {
$obj = [
$this->options->getArchivedColumn() => 1,
$this->getPrimaryKey() => $id
];
try {
$this->persist($obj, true);
} catch (Exception $ex) {
throw new GenericRepositoryArchiveException(1902);
}
} else {
throw new GenericRepositoryArchiveException(1910);
}
}
}
public function duplicate($id, $data = null)
{
$colDefs = $this->getColumnDefinitions();
$criteria = new CriteriaComparison(
'=',
new CriteriaUnmappedColumn($this->getPrimaryKey()),
new CriteriaConstant($id)
);
$object = $this->findOneBy($criteria);
if (null !== $data) {
foreach (array_keys($object) as $key) {
if (array_key_exists($key, $data)) {
$object[$key] = $data[$key];
}
}
}
foreach (array_keys($object) as $column) {
/*
* If »ColDefinition_ExcludeFromCloning« is true for a column and the user did not change the columns value,
* overwrite the data with the default value (ColDefinition_StandardValue).
* This prevents the duplication of some values, e.g. unique project numbers.
*/
if (isset($colDefs[$column]) && $colDefs[$column]->getExcludeFromCloning() && (!isset($data[$column]) || $data[$column] === $object[$column])) {
$object[$column] = $colDefs[$column]->getDefault();
}
}
if ($object !== false) {
unset($object[$this->getPrimaryKey()]);
if ($this->getOptions()
->useVersioning()
) {
unset($object[$this->getOptions()
->getVersionColumn()]);
}
return $this->create($object);
}
return null;
}
/**
* Restores a version by getting the specified id and saving it as a new version.
*
* @param int $id primary key id of the version to restore.
* @throws Exception
*/
public function restoreVersion($id)
{
if (!$this->getOptions()->useVersioning()) {
throw new \Exception('Cannot restore a version in an unversioned table.');
}
$targetVersion = $this->find($id, false, false, true);
unset($targetVersion[$this->getPrimaryKey()]);
unset($targetVersion[$this->options->getVersionColumn()]);
$this->versionedUpdate($id, $targetVersion, false, true);
}
/**
* @param array $columns The columns that are required to be readable
*
* @return GenericRepository
*
* @throws RequiredColumnNotReadableException
*/
public function requireReadableColumns(array $columns)
{
foreach ($columns as $column) {
if (!$this->isReadable($column)) {
throw new RequiredColumnNotReadableException($column);
}
}
return $this;
}
public function isReadable($column)
{
return $this->getTableInfo()
->isReadable($column, $this->getUserRights());
}
/**
* @param array $columns The columns that are required to be writeable
*
* @return GenericRepository
*
* @throws RequiredColumnNotWriteableException
*/
public function requireWriteableColumns(array $columns)
{
foreach ($columns as $column) {
if (!$this->isReadable($column)) {
throw new RequiredColumnNotWriteableException($column);
}
}
return $this;
}
/**
* @param array $view
* @param bool $with_required
*
* @return array
*/
public function getReadableColumnsInView($view = null, $with_required = true)
{
$viewColumnsOrAll = $this->getColumnsInView($view);
$viewAndRequired = $viewColumnsOrAll;
if ($with_required) {
$requiredColumns = $this->getRequiredColumns();
foreach ($requiredColumns as $column) {
$viewAndRequired[] = $column;
}
$viewAndRequired[] = $this->getPrimaryKey();
}
$readableColumns = $this->getReadableColumns();
return array_unique(array_intersect($viewAndRequired, $readableColumns));
}
public function getColumnsInView($view = null)
{
if (null === $view) {
return $this->tableInfo->getColumns();
}
return array_intersect($view, $this->tableInfo->getColumns());
}
public function getReadableColumns()
{
return $this->getTableInfo()
->getReadableColumns($this->getUserRights());
}
public function defaultObject()
{
$result = [];
$columnDefinitions = $this->getColumnDefinitions();
$readableColumns = $this->getReadableColumns();
foreach ($readableColumns as $column) {
$col = new Column($column, 'view');
$colDef = $columnDefinitions[$col->getDefinitionName()];
$defaultValue = $colDef->getDefault();
if ($defaultValue !== null) {
$result[$column] = $defaultValue;
}
}
unset($result[$this->getPrimaryKey()]);
return $result;
}
public function isSystemColumn($column)
{
return $this->options->isSystemColumn($column);
}
public function isAutoColumn($column)
{
return $this->options->isAutoColumn($column);
}
/**
* Return type for special columns
*
* Returns 'hidden' for primary key columns and 'log' for log-columns.
*
* @todo Try to get rid of this function. It is called in RepositoryBlock, TableViewController,
* TableDataViewController and GenericTableType. Try to replace it with something better
*
* @deprecated
*
* @param string $columnName
*
* @return null|string
*/
public function getSpecialColumnType($columnName)
{
if ($this->getPrimaryKey() == $columnName) {
return 'hidden';
}
if ($this->options->useLog()) {
if (in_array($columnName, $this->options->getLogColumns())) {
return 'log';
}
}
return null;
}
public function getLabelForPropertyPath($propertyPath) {
return $this->getLabelForPropertyPathWithInfo(explode(':', $propertyPath), $this->tableInfo);
}
private function getLabelForPropertyPathWithInfo($propertyPath, $info) {
if (count($propertyPath) == 1) {
return $this->getLabelForProperty($propertyPath[0], $info);
}
else if (count($propertyPath) > 1) {
$part0 = array_shift($propertyPath);
$column = new Column($part0, 'view');
$sourceInfo = $this->tableInfo->getSourceTableInfo($column->getDefinitionName());
return $this->getLabelForProperty($part0, $info) . ': ' . $this->getLabelForPropertyPathWithInfo($propertyPath, $sourceInfo);
}
}
public function isVirtualForPropertyPath($propertyPath) {
return $this->isVirtualForPropertyPathWithInfo(explode(':', $propertyPath), $this->tableInfo);
}
private function isVirtualForPropertyPathWithInfo($propertyPath, TableInfoInterface $info) {
if (count($propertyPath) == 1) {
$colDefs = $info->getColumnDefinitions();
$column = new Column($propertyPath[0], 'view');
$defName = $column->getDefinitionName();
if (!array_key_exists($defName, $colDefs)) {
throw new \Exception('Missing Column Definition: ' . $defName);
}
return $colDefs[$defName]->isVirtual();
}
else if (count($propertyPath) > 1) {
$part0 = array_shift($propertyPath);
$column = new Column($part0, 'view');
$sourceInfo = $this->tableInfo->getSourceTableInfo($column->getDefinitionName());
return $this->isVirtualForPropertyPathWithInfo($propertyPath, $sourceInfo);
}
}
private function getLabelForProperty($property, $info) {
return $info->getLabelForViewColumn($property);
}
}