diff --git a/.gitignore b/.gitignore index f60dda513e4bbf197a03cf88aaa53f4aa21d8e14..63a34beb978d4827fbdd9ac0cee151d3e91afb6a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ !/apps/admin_audit !/apps/updatenotification !/apps/theming +!/apps/workflowengine /apps/files_external/3rdparty/irodsphp/PHPUnitTest /apps/files_external/3rdparty/irodsphp/web /apps/files_external/3rdparty/irodsphp/prods/test diff --git a/apps/workflowengine/appinfo/app.php b/apps/workflowengine/appinfo/app.php new file mode 100644 index 0000000000000000000000000000000000000000..f6f22ce9488715187e3f0fded34f0d6cef2cd523 --- /dev/null +++ b/apps/workflowengine/appinfo/app.php @@ -0,0 +1,23 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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/>. + * + */ + +$application = new \OCA\WorkflowEngine\AppInfo\Application(); +$application->registerHooksAndListeners(); diff --git a/apps/workflowengine/appinfo/database.xml b/apps/workflowengine/appinfo/database.xml new file mode 100644 index 0000000000000000000000000000000000000000..b67a41faed20ab470a90b5d69f98d759b033e152 --- /dev/null +++ b/apps/workflowengine/appinfo/database.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="ISO-8859-1" ?> +<database> + <name>*dbname*</name> + <create>true</create> + <overwrite>false</overwrite> + <charset>utf8</charset> + + <table> + <name>*dbprefix*flow_checks</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <length>4</length> + </field> + + <field> + <name>class</name> + <type>text</type> + <notnull>true</notnull> + <length>256</length> + </field> + <field> + <name>operator</name> + <type>text</type> + <notnull>true</notnull> + <length>16</length> + </field> + <field> + <name>value</name> + <type>clob</type> + <notnull>false</notnull> + </field> + <field> + <name>hash</name> + <type>text</type> + <notnull>true</notnull> + <length>32</length> + </field> + + <index> + <name>flow_unique_hash</name> + <unique>true</unique> + <field> + <name>hash</name> + </field> + </index> + </declaration> + </table> + + <table> + <name>*dbprefix*flow_operations</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <length>4</length> + </field> + + <field> + <name>class</name> + <type>text</type> + <notnull>true</notnull> + <length>256</length> + </field> + <field> + <name>name</name> + <type>text</type> + <notnull>true</notnull> + <length>256</length> + </field> + <field> + <name>checks</name> + <type>clob</type> + <notnull>false</notnull> + </field> + <field> + <name>operation</name> + <type>clob</type> + <notnull>false</notnull> + </field> + </declaration> + </table> +</database> diff --git a/apps/workflowengine/appinfo/info.xml b/apps/workflowengine/appinfo/info.xml new file mode 100644 index 0000000000000000000000000000000000000000..066589c6618a8282cd737990e74a2e7b60f0410f --- /dev/null +++ b/apps/workflowengine/appinfo/info.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<info> + <id>workflowengine</id> + <name>Files Workflow Engine</name> + <description></description> + <licence>AGPL</licence> + <author>Morris Jobke</author> + <version>1.0.0</version> + <namespace>WorkflowEngine</namespace> + + <category>other</category> + <website>https://github.com/nextcloud/server</website> + <bugs>https://github.com/nextcloud/server/issues</bugs> + <repository type="git">https://github.com/nextcloud/server.git</repository> + + <types> + <filesystem/> + </types> + + <dependencies> + <owncloud min-version="9.2" max-version="9.2" /> + </dependencies> +</info> diff --git a/apps/workflowengine/appinfo/routes.php b/apps/workflowengine/appinfo/routes.php new file mode 100644 index 0000000000000000000000000000000000000000..69478b1715cb971fd6681cf178dd5f1d958bfa43 --- /dev/null +++ b/apps/workflowengine/appinfo/routes.php @@ -0,0 +1,30 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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/>. + * + */ + +return [ + 'routes' => [ + ['name' => 'flowOperations#getChecks', 'url' => '/checks', 'verb' => 'GET'], // TODO rm and do via js? + ['name' => 'flowOperations#getOperations', 'url' => '/operations', 'verb' => 'GET'], + ['name' => 'flowOperations#addOperation', 'url' => '/operations', 'verb' => 'POST'], + ['name' => 'flowOperations#updateOperation', 'url' => '/operations/{id}', 'verb' => 'PUT'], + ['name' => 'flowOperations#deleteOperation', 'url' => '/operations/{id}', 'verb' => 'DELETE'], + ] +]; diff --git a/apps/workflowengine/css/admin.css b/apps/workflowengine/css/admin.css new file mode 100644 index 0000000000000000000000000000000000000000..73ac448cd7b16d5bf4681d1a80ea611800b1bd70 --- /dev/null +++ b/apps/workflowengine/css/admin.css @@ -0,0 +1,43 @@ +.workflowengine .operation { + padding: 5px; + border-bottom: #eee 1px solid; + border-left: rgba(0,0,0,0) 1px solid; +} +.workflowengine .operation.modified { + border-left: rgb(255, 94, 32) 1px solid; +} +.workflowengine .operation button { + margin-bottom: 0; +} +.workflowengine .operation span.info { + padding: 7px; + color: #eee; +} +.workflowengine .rules .operation:nth-last-child(2) { + margin-bottom: 5px; +} + +.workflowengine .pull-right { + float: right +} + +.workflowengine .operation .msg { + border-radius: 3px; + margin: 3px 3px 3px 0; + padding: 5px; + transition: opacity .5s; +} + +.workflowengine .operation .button-delete, +.workflowengine .operation .button-delete-check { + opacity: 0.5; + padding: 7px; +} +.workflowengine .operation .button-delete:hover, +.workflowengine .operation .button-delete:focus, +.workflowengine .operation .button-delete-check:hover, +.workflowengine .operation .button-delete-check:focus { + opacity: 1; + cursor: pointer; +} + diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js new file mode 100644 index 0000000000000000000000000000000000000000..f51352c45e4aac93eef99f2b3bd622c2ce7c69be Binary files /dev/null and b/apps/workflowengine/js/admin.js differ diff --git a/apps/workflowengine/js/usergroupmembershipplugin.js b/apps/workflowengine/js/usergroupmembershipplugin.js new file mode 100644 index 0000000000000000000000000000000000000000..2a6068cda90f0ed086e2779947b2d34dca1e85f5 Binary files /dev/null and b/apps/workflowengine/js/usergroupmembershipplugin.js differ diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php new file mode 100644 index 0000000000000000000000000000000000000000..c196ecd955c598bf78718d69e9169861a074a0f3 --- /dev/null +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -0,0 +1,62 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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\AppInfo; + +use OCP\Util; +use OCP\WorkflowEngine\RegisterCheckEvent; + +class Application extends \OCP\AppFramework\App { + + public function __construct() { + parent::__construct('workflowengine'); + + $this->getContainer()->registerAlias('FlowOperationsController', 'OCA\WorkflowEngine\Controller\FlowOperations'); + } + + /** + * Register all hooks and listeners + */ + public function registerHooksAndListeners() { + $dispatcher = $this->getContainer()->getServer()->getEventDispatcher(); + $dispatcher->addListener( + 'OCP\WorkflowEngine\RegisterCheckEvent', + function(RegisterCheckEvent $event) { + $event->addCheck( + 'OCA\WorkflowEngine\Check\UserGroupMembership', + 'User group membership', + ['is', '!is'] + ); + }, + -100 + ); + + $dispatcher->addListener( + 'OCP\WorkflowEngine::loadAdditionalSettingScripts', + function() { + Util::addStyle('workflowengine', 'admin'); + Util::addScript('workflowengine', 'admin'); + Util::addScript('workflowengine', 'usergroupmembershipplugin'); + }, + -100 + ); + } +} diff --git a/apps/workflowengine/lib/Check/UserGroupMembership.php b/apps/workflowengine/lib/Check/UserGroupMembership.php new file mode 100644 index 0000000000000000000000000000000000000000..f437dbfc2d1d0b34356915f969d153dcc0e7d3b9 --- /dev/null +++ b/apps/workflowengine/lib/Check/UserGroupMembership.php @@ -0,0 +1,108 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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\Check; + + +use OCP\Files\Storage\IStorage; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use OCP\WorkflowEngine\ICheck; + +class UserGroupMembership implements ICheck { + + /** @var string */ + protected $cachedUser; + + /** @var string[] */ + protected $cachedGroupMemberships; + + /** @var IUserSession */ + protected $userSession; + + /** @var IGroupManager */ + protected $groupManager; + + /** + * @param IUserSession $userSession + * @param IGroupManager $groupManager + */ + public function __construct(IUserSession $userSession, IGroupManager $groupManager) { + $this->userSession = $userSession; + $this->groupManager = $groupManager; + } + + /** + * @param IStorage $storage + * @param string $path + */ + public function setFileInfo(IStorage $storage, $path) { + // A different path doesn't change group memberships, so nothing to do here. + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $user = $this->userSession->getUser(); + + if ($user instanceof IUser) { + $groupIds = $this->getUserGroups($user); + return ($operator === 'is') === in_array($value, $groupIds); + } else { + return $operator !== 'is'; + } + } + + + /** + * @param string $operator + * @param string $value + * @throws \UnexpectedValueException + */ + public function validateCheck($operator, $value) { + if (!in_array($operator, ['is', '!is'])) { + throw new \UnexpectedValueException('Invalid operator', 1); + } + + if (!$this->groupManager->groupExists($value)) { + throw new \UnexpectedValueException('Group does not exist', 2); + } + } + + /** + * @param IUser $user + * @return string[] + */ + protected function getUserGroups(IUser $user) { + $uid = $user->getUID(); + + if ($this->cachedUser !== $uid) { + $this->cachedUser = $uid; + $this->cachedGroupMemberships = $this->groupManager->getUserGroupIds($user); + } + + return $this->cachedGroupMemberships; + } +} diff --git a/apps/workflowengine/lib/Controller/FlowOperations.php b/apps/workflowengine/lib/Controller/FlowOperations.php new file mode 100644 index 0000000000000000000000000000000000000000..e0836c727a27b7ae12803bb0eb13d3948688f4be --- /dev/null +++ b/apps/workflowengine/lib/Controller/FlowOperations.php @@ -0,0 +1,141 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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\Manager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\WorkflowEngine\RegisterCheckEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +class FlowOperations extends Controller { + + /** @var Manager */ + protected $manager; + + /** @var EventDispatcherInterface */ + protected $dispatcher; + + /** + * @param IRequest $request + * @param Manager $manager + * @param EventDispatcherInterface $dispatcher + */ + public function __construct(IRequest $request, Manager $manager, EventDispatcherInterface $dispatcher) { + parent::__construct('workflowengine', $request); + $this->manager = $manager; + $this->dispatcher = $dispatcher; + } + + /** + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function getChecks() { + $event = new RegisterCheckEvent(); + $this->dispatcher->dispatch('OCP\WorkflowEngine\RegisterCheckEvent', $event); + + return new JSONResponse($event->getChecks()); + } + + /** + * @NoCSRFRequired + * + * @param string $class + * @return JSONResponse + */ + public function getOperations($class) { + $operations = $this->manager->getOperations($class); + + foreach ($operations as &$operation) { + $operation = $this->prepareOperation($operation); + } + + return new JSONResponse($operations); + } + + /** + * @param string $class + * @param string $name + * @param array[] $checks + * @param string $operation + * @return JSONResponse The added element + */ + public function addOperation($class, $name, $checks, $operation) { + try { + $operation = $this->manager->addOperation($class, $name, $checks, $operation); + $operation = $this->prepareOperation($operation); + return new JSONResponse($operation); + } catch (\UnexpectedValueException $e) { + return new JSONResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); + } + } + + /** + * @param int $id + * @param string $name + * @param array[] $checks + * @param string $operation + * @return JSONResponse The updated element + */ + public function updateOperation($id, $name, $checks, $operation) { + try { + $operation = $this->manager->updateOperation($id, $name, $checks, $operation); + $operation = $this->prepareOperation($operation); + return new JSONResponse($operation); + } catch (\UnexpectedValueException $e) { + return new JSONResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); + } + } + + /** + * @param int $id + * @return JSONResponse + */ + public function deleteOperation($id) { + $deleted = $this->manager->deleteOperation((int) $id); + return new JSONResponse($deleted); + } + + /** + * @param array $operation + * @return array + */ + protected function prepareOperation(array $operation) { + $checkIds = json_decode($operation['checks']); + $checks = $this->manager->getChecks($checkIds); + + $operation['checks'] = []; + foreach ($checks as $check) { + // Remove internal values + unset($check['id']); + unset($check['hash']); + + $operation['checks'][] = $check; + } + + return $operation; + } +} diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php new file mode 100644 index 0000000000000000000000000000000000000000..98c34e894ccd0cde28ecdddd5608d924fc697b25 --- /dev/null +++ b/apps/workflowengine/lib/Manager.php @@ -0,0 +1,306 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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; + + +use OCP\AppFramework\QueryException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Storage\IStorage; +use OCP\IDBConnection; +use OCP\IServerContainer; +use OCP\WorkflowEngine\ICheck; +use OCP\WorkflowEngine\IManager; + +class Manager implements IManager { + + /** @var IStorage */ + protected $storage; + + /** @var string */ + protected $path; + + /** @var array[] */ + protected $operations = []; + + /** @var array[] */ + protected $checks = []; + + /** @var IDBConnection */ + protected $connection; + + /** @var IServerContainer|\OC\Server */ + protected $container; + + /** + * @param IDBConnection $connection + * @param IServerContainer $container + */ + public function __construct(IDBConnection $connection, IServerContainer $container) { + $this->connection = $connection; + $this->container = $container; + } + + /** + * @inheritdoc + */ + public function setFileInfo(IStorage $storage, $path) { + $this->storage = $storage; + $this->path = $path; + } + + /** + * @inheritdoc + */ + public function getMatchingOperations($class, $returnFirstMatchingOperationOnly = true) { + $operations = $this->getOperations($class); + + $matches = []; + foreach ($operations as $operation) { + $checkIds = json_decode($operation['checks'], true); + $checks = $this->getChecks($checkIds); + + foreach ($checks as $check) { + if (!$this->check($check)) { + // Check did not match, continue with the next operation + continue 2; + } + } + + if ($returnFirstMatchingOperationOnly) { + return $operation; + } + $matches[] = $operation; + } + + return $matches; + } + + /** + * @param array $check + * @return bool + */ + protected function check(array $check) { + try { + $checkInstance = $this->container->query($check['class']); + } catch (QueryException $e) { + // Check does not exist, assume it matches. + return true; + } + + if ($checkInstance instanceof ICheck) { + $checkInstance->setFileInfo($this->storage, $this->path); + return $checkInstance->executeCheck($check['operator'], $check['value']); + } else { + // Check is invalid, assume it matches. + return true; + } + } + + /** + * @param string $class + * @return array[] + */ + public function getOperations($class) { + if (isset($this->operations[$class])) { + return $this->operations[$class]; + } + + $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]; + } + + /** + * @param int $id + * @return array + * @throws \UnexpectedValueException + */ + protected function getOperation($id) { + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from('flow_operations') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row) { + return $row; + } + + throw new \UnexpectedValueException('Operation does not exist'); + } + + /** + * @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) { + $checkIds = []; + foreach ($checks as $check) { + $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']); + } + + $query = $this->connection->getQueryBuilder(); + $query->insert('flow_operations') + ->values([ + 'class' => $query->createNamedParameter($class), + 'name' => $query->createNamedParameter($name), + 'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))), + 'operation' => $query->createNamedParameter($operation), + ]); + $query->execute(); + + $id = $query->getLastInsertId(); + return $this->getOperation($id); + } + + /** + * @param int $id + * @param string $name + * @param array[] $checks + * @param string $operation + * @return array The updated operation + * @throws \UnexpectedValueException + */ + public function updateOperation($id, $name, array $checks, $operation) { + $checkIds = []; + 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(); + + return $this->getOperation($id); + } + + /** + * @param int $id + * @return bool + * @throws \UnexpectedValueException + */ + public function deleteOperation($id) { + $query = $this->connection->getQueryBuilder(); + $query->delete('flow_operations') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + return (bool) $query->execute(); + } + + /** + * @param int[] $checkIds + * @return array[] + */ + public function getChecks(array $checkIds) { + $checkIds = array_map('intval', $checkIds); + + $checks = []; + foreach ($checkIds as $i => $checkId) { + if (isset($this->checks[$checkId])) { + $checks[$checkId] = $this->checks[$checkId]; + unset($checkIds[$i]); + } + } + + if (empty($checkIds)) { + return $checks; + } + + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from('flow_checks') + ->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY))); + $result = $query->execute(); + + $checks = []; + while ($row = $result->fetch()) { + $this->checks[(int) $row['id']] = $row; + $checks[(int) $row['id']] = $row; + } + $result->closeCursor(); + + // TODO What if a check is missing? Should we throw? + // As long as we only allow AND-concatenation of checks, a missing check + // is like a matching check, so it evaluates to true and therefor blocks + // access. So better save than sorry. + + return $checks; + } + + /** + * @param string $class + * @param string $operator + * @param string $value + * @return int Check unique ID + * @throws \UnexpectedValueException + */ + protected function addCheck($class, $operator, $value) { + /** @var ICheck $check */ + $check = $this->container->query($class); + $check->validateCheck($operator, $value); + + $hash = md5($class . '::' . $operator . '::' . $value); + + $query = $this->connection->getQueryBuilder(); + $query->select('id') + ->from('flow_checks') + ->where($query->expr()->eq('hash', $query->createNamedParameter($hash))); + $result = $query->execute(); + + if ($row = $result->fetch()) { + $result->closeCursor(); + return (int) $row['id']; + } + + $query = $this->connection->getQueryBuilder(); + $query->insert('flow_checks') + ->values([ + 'class' => $query->createNamedParameter($class), + 'operator' => $query->createNamedParameter($operator), + 'value' => $query->createNamedParameter($value), + 'hash' => $query->createNamedParameter($hash), + ]); + $query->execute(); + + return $query->getLastInsertId(); + } +} diff --git a/apps/workflowengine/templates/admin.php b/apps/workflowengine/templates/admin.php new file mode 100644 index 0000000000000000000000000000000000000000..86ecfe085569ec2ee9732bb1452c3f44c42831b5 --- /dev/null +++ b/apps/workflowengine/templates/admin.php @@ -0,0 +1,79 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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/>. + * + */ + +/** @var array $_ */ +/** @var OC_L10N $l */ +?> +<div id="workflowengine" class="section workflowengine"> + <h2 class="inlineblock"><?php p($_['heading']); ?></h2> + <script type="text/template" id="operations-template"> + <div class="operations"></div> + <button class="button-add-operation">Add operation</button> + </script> + + <script type="text/template" id="operation-template"> + <div class="operation{{#if hasChanged}} modified{{/if}}"> + <input type="text" class="operation-name" value="{{operation.name}}"> + {{! delete only makes sense if the operation is already saved }} + {{#if operation.id}} + <span class="button-delete pull-right icon-delete"></span> + {{/if}} + <span class="pull-right info">{{operation.class}} - ID: {{operation.id}} - operation: {{operation.operation}}</span> + + <div class="checks"> + {{#each operation.checks}} + <div class="check" data-id="{{@index}}"> + <select class="check-class"> + {{#each ../classes}} + <option value="{{class}}" {{selectItem class ../class}}>{{name}}</option> + {{/each}} + </select> + <select class="check-operator"> + {{#each (getOperators class)}} + <option value="{{this}}" {{selectItem this ../operator}}>{{this}}</option> + {{/each}} + </select> + <input type="text" class="check-value" value="{{value}}"> + <span class="button-delete-check pull-right icon-delete"></span> + </div> + {{/each}} + </div> + <button class="button-add">Add check</button> + {{#if hasChanged}} + {{! reset only makes sense if the operation is already saved }} + {{#if operation.id}} + <button class="button-reset pull-right">Reset</button> + {{/if}} + <button class="button-save pull-right">Save</button> + {{/if}} + {{#if saving}} + <span class="icon-loading-small pull-right"></span> + <span class="pull-right">Saving ...</span> + {{else}}{{#if message}} + <span class="msg pull-right {{#if errorMessage}}error{{else}}success{{/if}}"> + {{message}}{{#if errorMessage}} {{errorMessage}}{{/if}} + </span> + {{/if}}{{/if}} + </div> + </script> + + <div class="rules"><span class="icon-loading-small"></span> Loading ...</div> +</div> diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 487e025092bc81376fab22bfd897c432f7000bff..d397a2852ad06b98765d0f88c534a50121aa6ae5 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -295,6 +295,7 @@ Feature: provisioning | systemtags | | theming | | updatenotification | + | workflowengine | Scenario: get app info Given As an "admin" diff --git a/core/shipped.json b/core/shipped.json index da48d14dc0b2fff07a8e91b8f355d2ea061c65b6..8d3056eb9083389b5c8478097abf2052609d7a34 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -27,11 +27,13 @@ "updatenotification", "user_external", "user_ldap", - "user_saml" + "user_saml", + "workflowengine" ], "alwaysEnabled": [ "files", "dav", - "federatedfilesharing" + "federatedfilesharing", + "workflowengine" ] } diff --git a/lib/public/WorkflowEngine/ICheck.php b/lib/public/WorkflowEngine/ICheck.php new file mode 100644 index 0000000000000000000000000000000000000000..7e3d86caad9f30e806e70cdddb8c898556cd6879 --- /dev/null +++ b/lib/public/WorkflowEngine/ICheck.php @@ -0,0 +1,56 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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 OCP\WorkflowEngine; + + +use OCP\Files\Storage\IStorage; + +/** + * Interface ICheck + * + * @package OCP\WorkflowEngine + * @since 9.1 + */ +interface ICheck { + /** + * @param IStorage $storage + * @param string $path + * @since 9.1 + */ + public function setFileInfo(IStorage $storage, $path); + + /** + * @param string $operator + * @param string $value + * @return bool + * @since 9.1 + */ + public function executeCheck($operator, $value); + + /** + * @param string $operator + * @param string $value + * @throws \UnexpectedValueException + * @since 9.1 + */ + public function validateCheck($operator, $value); +} diff --git a/lib/public/WorkflowEngine/IManager.php b/lib/public/WorkflowEngine/IManager.php new file mode 100644 index 0000000000000000000000000000000000000000..e53a06ec9299fb929c4091aa93ac49336877fe37 --- /dev/null +++ b/lib/public/WorkflowEngine/IManager.php @@ -0,0 +1,48 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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 OCP\WorkflowEngine; + + +use OCP\Files\Storage\IStorage; + +/** + * Interface IManager + * + * @package OCP\WorkflowEngine + * @since 9.1 + */ +interface IManager { + /** + * @param IStorage $storage + * @param string $path + * @since 9.1 + */ + public function setFileInfo(IStorage $storage, $path); + + /** + * @param string $class + * @param bool $returnFirstMatchingOperationOnly + * @return array + * @since 9.1 + */ + public function getMatchingOperations($class, $returnFirstMatchingOperationOnly = true); +} diff --git a/lib/public/WorkflowEngine/RegisterCheckEvent.php b/lib/public/WorkflowEngine/RegisterCheckEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..e08aae5fbc0a0f6c05a0f1743324d5bbcd67994b --- /dev/null +++ b/lib/public/WorkflowEngine/RegisterCheckEvent.php @@ -0,0 +1,79 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.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 OCP\WorkflowEngine; + + +use Symfony\Component\EventDispatcher\Event; + +/** + * Class RegisterCheckEvent + * + * @package OCP\WorkflowEngine + * @since 9.1 + */ +class RegisterCheckEvent extends Event { + + /** @var array[] */ + protected $checks = []; + + /** + * @param string $class + * @param string $name + * @param string[] $operators + * @throws \OutOfBoundsException when the check class is already registered + * @throws \OutOfBoundsException when the provided information is invalid + * @since 9.1 + */ + public function addCheck($class, $name, array $operators) { + if (!is_string($class)) { + throw new \OutOfBoundsException('Given class name is not a string'); + } + + if (isset($this->checks[$class])) { + throw new \OutOfBoundsException('Duplicate check class "' . $class . '"'); + } + + if (!is_string($name)) { + throw new \OutOfBoundsException('Given check name is not a string'); + } + + foreach ($operators as $operator) { + if (!is_string($operator)) { + throw new \OutOfBoundsException('At least one of the operators is not a string'); + } + } + + $this->checks[$class] = [ + 'class' => $class, + 'name' => $name, + 'operators' => $operators, + ]; + } + + /** + * @return array[] + * @since 9.1 + */ + public function getChecks() { + return array_values($this->checks); + } +} diff --git a/tests/lib/App/ManagerTest.php b/tests/lib/App/ManagerTest.php index 2d4ec4968b00ac12f647a06c6a0a16621dfd59db..80754413fc81e74b542b68eea02e1a1ac45c481f 100644 --- a/tests/lib/App/ManagerTest.php +++ b/tests/lib/App/ManagerTest.php @@ -306,7 +306,7 @@ class ManagerTest extends TestCase { $this->appConfig->setValue('test1', 'enabled', 'yes'); $this->appConfig->setValue('test2', 'enabled', 'no'); $this->appConfig->setValue('test3', 'enabled', '["foo"]'); - $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3'], $this->manager->getInstalledApps()); + $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getInstalledApps()); } public function testGetAppsForUser() { @@ -320,7 +320,7 @@ class ManagerTest extends TestCase { $this->appConfig->setValue('test2', 'enabled', 'no'); $this->appConfig->setValue('test3', 'enabled', '["foo"]'); $this->appConfig->setValue('test4', 'enabled', '["asd"]'); - $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3'], $this->manager->getEnabledAppsForUser($user)); + $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getEnabledAppsForUser($user)); } public function testGetAppsNeedingUpgrade() { @@ -338,6 +338,7 @@ class ManagerTest extends TestCase { 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], 'test4' => ['id' => 'test4', 'version' => '3.0.0', 'requiremin' => '8.1.0'], 'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'], + 'workflowengine' => ['id' => 'workflowengine'], ]; $this->manager->expects($this->any()) @@ -378,6 +379,7 @@ class ManagerTest extends TestCase { 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], 'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'], + 'workflowengine' => ['id' => 'workflowengine'], ]; $this->manager->expects($this->any()) diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index 2e5b6f74ab7a542e33f53da624ac1e8d4222b9ef..d37b4a0f56a0a77bb311ae4616a763b0aad3a4eb 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -316,6 +316,7 @@ class AppTest extends \Test\TestCase { 'appforgroup12', 'dav', 'federatedfilesharing', + 'workflowengine', ), false ), @@ -330,6 +331,7 @@ class AppTest extends \Test\TestCase { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), false ), @@ -345,6 +347,7 @@ class AppTest extends \Test\TestCase { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), false ), @@ -360,6 +363,7 @@ class AppTest extends \Test\TestCase { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), false, ), @@ -375,6 +379,7 @@ class AppTest extends \Test\TestCase { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), true, ), @@ -452,11 +457,11 @@ class AppTest extends \Test\TestCase { ); $apps = \OC_App::getEnabledApps(); - $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing',), $apps); + $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps); // mock should not be called again here $apps = \OC_App::getEnabledApps(); - $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing',), $apps); + $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps); $this->restoreAppConfig(); \OC_User::setUserId(null);