scope aware workflow controller and manager

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
This commit is contained in:
Arthur Schiwon 2019-08-19 17:13:47 +02:00
parent bd5c455da4
commit 4aba1f1cff
No known key found for this signature in database
GPG Key ID: 7424F1874854DF23
9 changed files with 763 additions and 161 deletions

View File

@ -18,12 +18,15 @@ return array(
'OCA\\WorkflowEngine\\Check\\RequestUserAgent' => $baseDir . '/../lib/Check/RequestUserAgent.php',
'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => $baseDir . '/../lib/Check/UserGroupMembership.php',
'OCA\\WorkflowEngine\\Command\\Index' => $baseDir . '/../lib/Command/Index.php',
'OCA\\WorkflowEngine\\Controller\\AWorkflowController' => $baseDir . '/../lib/Controller/AWorkflowController.php',
'OCA\\WorkflowEngine\\Controller\\FlowOperations' => $baseDir . '/../lib/Controller/FlowOperations.php',
'OCA\\WorkflowEngine\\Controller\\GlobalWorkflowsController' => $baseDir . '/../lib/Controller/GlobalWorkflowsController.php',
'OCA\\WorkflowEngine\\Controller\\RequestTime' => $baseDir . '/../lib/Controller/RequestTime.php',
'OCA\\WorkflowEngine\\Controller\\UserWorkflowsController' => $baseDir . '/../lib/Controller/UserWorkflowsController.php',
'OCA\\WorkflowEngine\\Entity\\File' => $baseDir . '/../lib/Entity/File.php',
'OCA\\WorkflowEngine\\Entity\\GenericEntityEmitterEvent' => $baseDir . '/../lib/Entity/GenericEntityEmitterEvent.php',
'OCA\\WorkflowEngine\\Entity\\IEntityEmitterEvent' => $baseDir . '/../lib/Entity/IEntityEmitterEvent.php',
'OCA\\WorkflowEngine\\Helper\\ScopeContext' => $baseDir . '/../lib/Helper/ScopeContext.php',
'OCA\\WorkflowEngine\\Manager' => $baseDir . '/../lib/Manager.php',
'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => $baseDir . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php',
'OCA\\WorkflowEngine\\Migration\\Version2019Date20190808074233' => $baseDir . '/../lib/Migration/Version2019Date20190808074233.php',

View File

@ -33,12 +33,15 @@ class ComposerStaticInitWorkflowEngine
'OCA\\WorkflowEngine\\Check\\RequestUserAgent' => __DIR__ . '/..' . '/../lib/Check/RequestUserAgent.php',
'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => __DIR__ . '/..' . '/../lib/Check/UserGroupMembership.php',
'OCA\\WorkflowEngine\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php',
'OCA\\WorkflowEngine\\Controller\\AWorkflowController' => __DIR__ . '/..' . '/../lib/Controller/AWorkflowController.php',
'OCA\\WorkflowEngine\\Controller\\FlowOperations' => __DIR__ . '/..' . '/../lib/Controller/FlowOperations.php',
'OCA\\WorkflowEngine\\Controller\\GlobalWorkflowsController' => __DIR__ . '/..' . '/../lib/Controller/GlobalWorkflowsController.php',
'OCA\\WorkflowEngine\\Controller\\RequestTime' => __DIR__ . '/..' . '/../lib/Controller/RequestTime.php',
'OCA\\WorkflowEngine\\Controller\\UserWorkflowsController' => __DIR__ . '/..' . '/../lib/Controller/UserWorkflowsController.php',
'OCA\\WorkflowEngine\\Entity\\File' => __DIR__ . '/..' . '/../lib/Entity/File.php',
'OCA\\WorkflowEngine\\Entity\\GenericEntityEmitterEvent' => __DIR__ . '/..' . '/../lib/Entity/GenericEntityEmitterEvent.php',
'OCA\\WorkflowEngine\\Entity\\IEntityEmitterEvent' => __DIR__ . '/..' . '/../lib/Entity/IEntityEmitterEvent.php',
'OCA\\WorkflowEngine\\Helper\\ScopeContext' => __DIR__ . '/..' . '/../lib/Helper/ScopeContext.php',
'OCA\\WorkflowEngine\\Manager' => __DIR__ . '/..' . '/../lib/Manager.php',
'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => __DIR__ . '/..' . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php',
'OCA\\WorkflowEngine\\Migration\\Version2019Date20190808074233' => __DIR__ . '/..' . '/../lib/Migration/Version2019Date20190808074233.php',

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace OCA\WorkflowEngine\Command;
use OCA\WorkflowEngine\Helper\ScopeContext;
use OCA\WorkflowEngine\Manager;
use OCP\WorkflowEngine\IManager;
use Symfony\Component\Console\Command\Command;
@ -69,8 +70,10 @@ class Index extends Command {
protected function execute(InputInterface $input, OutputInterface $output) {
$ops = $this->manager->getAllOperations(
$this->mappedScope($input->getArgument('scope')),
$input->getArgument('scopeId')
new ScopeContext(
$this->mappedScope($input->getArgument('scope')),
$input->getArgument('scopeId')
)
);
$output->writeln(\json_encode($ops));
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\WorkflowEngine\Controller;
use Doctrine\DBAL\DBALException;
use OCA\WorkflowEngine\Helper\ScopeContext;
use OCA\WorkflowEngine\Manager;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
abstract class AWorkflowController extends OCSController {
/** @var Manager */
protected $manager;
public function __construct(
$appName,
IRequest $request,
Manager $manager
) {
parent::__construct($appName, $request);
$this->manager = $manager;
}
/**
* @throws OCSForbiddenException
*/
abstract protected function getScopeContext(): ScopeContext;
/**
* Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global?format=json"
*
* @throws OCSForbiddenException
*/
public function index(): DataResponse {
$operationsByClass = $this->manager->getAllOperations($this->getScopeContext());
foreach ($operationsByClass as &$operations) {
foreach ($operations as &$operation) {
$operation = $this->manager->formatOperation($operation);
}
}
return new DataResponse($operationsByClass);
}
/**
* Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global/OCA\\Workflow_DocToPdf\\Operation?format=json"
*
* @throws OCSForbiddenException
*/
public function show(string $id): DataResponse {
$context = $this->getScopeContext();
// The ID corresponds to a class name
$operations = $this->manager->getOperations($id, $context);
foreach ($operations as &$operation) {
$operation = $this->manager->formatOperation($operation);
}
return new DataResponse($operations);
}
/**
* @throws OCSBadRequestException
* @throws OCSForbiddenException
* @throws OCSException
*/
public function create(string $class, string $name, array $checks, string $operation): DataResponse {
$context = $this->getScopeContext();
try {
$operation = $this->manager->addOperation($class, $name, $checks, $operation, $context);
$operation = $this->manager->formatOperation($operation);
return new DataResponse($operation);
} catch (\UnexpectedValueException $e) {
throw new OCSBadRequestException($e->getMessage(), $e);
} catch (\DomainException $e) {
throw new OCSForbiddenException($e->getMessage(), $e);
} catch(DBALException $e) {
throw new OCSException('An internal error occurred', $e);
}
}
/**
* @throws OCSBadRequestException
* @throws OCSForbiddenException
* @throws OCSException
*/
public function update(int $id, string $name, array $checks, string $operation): DataResponse {
try {
$operation = $this->manager->updateOperation($id, $name, $checks, $operation, $this->getScopeContext());
$operation = $this->manager->formatOperation($operation);
return new DataResponse($operation);
} catch (\UnexpectedValueException $e) {
throw new OCSBadRequestException($e->getMessage(), $e);
} catch (\DomainException $e) {
throw new OCSForbiddenException($e->getMessage(), $e);
} catch(DBALException $e) {
throw new OCSException('An internal error occurred', $e);
}
}
/**
* @throws OCSBadRequestException
* @throws OCSForbiddenException
* @throws OCSException
*/
public function destroy(int $id): DataResponse {
try {
$deleted = $this->manager->deleteOperation($id, $this->getScopeContext());
return new DataResponse($deleted);
} catch (\UnexpectedValueException $e) {
throw new OCSBadRequestException($e->getMessage(), $e);
} catch (\DomainException $e) {
throw new OCSForbiddenException($e->getMessage(), $e);
} catch(DBALException $e) {
throw new OCSException('An internal error occurred', $e);
}
}
}

View File

@ -24,88 +24,18 @@ declare(strict_types=1);
namespace OCA\WorkflowEngine\Controller;
use OCA\WorkflowEngine\Manager;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCA\WorkflowEngine\Helper\ScopeContext;
use OCP\WorkflowEngine\IManager;
class GlobalWorkflowsController extends OCSController {
class GlobalWorkflowsController extends AWorkflowController {
/** @var Manager */
private $manager;
/** @var ScopeContext */
private $scopeContext;
public function __construct(
$appName,
IRequest $request,
Manager $manager
) {
parent::__construct($appName, $request);
$this->manager = $manager;
}
/**
* Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global?format=json"
*/
public function index(): DataResponse {
$operationsByClass = $this->manager->getAllOperations();
foreach ($operationsByClass as &$operations) {
foreach ($operations as &$operation) {
$operation = $this->manager->formatOperation($operation);
}
protected function getScopeContext(): ScopeContext {
if($this->scopeContext === null) {
$this->scopeContext = new ScopeContext(IManager::SCOPE_ADMIN);
}
return new DataResponse($operationsByClass);
}
/**
* @throws OCSBadRequestException
*
* Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global/OCA\\Workflow_DocToPdf\\Operation?format=json"
*/
public function show(string $id): DataResponse {
// The ID corresponds to a class name
$operations = $this->manager->getOperations($id);
foreach ($operations as &$operation) {
$operation = $this->manager->formatOperation($operation);
}
return new DataResponse($operations);
}
/**
* @throws OCSBadRequestException
*/
public function create(string $class, string $name, array $checks, string $operation): DataResponse {
try {
$operation = $this->manager->addOperation($class, $name, $checks, $operation);
$operation = $this->manager->formatOperation($operation);
return new DataResponse($operation);
} catch (\UnexpectedValueException $e) {
throw new OCSBadRequestException($e->getMessage(), $e);
}
}
/**
* @throws OCSBadRequestException
*/
public function update(int $id, string $name, array $checks, string $operation): DataResponse {
try {
$operation = $this->manager->updateOperation($id, $name, $checks, $operation);
$operation = $this->manager->formatOperation($operation);
return new DataResponse($operation);
} catch (\UnexpectedValueException $e) {
throw new OCSBadRequestException($e->getMessage(), $e);
}
}
/**
*/
public function destroy(int $id): DataResponse {
$deleted = $this->manager->deleteOperation((int) $id);
return new DataResponse($deleted);
return $this->scopeContext;
}
}

View File

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\WorkflowEngine\Controller;
use OCA\WorkflowEngine\Helper\ScopeContext;
use OCA\WorkflowEngine\Manager;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\WorkflowEngine\IManager;
class UserWorkflowsController extends AWorkflowController {
/** @var IUserSession */
private $session;
/** @var ScopeContext */
private $scopeContext;
public function __construct(
$appName,
IRequest $request,
Manager $manager,
IUserSession $session
) {
parent::__construct($appName, $request, $manager);
$this->session = $session;
}
/**
* Retrieve all configured workflow rules
*
* Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/user?format=json"
*
* @NoAdminRequired
* @throws OCSForbiddenException
*/
public function index(): DataResponse {
return parent::index();
}
/**
* @NoAdminRequired
*
* Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/user/OCA\\Workflow_DocToPdf\\Operation?format=json"
* @throws OCSForbiddenException
*/
public function show(string $id): DataResponse {
return parent::show($id);
}
/**
* @NoAdminRequired
* @throws OCSBadRequestException
* @throws OCSForbiddenException
*/
public function create(string $class, string $name, array $checks, string $operation): DataResponse {
return parent::create($class, $name, $checks, $operation);
}
/**
* @NoAdminRequired
* @throws OCSBadRequestException
* @throws OCSForbiddenException
*/
public function update(int $id, string $name, array $checks, string $operation): DataResponse {
return parent::update($id, $name, $checks, $operation);
}
/**
* @NoAdminRequired
* @throws OCSForbiddenException
*/
public function destroy(int $id): DataResponse {
return parent::destroy($id);
}
/**
* @throws OCSForbiddenException
*/
protected function getScopeContext(): ScopeContext {
if($this->scopeContext === null) {
$user = $this->session->getUser();
if(!$user) {
throw new OCSForbiddenException('User not logged in');
}
$this->scopeContext = new ScopeContext(IManager::SCOPE_USER, $user->getUID());
}
return $this->scopeContext;
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\WorkflowEngine\Helper;
use OCP\WorkflowEngine\IManager;
class ScopeContext {
/** @var int */
private $scope;
/** @var string */
private $scopeId;
/** @var string */
private $hash;
public function __construct(int $scope, string $scopeId = null) {
$this->scope = $this->evaluateScope($scope);
$this->scopeId = $this->evaluateScopeId($scopeId);
}
private function evaluateScope(int $scope): int {
if(in_array($scope, [IManager::SCOPE_ADMIN, IManager::SCOPE_USER], true)) {
return $scope;
}
throw new \InvalidArgumentException('Invalid scope');
}
private function evaluateScopeId(string $scopeId = null): string {
if($this->scope === IManager::SCOPE_USER
&& trim((string)$scopeId) === '')
{
throw new \InvalidArgumentException('user scope requires a user id');
}
return trim((string)$scopeId);
}
/**
* @return int
*/
public function getScope(): int {
return $this->scope;
}
/**
* @return string
*/
public function getScopeId(): string {
return $this->scopeId;
}
public function getHash(): string {
if($this->hash === null) {
$this->hash = \hash('sha256', $this->getScope() . '::' . $this->getScopeId());
}
return $this->hash;
}
}

View File

@ -23,7 +23,10 @@ namespace OCA\WorkflowEngine;
use OC\Files\Storage\Wrapper\Jail;
use Doctrine\DBAL\DBALException;
use OC\Cache\CappedMemoryCache;
use OCA\WorkflowEngine\Entity\File;
use OCA\WorkflowEngine\Helper\ScopeContext;
use OCP\AppFramework\QueryException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Storage\IStorage;
@ -74,6 +77,10 @@ class Manager implements IManager, IEntityAware {
/** @var ILogger */
protected $logger;
/** @var CappedMemoryCache */
protected $operationsByScope = [];
/** @var IUserSession */
protected $session;
@ -95,6 +102,7 @@ class Manager implements IManager, IEntityAware {
$this->l = $l;
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
$this->operationsByScope = new CappedMemoryCache(64);
$this->session = $session;
}
@ -114,7 +122,16 @@ class Manager implements IManager, IEntityAware {
* @inheritdoc
*/
public function getMatchingOperations($class, $returnFirstMatchingOperationOnly = true) {
$operations = $this->getOperations($class);
$scopes[] = new ScopeContext(IManager::SCOPE_ADMIN);
$user = $this->session->getUser();
if($user !== null) {
$scopes[] = new ScopeContext(IManager::SCOPE_USER, $user->getUID());
}
$operations = [];
foreach ($scopes as $scope) {
$operations = array_merge($operations, $this->getOperations($class, $scope));
}
$matches = [];
foreach ($operations as $operation) {
@ -160,19 +177,10 @@ class Manager implements IManager, IEntityAware {
throw new \UnexpectedValueException($this->l->t('Check %s is invalid or does not exist', $check['class']));
}
}
public function getAllOperations(int $scope = IManager::SCOPE_ADMIN, string $scopeId = null): array {
if(!in_array($scope, [IManager::SCOPE_ADMIN, IManager::SCOPE_USER])) {
throw new \InvalidArgumentException('Provided value for scope is not supported');
public function getAllOperations(ScopeContext $scopeContext): array {
if(isset($this->operations[$scopeContext->getHash()])) {
return $this->operations[$scopeContext->getHash()];
}
if($scope === IManager::SCOPE_USER && $scopeId === null) {
$user = $this->session->getUser();
if($user === null) {
throw new \InvalidArgumentException('No user ID was provided');
}
$scopeId = $user->getUID();
}
$this->operations = [];
$query = $this->connection->getQueryBuilder();
@ -181,48 +189,29 @@ class Manager implements IManager, IEntityAware {
->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id'))
->where($query->expr()->eq('s.type', $query->createParameter('scope')));
if($scope === IManager::SCOPE_USER) {
if($scopeContext->getScope() === IManager::SCOPE_USER) {
$query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId')));
}
$query->setParameters(['scope' => $scope, 'scopeId' => $scopeId]);
$query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
$result = $query->execute();
$this->operations[$scopeContext->getHash()] = [];
while ($row = $result->fetch()) {
if(!isset($this->operations[$row['class']])) {
$this->operations[$row['class']] = [];
if(!isset($this->operations[$scopeContext->getHash()][$row['class']])) {
$this->operations[$scopeContext->getHash()][$row['class']] = [];
}
$this->operations[$row['class']][] = $row;
$this->operations[$scopeContext->getHash()][$row['class']][] = $row;
}
return $this->operations;
return $this->operations[$scopeContext->getHash()];
}
/**
* @param string $class
* @return array[]
*/
public function getOperations($class) {
if (isset($this->operations[$class])) {
return $this->operations[$class];
public function getOperations(string $class, ScopeContext $scopeContext): array {
if (!isset($this->operations[$scopeContext->getHash()])) {
$this->getAllOperations($scopeContext);
}
$query = $this->connection->getQueryBuilder();
$query->select('*')
->from('flow_operations')
->where($query->expr()->eq('class', $query->createNamedParameter($class)));
$result = $query->execute();
$this->operations[$class] = [];
while ($row = $result->fetch()) {
$this->operations[$class][] = $row;
}
$result->closeCursor();
return $this->operations[$class];
return $this->operations[$scopeContext->getHash()][$class] ?? [];
}
/**
@ -246,22 +235,7 @@ class Manager implements IManager, IEntityAware {
throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id]));
}
/**
* @param string $class
* @param string $name
* @param array[] $checks
* @param string $operation
* @return array The added operation
* @throws \UnexpectedValueException
*/
public function addOperation($class, $name, array $checks, $operation) {
$this->validateOperation($class, $name, $checks, $operation);
$checkIds = [];
foreach ($checks as $check) {
$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
}
protected function insertOperation(string $class, string $name, array $checkIds, string $operation): int {
$query = $this->connection->getQueryBuilder();
$query->insert('flow_operations')
->values([
@ -272,10 +246,68 @@ class Manager implements IManager, IEntityAware {
]);
$query->execute();
$id = $query->getLastInsertId();
return $query->getLastInsertId();
}
/**
* @param string $class
* @param string $name
* @param array[] $checks
* @param string $operation
* @return array The added operation
* @throws \UnexpectedValueException
* @throws DBALException
*/
public function addOperation($class, $name, array $checks, $operation, ScopeContext $scope) {
$this->validateOperation($class, $name, $checks, $operation);
$this->connection->beginTransaction();
try {
$checkIds = [];
foreach ($checks as $check) {
$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
}
$id = $this->insertOperation($class, $name, $checkIds, $operation);
$this->addScope($id, $scope);
$this->connection->commit();
} catch (DBALException $e) {
$this->connection->rollBack();
throw $e;
}
return $this->getOperation($id);
}
protected function canModify(int $id, ScopeContext $scopeContext):bool {
if(isset($this->operationsByScope[$scopeContext->getHash()])) {
return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
}
$qb = $this->connection->getQueryBuilder();
$qb = $qb->select('o.id')
->from('flow_operations', 'o')
->leftJoin('o', 'flow_operations_scope', 's', $qb->expr()->eq('o.id', 's.operation_id'))
->where($qb->expr()->eq('s.type', $qb->createParameter('scope')));
if($scopeContext->getScope() !== IManager::SCOPE_ADMIN) {
$qb->where($qb->expr()->eq('s.value', $qb->createParameter('scopeId')));
}
$qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]);
$result = $qb->execute();
$this->operationsByScope[$scopeContext->getHash()] = [];
while($opId = $result->fetchColumn(0)) {
$this->operationsByScope[$scopeContext->getHash()][] = (int)$opId;
}
$result->closeCursor();
return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true);
}
/**
* @param int $id
* @param string $name
@ -283,23 +315,36 @@ class Manager implements IManager, IEntityAware {
* @param string $operation
* @return array The updated operation
* @throws \UnexpectedValueException
* @throws \DomainException
* @throws DBALException
*/
public function updateOperation($id, $name, array $checks, $operation) {
public function updateOperation($id, $name, array $checks, $operation, ScopeContext $scopeContext): array {
if(!$this->canModify($id, $scopeContext)) {
throw new \DomainException('Target operation not within scope');
};
$row = $this->getOperation($id);
$this->validateOperation($row['class'], $name, $checks, $operation);
$checkIds = [];
foreach ($checks as $check) {
$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
}
try {
$this->connection->beginTransaction();
foreach ($checks as $check) {
$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
}
$query = $this->connection->getQueryBuilder();
$query->update('flow_operations')
->set('name', $query->createNamedParameter($name))
->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
->set('operation', $query->createNamedParameter($operation))
->where($query->expr()->eq('id', $query->createNamedParameter($id)));
$query->execute();
$query = $this->connection->getQueryBuilder();
$query->update('flow_operations')
->set('name', $query->createNamedParameter($name))
->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
->set('operation', $query->createNamedParameter($operation))
->where($query->expr()->eq('id', $query->createNamedParameter($id)));
$query->execute();
$this->connection->commit();
} catch (DBALException $e) {
$this->connection->rollBack();
throw $e;
}
unset($this->operations[$scopeContext->getHash()]);
return $this->getOperation($id);
}
@ -308,12 +353,36 @@ class Manager implements IManager, IEntityAware {
* @param int $id
* @return bool
* @throws \UnexpectedValueException
* @throws DBALException
* @throws \DomainException
*/
public function deleteOperation($id) {
public function deleteOperation($id, ScopeContext $scopeContext) {
if(!$this->canModify($id, $scopeContext)) {
throw new \DomainException('Target operation not within scope');
};
$query = $this->connection->getQueryBuilder();
$query->delete('flow_operations')
->where($query->expr()->eq('id', $query->createNamedParameter($id)));
return (bool) $query->execute();
try {
$this->connection->beginTransaction();
$result = (bool)$query->delete('flow_operations')
->where($query->expr()->eq('id', $query->createNamedParameter($id)))
->execute();
if($result) {
$qb = $this->connection->getQueryBuilder();
$result &= (bool)$qb->delete('flow_operations_scope')
->where($qb->expr()->eq('operation_id', $query->createNamedParameter($id)))
->execute();
}
$this->connection->commit();
} catch (DBALException $e) {
$this->connection->rollBack();
throw $e;
}
if(isset($this->operations[$scopeContext->getHash()])) {
unset($this->operations[$scopeContext->getHash()]);
}
return $result;
}
/**
@ -427,6 +496,18 @@ class Manager implements IManager, IEntityAware {
return $query->getLastInsertId();
}
protected function addScope(int $operationId, ScopeContext $scope): void {
$query = $this->connection->getQueryBuilder();
$insertQuery = $query->insert('flow_operations_scope');
$insertQuery->values([
'operation_id' => $query->createNamedParameter($operationId),
'type' => $query->createNamedParameter($scope->getScope()),
'value' => $query->createNamedParameter($scope->getScopeId()),
]);
$insertQuery->execute();
}
public function formatOperation(array $operation): array {
$checkIds = json_decode($operation['checks'], true);
$checks = $this->getChecks($checkIds);

View File

@ -23,12 +23,16 @@ namespace OCA\WorkflowEngine\Tests;
use OCA\WorkflowEngine\Entity\File;
use OCA\WorkflowEngine\Helper\ScopeContext;
use OCA\WorkflowEngine\Manager;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IServerContainer;
use OCP\WorkflowEngine\ICheck;
use OCP\WorkflowEngine\IEntity;
use OCP\WorkflowEngine\IManager;
use OCP\WorkflowEngine\IOperation;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Test\TestCase;
@ -74,18 +78,38 @@ class ManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger
);
$this->clearChecks();
$this->clearTables();
}
protected function tearDown() {
$this->clearChecks();
$this->clearTables();
parent::tearDown();
}
public function clearChecks() {
/**
* @return MockObject|ScopeContext
*/
protected function buildScope(string $scopeId = null): MockObject {
$scopeContext = $this->createMock(ScopeContext::class);
$scopeContext->expects($this->any())
->method('getScope')
->willReturn($scopeId ? IManager::SCOPE_USER : IManager::SCOPE_ADMIN);
$scopeContext->expects($this->any())
->method('getScopeId')
->willReturn($scopeId ?? '');
$scopeContext->expects($this->any())
->method('getHash')
->willReturn(md5($scopeId ?? ''));
return $scopeContext;
}
public function clearTables() {
$query = $this->db->getQueryBuilder();
$query->delete('flow_checks')
->execute();
foreach(['flow_checks', 'flow_operations', 'flow_operations_scope'] as $table) {
$query->delete($table)
->execute();
}
}
public function testChecks() {
@ -109,6 +133,221 @@ class ManagerTest extends TestCase {
$this->assertArrayHasKey($check2, $data);
}
public function testScope() {
$adminScope = $this->buildScope();
$userScope = $this->buildScope('jackie');
$opId1 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo']
);
$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
$opId2 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
$opId3 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId1, $adminScope]));
$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId2, $adminScope]));
$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId3, $adminScope]));
$this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId1, $userScope]));
$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId2, $userScope]));
$this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId3, $userScope]));
}
public function testGetAllOperations() {
$adminScope = $this->buildScope();
$userScope = $this->buildScope('jackie');
$opId1 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo']
);
$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
$opId2 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
$opId3 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestUserOp', 'Test03', [11, 44], 'foobar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
$adminOps = $this->manager->getAllOperations($adminScope);
$userOps = $this->manager->getAllOperations($userScope);
$this->assertSame(1, count($adminOps));
$this->assertTrue(array_key_exists('OCA\WFE\TestAdminOp', $adminOps));
$this->assertFalse(array_key_exists('OCA\WFE\TestUserOp', $adminOps));
$this->assertSame(1, count($userOps));
$this->assertFalse(array_key_exists('OCA\WFE\TestAdminOp', $userOps));
$this->assertTrue(array_key_exists('OCA\WFE\TestUserOp', $userOps));
$this->assertSame(2, count($userOps['OCA\WFE\TestUserOp']));
}
public function testGetOperations() {
$adminScope = $this->buildScope();
$userScope = $this->buildScope('jackie');
$opId1 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo']
);
$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
$opId4 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\OtherTestOp', 'Test04', [5], 'foo']
);
$this->invokePrivate($this->manager, 'addScope', [$opId4, $adminScope]);
$opId2 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
$opId3 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]);
$opId5 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\OtherTestOp', 'Test05', [5], 'foobar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]);
$adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope);
$userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope);
$this->assertSame(1, count($adminOps));
array_walk($adminOps, function ($op) {
$this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
});
$this->assertSame(2, count($userOps));
array_walk($userOps, function ($op) {
$this->assertTrue($op['class'] === 'OCA\WFE\TestOp');
});
}
public function testUpdateOperation() {
$adminScope = $this->buildScope();
$userScope = $this->buildScope('jackie');
$this->container->expects($this->any())
->method('query')
->willReturnCallback(function ($class) {
if(substr($class, -2) === 'Op') {
return $this->createMock(IOperation::class);
}
return $this->createMock(ICheck::class);
});
$opId1 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo']
);
$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
$opId2 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
$check1 = ['class' => 'OCA\WFE\C22', 'operator' => 'eq', 'value' => 'asdf'];
$check2 = ['class' => 'OCA\WFE\C33', 'operator' => 'eq', 'value' => 23456];
/** @noinspection PhpUnhandledExceptionInspection */
$op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope);
$this->assertSame('Test01a', $op['name']);
$this->assertSame('foohur', $op['operation']);
/** @noinspection PhpUnhandledExceptionInspection */
$op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope);
$this->assertSame('Test02a', $op['name']);
$this->assertSame('barfoo', $op['operation']);
foreach([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
try {
/** @noinspection PhpUnhandledExceptionInspection */
$this->manager->updateOperation($run[1], 'Evil', [$check2], 'hackx0r', $run[0]);
$this->assertTrue(false, 'DomainException not thrown');
} catch (\DomainException $e) {
$this->assertTrue(true);
}
}
}
public function testDeleteOperation() {
$adminScope = $this->buildScope();
$userScope = $this->buildScope('jackie');
$opId1 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo']
);
$this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]);
$opId2 = $this->invokePrivate(
$this->manager,
'insertOperation',
['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar']
);
$this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]);
foreach([[$adminScope, $opId2], [$userScope, $opId1]] as $run) {
try {
/** @noinspection PhpUnhandledExceptionInspection */
$this->manager->deleteOperation($run[1], $run[0]);
$this->assertTrue(false, 'DomainException not thrown');
} catch (\Exception $e) {
$this->assertInstanceOf(\DomainException::class, $e);
}
}
/** @noinspection PhpUnhandledExceptionInspection */
$this->manager->deleteOperation($opId1, $adminScope);
/** @noinspection PhpUnhandledExceptionInspection */
$this->manager->deleteOperation($opId2, $userScope);
foreach([$opId1, $opId2] as $opId) {
try {
$this->invokePrivate($this->manager, 'getOperation', [$opId]);
$this->assertTrue(false, 'UnexpectedValueException not thrown');
} catch(\Exception $e) {
$this->assertInstanceOf(\UnexpectedValueException::class, $e);
}
}
}
public function testGetEntitiesListBuildInOnly() {
$fileEntityMock = $this->createMock(File::class);