!/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
--- /dev/null
+<?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();
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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'],
+ ]
+];
--- /dev/null
+.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;
+}
+
--- /dev/null
+/**
+ * @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/>.
+ *
+ */
+
+(function() {
+ Handlebars.registerHelper('selectItem', function(currentValue, itemValue) {
+ if(currentValue === itemValue) {
+ return 'selected=selected';
+ }
+
+ return "";
+ });
+
+ Handlebars.registerHelper('getOperators', function(classname) {
+ return OCA.WorkflowEngine.availableChecks
+ .getOperatorsByClassName(classname);
+ });
+
+ OCA.WorkflowEngine = OCA.WorkflowEngine || {};
+
+ /**
+ * 888b d888 888 888
+ * 8888b d8888 888 888
+ * 88888b.d88888 888 888
+ * 888Y88888P888 .d88b. .d88888 .d88b. 888 .d8888b
+ * 888 Y888P 888 d88""88b d88" 888 d8P Y8b 888 88K
+ * 888 Y8P 888 888 888 888 888 88888888 888 "Y8888b.
+ * 888 " 888 Y88..88P Y88b 888 Y8b. 888 X88
+ * 888 888 "Y88P" "Y88888 "Y8888 888 88888P'
+ */
+
+ /**
+ * @class OCA.WorkflowEngine.Operation
+ */
+ OCA.WorkflowEngine.Operation =
+ OC.Backbone.Model.extend({
+ defaults: {
+ 'class': 'OCA\\WorkflowEngine\\Operation',
+ 'name': '',
+ 'checks': [],
+ 'operation': ''
+ }
+ });
+
+ /**
+ * @class OCA.WorkflowEngine.AvailableCheck
+ */
+ OCA.WorkflowEngine.AvailableCheck =
+ OC.Backbone.Model.extend({});
+
+ /**
+ * .d8888b. 888 888 888 d8b
+ * d88P Y88b 888 888 888 Y8P
+ * 888 888 888 888 888
+ * 888 .d88b. 888 888 .d88b. .d8888b 888888 888 .d88b. 88888b. .d8888b
+ * 888 d88""88b 888 888 d8P Y8b d88P" 888 888 d88""88b 888 "88b 88K
+ * 888 888 888 888 888 888 88888888 888 888 888 888 888 888 888 "Y8888b.
+ * Y88b d88P Y88..88P 888 888 Y8b. Y88b. Y88b. 888 Y88..88P 888 888 X88
+ * "Y8888P" "Y88P" 888 888 "Y8888 "Y8888P "Y888 888 "Y88P" 888 888 88888P'
+ */
+
+ /**
+ * @class OCA.WorkflowEngine.OperationsCollection
+ *
+ * collection for all configurated operations
+ */
+ OCA.WorkflowEngine.OperationsCollection =
+ OC.Backbone.Collection.extend({
+ model: OCA.WorkflowEngine.Operation,
+ url: OC.generateUrl('apps/workflowengine/operations')
+ });
+
+ /**
+ * @class OCA.WorkflowEngine.AvailableChecksCollection
+ *
+ * collection for all available checks
+ */
+ OCA.WorkflowEngine.AvailableChecksCollection =
+ OC.Backbone.Collection.extend({
+ model: OCA.WorkflowEngine.AvailableCheck,
+ url: OC.generateUrl('apps/workflowengine/checks'),
+ getOperatorsByClassName: function(classname) {
+ return OCA.WorkflowEngine.availableChecks
+ .findWhere({'class': classname})
+ .get('operators');
+ }
+ });
+
+ /**
+ * 888 888 d8b
+ * 888 888 Y8P
+ * 888 888
+ * Y88b d88P 888 .d88b. 888 888 888 .d8888b
+ * Y88b d88P 888 d8P Y8b 888 888 888 88K
+ * Y88o88P 888 88888888 888 888 888 "Y8888b.
+ * Y888P 888 Y8b. Y88b 888 d88P X88
+ * Y8P 888 "Y8888 "Y8888888P" 88888P'
+ */
+
+ /**
+ * @class OCA.WorkflowEngine.TemplateView
+ *
+ * a generic template that handles the Handlebars template compile step
+ * in a method called "template()"
+ */
+ OCA.WorkflowEngine.TemplateView =
+ OC.Backbone.View.extend({
+ _template: null,
+ template: function(vars) {
+ if (!this._template) {
+ this._template = Handlebars.compile($(this.templateId).html());
+ }
+ return this._template(vars);
+ }
+ });
+
+ /**
+ * @class OCA.WorkflowEngine.OperationView
+ *
+ * this creates the view for a single operation
+ */
+ OCA.WorkflowEngine.OperationView =
+ OCA.WorkflowEngine.TemplateView.extend({
+ templateId: '#operation-template',
+ events: {
+ 'change .check-class': 'checkChanged',
+ 'change .check-operator': 'checkChanged',
+ 'change .check-value': 'checkChanged',
+ 'change .operation-name': 'operationChanged',
+ 'click .button-reset': 'reset',
+ 'click .button-save': 'save',
+ 'click .button-add': 'add',
+ 'click .button-delete': 'delete',
+ 'click .button-delete-check': 'deleteCheck'
+ },
+ originalModel: null,
+ hasChanged: false,
+ message: '',
+ errorMessage: '',
+ saving: false,
+ plugins: [],
+ initialize: function() {
+ // this creates a new copy of the object to definitely have a new reference and being able to reset the model
+ this.originalModel = JSON.parse(JSON.stringify(this.model));
+ this.model.on('change', function(){
+ console.log('model changed');
+ this.hasChanged = true;
+ this.render();
+ }, this);
+
+ if (this.model.get('id') === undefined) {
+ this.hasChanged = true;
+ }
+
+ this.plugins = OC.Plugins.getPlugins('OCA.WorkflowEngine.CheckPlugins');
+ _.each(this.plugins, function(plugin) {
+ if (_.isFunction(plugin.initialize)) {
+ plugin.initialize();
+ }
+ });
+ },
+ delete: function() {
+ this.model.destroy();
+ this.remove();
+ },
+ reset: function() {
+ this.hasChanged = false;
+ // silent is need to not trigger the change event which resets the hasChanged attribute
+ this.model.set(this.originalModel, {silent: true});
+ this.render();
+ },
+ save: function() {
+ var success = function(model, response, options) {
+ this.saving = false;
+ this.originalModel = JSON.parse(JSON.stringify(this.model));
+
+ this.message = t('workflowengine', 'Successfully saved');
+ this.errorMessage = '';
+ this.render();
+ };
+ var error = function(model, response, options) {
+ this.saving = false;
+ this.hasChanged = true;
+
+ this.message = t('workflowengine', 'Saving failed:');
+ this.errorMessage = response.responseText;
+ this.render();
+ };
+ this.hasChanged = false;
+ this.saving = true;
+ this.render();
+ this.model.save(null, {success: success, error: error, context: this});
+ },
+ add: function() {
+ var checks = _.clone(this.model.get('checks')),
+ classname = OCA.WorkflowEngine.availableChecks.at(0).get('class'),
+ operators = OCA.WorkflowEngine.availableChecks
+ .getOperatorsByClassName(classname);
+
+ checks.push({
+ 'class': classname,
+ 'operator': operators[0],
+ 'value': ''
+ });
+ this.model.set({'checks': checks});
+ },
+ checkChanged: function(event) {
+ var value = event.target.value,
+ id = $(event.target.parentElement).data('id'),
+ // this creates a new copy of the object to definitely have a new reference
+ checks = JSON.parse(JSON.stringify(this.model.get('checks'))),
+ key = null;
+
+ for (var i = 0; i < event.target.classList.length; i++) {
+ var className = event.target.classList[i];
+ if (className.substr(0, 'check-'.length) === 'check-') {
+ key = className.substr('check-'.length);
+ break;
+ }
+ }
+
+ if (key === null) {
+ console.warn('checkChanged triggered but element doesn\'t have any "check-" class');
+ return;
+ }
+
+ if (!_.has(checks[id], key)) {
+ console.warn('key "' + key + '" is not available in check', check);
+ return;
+ }
+
+ checks[id][key] = value;
+ // if the class is changed most likely also the operators have changed
+ // with this we set the operator to the first possible operator
+ if (key === 'class') {
+ var operators = OCA.WorkflowEngine.availableChecks
+ .getOperatorsByClassName(value);
+ checks[id]['operator'] = operators[0];
+ }
+ // model change will trigger render
+ this.model.set({'checks': checks});
+ },
+ deleteCheck: function() {
+ console.log(arguments);
+ var id = $(event.target.parentElement).data('id'),
+ checks = JSON.parse(JSON.stringify(this.model.get('checks')));
+
+ // splice removes 1 element at index `id`
+ checks.splice(id, 1);
+ // model change will trigger render
+ this.model.set({'checks': checks});
+ },
+ operationChanged: function(event) {
+ var value = event.target.value,
+ key = null;
+
+ for (var i = 0; i < event.target.classList.length; i++) {
+ var className = event.target.classList[i];
+ if (className.substr(0, 'operation-'.length) === 'operation-') {
+ key = className.substr('operation-'.length);
+ break;
+ }
+ }
+
+ if (key === null) {
+ console.warn('operationChanged triggered but element doesn\'t have any "operation-" class');
+ return;
+ }
+
+ if (key !== 'name') {
+ console.warn('key "' + key + '" is no valid attribute');
+ return;
+ }
+
+ // model change will trigger render
+ this.model.set(key, value);
+ },
+ render: function() {
+ this.$el.html(this.template({
+ operation: this.model.toJSON(),
+ classes: OCA.WorkflowEngine.availableChecks.toJSON(),
+ hasChanged: this.hasChanged,
+ message: this.message,
+ errorMessage: this.errorMessage,
+ saving: this.saving
+ }));
+
+ var checks = this.model.get('checks');
+ _.each(this.$el.find('.check'), function(element){
+ var $element = $(element),
+ id = $element.data('id'),
+ check = checks[id],
+ valueElement = $element.find('.check-value').first();
+
+ _.each(this.plugins, function(plugin) {
+ if (_.isFunction(plugin.render)) {
+ plugin.render(valueElement, check['class'], check['value']);
+ }
+ });
+ }, this);
+
+ if (this.message !== '') {
+ // hide success messages after some time
+ _.delay(function(elements){
+ $(elements).css('opacity', 0);
+ }, 7000, this.$el.find('.msg.success'));
+ this.message = '';
+ }
+
+ }
+ });
+
+ /**
+ * @class OCA.WorkflowEngine.OperationsView
+ *
+ * this creates the view for configured operations
+ */
+ OCA.WorkflowEngine.OperationsView =
+ OCA.WorkflowEngine.TemplateView.extend({
+ templateId: '#operations-template',
+ events: {
+ 'click .button-add-operation': 'add'
+ },
+ initialize: function() {
+ this._initialize('OCA\\WorkflowEngine\\Operation');
+ },
+ _initialize: function(classname) {
+ var data = {};
+ if (this.operationsClass !== null) {
+ data['class'] = this.operationsClass;
+ }
+ this.collection.fetch({data: {
+ 'class': classname
+ }});
+ this.collection.once('sync', this.render, this);
+ },
+ add: function() {
+ var operation = new OCA.WorkflowEngine.Operation();
+ this.collection.add(operation);
+ this.renderOperation(operation);
+ },
+ renderOperation: function(operation){
+ console.log(operation);
+ var subView = new OCA.WorkflowEngine.OperationView({
+ model: operation
+ }),
+ operationsElement = this.$el.find('.operations');
+ operationsElement.append(subView.$el);
+ subView.render();
+ },
+ render: function() {
+ this.$el.html(this.template());
+ this.collection.each(this.renderOperation, this);
+ }
+ });
+})();
--- /dev/null
+/**
+ * @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/>.
+ *
+ */
+
+(function() {
+
+ OCA.WorkflowEngine = OCA.WorkflowEngine || {};
+ OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
+
+ OCA.WorkflowEngine.Plugins.UserGroupMembershipPlugin = {
+ render: function(element, classname, value) {
+ if (classname !== 'OCA\\WorkflowEngine\\Check\\UserGroupMembership') {
+ return;
+ }
+
+ $(element).css('width', '400px');
+
+ $(element).select2({
+ ajax: {
+ url: OC.generateUrl('settings/users/groups'),
+ dataType: 'json',
+ quietMillis: 100,
+ data: function (term) {
+ return {
+ pattern: term, //search term
+ filterGroups: true,
+ sortGroups: 2 // by groupname
+ };
+ },
+ results: function (response) {
+ // TODO improve error case
+ if (response.data === undefined) {
+ console.error('Failure happened', response);
+ return;
+ }
+
+ var results = [];
+
+ // add admin groups
+ $.each(response.data.adminGroups, function(id, group) {
+ results.push({ id: group.id });
+ });
+ // add groups
+ $.each(response.data.groups, function(id, group) {
+ results.push({ id: group.id });
+ });
+
+ // TODO once limit and offset is implemented for groups we should paginate the search results
+ return {
+ results: results,
+ more: false
+ };
+ }
+ },
+ initSelection: function (element, callback) {
+ callback({id: element.val()});
+ },
+ formatResult: function (element) {
+ return '<span>' + escapeHTML(element.id) + '</span>';
+ },
+ formatSelection: function (element) {
+ return '<span title="'+escapeHTML(element.id)+'">'+escapeHTML(element.id)+'</span>';
+ }
+ });
+ }
+ };
+})();
+
+OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.UserGroupMembershipPlugin);
--- /dev/null
+<?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
+ );
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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();
+ }
+}
"updatenotification",
"user_external",
"user_ldap",
- "user_saml"
+ "user_saml",
+ "workflowengine"
],
"alwaysEnabled": [
"files",
"dav",
- "federatedfilesharing"
+ "federatedfilesharing",
+ "workflowengine"
]
}
--- /dev/null
+<?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);
+}
--- /dev/null
+<?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);
+}
--- /dev/null
+<?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);
+ }
+}
$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() {
$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() {
'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())
'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())