From bc6c05afb6abbd4390c076200738e3538a3c8151 Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Tue, 26 Jul 2016 11:16:34 +0200 Subject: [PATCH 01/32] Add workflowengine --- .gitignore | 1 + apps/workflowengine/appinfo/app.php | 23 ++ apps/workflowengine/appinfo/database.xml | 90 +++++ apps/workflowengine/appinfo/info.xml | 23 ++ apps/workflowengine/appinfo/routes.php | 30 ++ apps/workflowengine/css/admin.css | 43 ++ apps/workflowengine/js/admin.js | 372 ++++++++++++++++++ .../js/usergroupmembershipplugin.js | 85 ++++ .../lib/AppInfo/Application.php | 62 +++ .../lib/Check/UserGroupMembership.php | 108 +++++ .../lib/Controller/FlowOperations.php | 141 +++++++ apps/workflowengine/lib/Manager.php | 306 ++++++++++++++ core/shipped.json | 6 +- lib/public/WorkflowEngine/ICheck.php | 56 +++ lib/public/WorkflowEngine/IManager.php | 48 +++ .../WorkflowEngine/RegisterCheckEvent.php | 79 ++++ tests/lib/App/ManagerTest.php | 6 +- 17 files changed, 1475 insertions(+), 4 deletions(-) create mode 100644 apps/workflowengine/appinfo/app.php create mode 100644 apps/workflowengine/appinfo/database.xml create mode 100644 apps/workflowengine/appinfo/info.xml create mode 100644 apps/workflowengine/appinfo/routes.php create mode 100644 apps/workflowengine/css/admin.css create mode 100644 apps/workflowengine/js/admin.js create mode 100644 apps/workflowengine/js/usergroupmembershipplugin.js create mode 100644 apps/workflowengine/lib/AppInfo/Application.php create mode 100644 apps/workflowengine/lib/Check/UserGroupMembership.php create mode 100644 apps/workflowengine/lib/Controller/FlowOperations.php create mode 100644 apps/workflowengine/lib/Manager.php create mode 100644 lib/public/WorkflowEngine/ICheck.php create mode 100644 lib/public/WorkflowEngine/IManager.php create mode 100644 lib/public/WorkflowEngine/RegisterCheckEvent.php diff --git a/.gitignore b/.gitignore index f60dda513e4bb..63a34beb978d4 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 0000000000000..f6f22ce948871 --- /dev/null +++ b/apps/workflowengine/appinfo/app.php @@ -0,0 +1,23 @@ + + * + * @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 . + * + */ + +$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 0000000000000..b67a41faed20a --- /dev/null +++ b/apps/workflowengine/appinfo/database.xml @@ -0,0 +1,90 @@ + + + *dbname* + true + false + utf8 + + + *dbprefix*flow_checks + + + id + integer + 0 + true + 1 + 4 + + + + class + text + true + 256 + + + operator + text + true + 16 + + + value + clob + false + + + hash + text + true + 32 + + + + flow_unique_hash + true + + hash + + + +
+ + + *dbprefix*flow_operations + + + id + integer + 0 + true + 1 + 4 + + + + class + text + true + 256 + + + name + text + true + 256 + + + checks + clob + false + + + operation + clob + false + + +
+
diff --git a/apps/workflowengine/appinfo/info.xml b/apps/workflowengine/appinfo/info.xml new file mode 100644 index 0000000000000..066589c6618a8 --- /dev/null +++ b/apps/workflowengine/appinfo/info.xml @@ -0,0 +1,23 @@ + + + workflowengine + Files Workflow Engine + + AGPL + Morris Jobke + 1.0.0 + WorkflowEngine + + other + https://github.com/nextcloud/server + https://github.com/nextcloud/server/issues + https://github.com/nextcloud/server.git + + + + + + + + + diff --git a/apps/workflowengine/appinfo/routes.php b/apps/workflowengine/appinfo/routes.php new file mode 100644 index 0000000000000..69478b1715cb9 --- /dev/null +++ b/apps/workflowengine/appinfo/routes.php @@ -0,0 +1,30 @@ + + * + * @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 . + * + */ + +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 0000000000000..73ac448cd7b16 --- /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 0000000000000..a29ad82ab8996 --- /dev/null +++ b/apps/workflowengine/js/admin.js @@ -0,0 +1,372 @@ +/** + * @copyright Copyright (c) 2016 Morris Jobke + * + * @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 . + * + */ + +(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); + } + }); +})(); diff --git a/apps/workflowengine/js/usergroupmembershipplugin.js b/apps/workflowengine/js/usergroupmembershipplugin.js new file mode 100644 index 0000000000000..2a6068cda90f0 --- /dev/null +++ b/apps/workflowengine/js/usergroupmembershipplugin.js @@ -0,0 +1,85 @@ +/** + * @copyright Copyright (c) 2016 Morris Jobke + * + * @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 . + * + */ + +(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 '' + escapeHTML(element.id) + ''; + }, + formatSelection: function (element) { + return ''+escapeHTML(element.id)+''; + } + }); + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.UserGroupMembershipPlugin); diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php new file mode 100644 index 0000000000000..c196ecd955c59 --- /dev/null +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -0,0 +1,62 @@ + + * + * @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 . + * + */ + +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 0000000000000..f437dbfc2d1d0 --- /dev/null +++ b/apps/workflowengine/lib/Check/UserGroupMembership.php @@ -0,0 +1,108 @@ + + * + * @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 . + * + */ + +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 0000000000000..e0836c727a27b --- /dev/null +++ b/apps/workflowengine/lib/Controller/FlowOperations.php @@ -0,0 +1,141 @@ + + * + * @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 . + * + */ + +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 0000000000000..98c34e894ccd0 --- /dev/null +++ b/apps/workflowengine/lib/Manager.php @@ -0,0 +1,306 @@ + + * + * @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 . + * + */ + +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/core/shipped.json b/core/shipped.json index da48d14dc0b2f..8d3056eb90833 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 0000000000000..7e3d86caad9f3 --- /dev/null +++ b/lib/public/WorkflowEngine/ICheck.php @@ -0,0 +1,56 @@ + + * + * @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 . + * + */ + +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 0000000000000..e53a06ec9299f --- /dev/null +++ b/lib/public/WorkflowEngine/IManager.php @@ -0,0 +1,48 @@ + + * + * @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 . + * + */ + +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 0000000000000..e08aae5fbc0a0 --- /dev/null +++ b/lib/public/WorkflowEngine/RegisterCheckEvent.php @@ -0,0 +1,79 @@ + + * + * @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 . + * + */ + +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 2d4ec4968b00a..80754413fc81e 100644 --- a/tests/lib/App/ManagerTest.php +++ b/tests/lib/App/ManagerTest.php @@ -306,7 +306,7 @@ public function testGetInstalledApps() { $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 @@ 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() { @@ -338,6 +338,7 @@ 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()) @@ -378,6 +379,7 @@ public function testGetIncompatibleApps() { '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()) From 6eabcf134f728c652028a8e417ff444297971cf0 Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Tue, 26 Jul 2016 12:03:16 +0200 Subject: [PATCH 02/32] create an operation based on the correct model --- apps/workflowengine/js/admin.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index a29ad82ab8996..f51352c45e4aa 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -341,18 +341,13 @@ 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); + var operation = this.collection.create(); this.renderOperation(operation); }, renderOperation: function(operation){ From 497954ca34cc0ed68b13d7bb690c8251ce601311 Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Tue, 26 Jul 2016 14:31:16 +0200 Subject: [PATCH 03/32] Move template to workflowengine app --- apps/workflowengine/templates/admin.php | 79 +++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 apps/workflowengine/templates/admin.php diff --git a/apps/workflowengine/templates/admin.php b/apps/workflowengine/templates/admin.php new file mode 100644 index 0000000000000..86ecfe085569e --- /dev/null +++ b/apps/workflowengine/templates/admin.php @@ -0,0 +1,79 @@ + + * + * @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 . + * + */ + +/** @var array $_ */ +/** @var OC_L10N $l */ +?> +
+

+ + + + +
Loading ...
+
From 80eeedc0057896cf8f1353ff5ba7a7d9ef70d647 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 26 Jul 2016 16:39:04 +0200 Subject: [PATCH 04/32] Add app to provisioning output --- build/integration/features/provisioning-v1.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 135c67dc3a675..1ef2b490201f5 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" From b2e4a8c0d3eb2eea8e6b1e80c083a29cc3bca600 Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Tue, 26 Jul 2016 16:43:46 +0200 Subject: [PATCH 05/32] fix unit tests --- tests/lib/AppTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index 2e5b6f74ab7a5..01ec1ade40355 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -452,11 +452,11 @@ public function testEnabledAppsCache() { ); $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); From 01ec62e3d2491cb999ec6f6b1485d1a5df1c501a Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Wed, 27 Jul 2016 08:36:03 +0200 Subject: [PATCH 06/32] fix enabled apps tests --- tests/lib/AppTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index 01ec1ade40355..d37b4a0f56a0a 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -316,6 +316,7 @@ function appConfigValuesProvider() { 'appforgroup12', 'dav', 'federatedfilesharing', + 'workflowengine', ), false ), @@ -330,6 +331,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), false ), @@ -345,6 +347,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), false ), @@ -360,6 +363,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), false, ), @@ -375,6 +379,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'workflowengine', ), true, ), From 155e4ced3f3955e91d6e4cd41bec3c8d911c0724 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 26 Jul 2016 17:34:05 +0200 Subject: [PATCH 07/32] Generate the checks list in JS --- apps/workflowengine/js/admin.js | 74 ++++++++++--------- .../js/usergroupmembershipplugin.js | 10 +++ apps/workflowengine/templates/admin.php | 2 +- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index f51352c45e4aa..590274b87700e 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -28,11 +28,27 @@ }); Handlebars.registerHelper('getOperators', function(classname) { - return OCA.WorkflowEngine.availableChecks - .getOperatorsByClassName(classname); + var check = OCA.WorkflowEngine.getCheckByClass(classname); + if (!_.isUndefined(check)) { + return check['operators']; + } + return []; }); - OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine = OCA.WorkflowEngine || { + availablePlugins: [], + availableChecks: [], + + getCheckByClass: function(className) { + var length = OCA.WorkflowEngine.availableChecks.length; + for (var i = 0; i < length; i++) { + if (OCA.WorkflowEngine.availableChecks[i]['class'] === className) { + return OCA.WorkflowEngine.availableChecks[i]; + } + } + return undefined; + } + }; /** * 888b d888 888 888 @@ -86,22 +102,6 @@ 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 @@ -154,7 +154,6 @@ 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)); @@ -167,13 +166,6 @@ 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(); @@ -209,9 +201,8 @@ }, add: function() { var checks = _.clone(this.model.get('checks')), - classname = OCA.WorkflowEngine.availableChecks.at(0).get('class'), - operators = OCA.WorkflowEngine.availableChecks - .getOperatorsByClassName(classname); + classname = OCA.WorkflowEngine.availableChecks[0]['class'], + operators = OCA.WorkflowEngine.availableChecks[0]['operators']; checks.push({ 'class': classname, @@ -249,9 +240,10 @@ // 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]; + var check = OCA.WorkflowEngine.getCheckByClass(value); + if (!_.isUndefined(check)) { + checks[id]['operator'] = check['operators'][0]; + } } // model change will trigger render this.model.set({'checks': checks}); @@ -294,7 +286,7 @@ render: function() { this.$el.html(this.template({ operation: this.model.toJSON(), - classes: OCA.WorkflowEngine.availableChecks.toJSON(), + classes: OCA.WorkflowEngine.availableChecks, hasChanged: this.hasChanged, message: this.message, errorMessage: this.errorMessage, @@ -308,7 +300,7 @@ check = checks[id], valueElement = $element.find('.check-value').first(); - _.each(this.plugins, function(plugin) { + _.each(OCA.WorkflowEngine.availablePlugins, function(plugin) { if (_.isFunction(plugin.render)) { plugin.render(valueElement, check['class'], check['value']); } @@ -334,6 +326,8 @@ OCA.WorkflowEngine.OperationsView = OCA.WorkflowEngine.TemplateView.extend({ templateId: '#operations-template', + collection: null, + $el: null, events: { 'click .button-add-operation': 'add' }, @@ -341,6 +335,16 @@ this._initialize('OCA\\WorkflowEngine\\Operation'); }, _initialize: function(classname) { + OCA.WorkflowEngine.availablePlugins = OC.Plugins.getPlugins('OCA.WorkflowEngine.CheckPlugins'); + _.each(OCA.WorkflowEngine.availablePlugins, function(plugin) { + if (_.isFunction(plugin.initialize)) { + plugin.initialize(); + } + if (_.isFunction(plugin.getCheck)) { + OCA.WorkflowEngine.availableChecks.push(plugin.getCheck()); + } + }); + this.collection.fetch({data: { 'class': classname }}); diff --git a/apps/workflowengine/js/usergroupmembershipplugin.js b/apps/workflowengine/js/usergroupmembershipplugin.js index 2a6068cda90f0..528a7bd3e3d05 100644 --- a/apps/workflowengine/js/usergroupmembershipplugin.js +++ b/apps/workflowengine/js/usergroupmembershipplugin.js @@ -24,6 +24,16 @@ OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; OCA.WorkflowEngine.Plugins.UserGroupMembershipPlugin = { + getCheck: function() { + return { + 'class': 'OCA\\WorkflowEngine\\Check\\UserGroupMembership', + 'name': t('workflowengine', 'User group membership'), + 'operators': [ + {'operator': 'is', 'name': t('workflowengine', 'is member of')}, + {'operator': '!is', 'name': t('workflowengine', 'is not member of')} + ] + }; + }, render: function(element, classname, value) { if (classname !== 'OCA\\WorkflowEngine\\Check\\UserGroupMembership') { return; diff --git a/apps/workflowengine/templates/admin.php b/apps/workflowengine/templates/admin.php index 86ecfe085569e..cb6895f80c12e 100644 --- a/apps/workflowengine/templates/admin.php +++ b/apps/workflowengine/templates/admin.php @@ -48,7 +48,7 @@ From 6a212893789c172335cdc46d69abcbd36d18d9c2 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 08:54:34 +0200 Subject: [PATCH 08/32] Remove php side of check registration --- apps/workflowengine/appinfo/routes.php | 1 - .../lib/AppInfo/Application.php | 12 --- .../lib/Controller/FlowOperations.php | 21 +---- apps/workflowengine/lib/Manager.php | 14 ++-- .../WorkflowEngine/RegisterCheckEvent.php | 79 ------------------- 5 files changed, 9 insertions(+), 118 deletions(-) delete mode 100644 lib/public/WorkflowEngine/RegisterCheckEvent.php diff --git a/apps/workflowengine/appinfo/routes.php b/apps/workflowengine/appinfo/routes.php index 69478b1715cb9..b8c9ae1c236a2 100644 --- a/apps/workflowengine/appinfo/routes.php +++ b/apps/workflowengine/appinfo/routes.php @@ -21,7 +21,6 @@ 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'], diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index c196ecd955c59..8433950304726 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -37,18 +37,6 @@ public function __construct() { */ 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() { diff --git a/apps/workflowengine/lib/Controller/FlowOperations.php b/apps/workflowengine/lib/Controller/FlowOperations.php index e0836c727a27b..94b8b9ddc79bf 100644 --- a/apps/workflowengine/lib/Controller/FlowOperations.php +++ b/apps/workflowengine/lib/Controller/FlowOperations.php @@ -26,38 +26,19 @@ 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) { + public function __construct(IRequest $request, Manager $manager) { 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()); } /** diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php index 98c34e894ccd0..f2a04dfb0fa83 100644 --- a/apps/workflowengine/lib/Manager.php +++ b/apps/workflowengine/lib/Manager.php @@ -110,8 +110,8 @@ protected function check(array $check) { $checkInstance->setFileInfo($this->storage, $this->path); return $checkInstance->executeCheck($check['operator'], $check['value']); } else { - // Check is invalid, assume it matches. - return true; + // Check is invalid + throw new \RuntimeException('Check ' . htmlspecialchars($check['class']) . ' is invalid or does not exist'); } } @@ -258,10 +258,12 @@ public function getChecks(array $checkIds) { } $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. + $checkIds = array_diff($checkIds, array_keys($checks)); + + if (!empty($checkIds)) { + $missingCheck = array_pop($checkIds); + throw new \RuntimeException('Check #' . htmlspecialchars($missingCheck) . ' is invalid or does not exist'); + } return $checks; } diff --git a/lib/public/WorkflowEngine/RegisterCheckEvent.php b/lib/public/WorkflowEngine/RegisterCheckEvent.php deleted file mode 100644 index e08aae5fbc0a0..0000000000000 --- a/lib/public/WorkflowEngine/RegisterCheckEvent.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * @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 . - * - */ - -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); - } -} From 34f46c8e7f0831664e6da3c5063738797068ddcc Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 10:38:03 +0200 Subject: [PATCH 09/32] Fix morris comments --- apps/workflowengine/js/admin.js | 7 ++----- apps/workflowengine/templates/admin.php | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index 590274b87700e..394c580fbb505 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -35,7 +35,7 @@ return []; }); - OCA.WorkflowEngine = OCA.WorkflowEngine || { + OCA.WorkflowEngine = _.extend(OCA.WorkflowEngine || {}, { availablePlugins: [], availableChecks: [], @@ -48,7 +48,7 @@ } return undefined; } - }; + }); /** * 888b d888 888 888 @@ -337,9 +337,6 @@ _initialize: function(classname) { OCA.WorkflowEngine.availablePlugins = OC.Plugins.getPlugins('OCA.WorkflowEngine.CheckPlugins'); _.each(OCA.WorkflowEngine.availablePlugins, function(plugin) { - if (_.isFunction(plugin.initialize)) { - plugin.initialize(); - } if (_.isFunction(plugin.getCheck)) { OCA.WorkflowEngine.availableChecks.push(plugin.getCheck()); } diff --git a/apps/workflowengine/templates/admin.php b/apps/workflowengine/templates/admin.php index cb6895f80c12e..c876f30c4c955 100644 --- a/apps/workflowengine/templates/admin.php +++ b/apps/workflowengine/templates/admin.php @@ -48,7 +48,7 @@ From 0ebc3bbc9f90d5a3605cea60f13407058bf2b2a8 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 12:29:30 +0200 Subject: [PATCH 10/32] Fix default value of operator --- apps/workflowengine/js/admin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index 394c580fbb505..48d1592b4571a 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -206,7 +206,7 @@ checks.push({ 'class': classname, - 'operator': operators[0], + 'operator': operators[0]['operator'], 'value': '' }); this.model.set({'checks': checks}); @@ -242,7 +242,7 @@ if (key === 'class') { var check = OCA.WorkflowEngine.getCheckByClass(value); if (!_.isUndefined(check)) { - checks[id]['operator'] = check['operators'][0]; + checks[id]['operator'] = check['operators'][0]['operator']; } } // model change will trigger render From ec8ec17cf4684e0a3abca8243bcb59957fe86f62 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 12:29:49 +0200 Subject: [PATCH 11/32] Allow to reuse the template --- apps/workflowengine/templates/admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/workflowengine/templates/admin.php b/apps/workflowengine/templates/admin.php index c876f30c4c955..9e01f8359fa92 100644 --- a/apps/workflowengine/templates/admin.php +++ b/apps/workflowengine/templates/admin.php @@ -22,7 +22,7 @@ /** @var array $_ */ /** @var OC_L10N $l */ ?> -
+

-
Loading ...
+
t('Loading…')); ?>
From c425a677b2c8a31b2f946159390a5b887a5dc31e Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Wed, 3 Aug 2016 10:29:06 +0200 Subject: [PATCH 15/32] Add workflowengine to check-code call --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 15413d3413eea..fbbff9fc16e60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ install: script: - sh -c "if [ '$TC' = 'syntax' ]; then composer install && lib/composer/bin/parallel-lint --exclude lib/composer/jakub-onderka/ --exclude 3rdparty/symfony/polyfill-php70/Resources/stubs/ --exclude 3rdparty/patchwork/utf8/src/Patchwork/Utf8/Bootup/ --exclude 3rdparty/paragonie/random_compat/lib/ --exclude lib/composer/composer/autoload_static.php --exclude 3rdparty/composer/autoload_static.php .; fi" - - sh -c "if [ '$TC' = 'app:check-code' ]; then ./occ app:check-code admin_audit; ./occ app:check-code comments; ./occ app:check-code federation; fi" + - sh -c "if [ '$TC' = 'app:check-code' ]; then ./occ app:check-code admin_audit; ./occ app:check-code comments; ./occ app:check-code federation; ./occ app:check-code workflowengine; fi" - sh -c "if [ '$TEST_DAV' != '1' ]; then echo \"Not testing DAV\"; fi" - sh -c "if [ '$TEST_DAV' = '1' ]; then echo \"Testing DAV\"; fi" From df3ca567adb9cb4d569e13f47f2dce556f0b9b50 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 15:17:09 +0200 Subject: [PATCH 16/32] Make sure each plugin is only added once --- apps/workflowengine/js/admin.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index 48d1592b4571a..b9f0744732ffe 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -74,12 +74,6 @@ } }); - /** - * @class OCA.WorkflowEngine.AvailableCheck - */ - OCA.WorkflowEngine.AvailableCheck = - OC.Backbone.Model.extend({}); - /** * .d8888b. 888 888 888 d8b * d88P Y88b 888 888 888 Y8P @@ -335,12 +329,14 @@ this._initialize('OCA\\WorkflowEngine\\Operation'); }, _initialize: function(classname) { - OCA.WorkflowEngine.availablePlugins = OC.Plugins.getPlugins('OCA.WorkflowEngine.CheckPlugins'); - _.each(OCA.WorkflowEngine.availablePlugins, function(plugin) { - if (_.isFunction(plugin.getCheck)) { - OCA.WorkflowEngine.availableChecks.push(plugin.getCheck()); - } - }); + if (!OCA.WorkflowEngine.availablePlugins.length) { + OCA.WorkflowEngine.availablePlugins = OC.Plugins.getPlugins('OCA.WorkflowEngine.CheckPlugins'); + _.each(OCA.WorkflowEngine.availablePlugins, function(plugin) { + if (_.isFunction(plugin.getCheck)) { + OCA.WorkflowEngine.availableChecks.push(plugin.getCheck(classname)); + } + }); + } this.collection.fetch({data: { 'class': classname From 7b73c0f5a22628381ff772a14d94ff36ba5f96f7 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 15:17:38 +0200 Subject: [PATCH 17/32] Add the systemtag js files --- .../lib/AppInfo/Application.php | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index 8433950304726..e5b884c0a2aef 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -40,9 +40,24 @@ public function registerHooksAndListeners() { $dispatcher->addListener( 'OCP\WorkflowEngine::loadAdditionalSettingScripts', function() { - Util::addStyle('workflowengine', 'admin'); - Util::addScript('workflowengine', 'admin'); - Util::addScript('workflowengine', 'usergroupmembershipplugin'); + style('workflowengine', [ + 'admin', + ]); + + script('core', [ + 'oc-backbone-webdav', + 'systemtags/systemtags', + 'systemtags/systemtagmodel', + 'systemtags/systemtagscollection', + ]); + + script('workflowengine', [ + 'admin', + + // Check plugins + 'filesystemtagsplugin', + 'usergroupmembershipplugin', + ]); }, -100 ); From 7b87935cb962d47f15d49006a6d50bdc467dfa50 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 15:37:03 +0200 Subject: [PATCH 18/32] Add system tag check --- apps/workflowengine/js/admin.js | 1 - .../workflowengine/js/filesystemtagsplugin.js | 82 +++++++++ .../lib/Check/FileSystemTags.php | 155 ++++++++++++++++++ 3 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 apps/workflowengine/js/filesystemtagsplugin.js create mode 100644 apps/workflowengine/lib/Check/FileSystemTags.php diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index b9f0744732ffe..e6df4b75f70e2 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -348,7 +348,6 @@ this.renderOperation(operation); }, renderOperation: function(operation){ - console.log(operation); var subView = new OCA.WorkflowEngine.OperationView({ model: operation }), diff --git a/apps/workflowengine/js/filesystemtagsplugin.js b/apps/workflowengine/js/filesystemtagsplugin.js new file mode 100644 index 0000000000000..6f2f231c5e71f --- /dev/null +++ b/apps/workflowengine/js/filesystemtagsplugin.js @@ -0,0 +1,82 @@ +/** + * @copyright Copyright (c) 2016 Joas Schilling + * + * @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 . + * + */ + +(function() { + + OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; + + OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin = { + getCheck: function() { + this.collection = OC.SystemTags.collection; + this.collection.fetch({ + success: function() { + console.log('done loading tags'); + } + }); + + return { + 'class': 'OCA\\WorkflowEngine\\Check\\FileSystemTags', + 'name': t('workflowengine', 'File system tag'), + 'operators': [ + {'operator': 'is', 'name': t('workflowengine', 'is tagged with')}, + {'operator': '!is', 'name': t('workflowengine', 'is not tagged with')} + ] + }; + }, + render: function(element, classname, value) { + if (classname !== 'OCA\\WorkflowEngine\\Check\\FileSystemTags') { + return; + } + + $(element).css('width', '400px'); + + $(element).select2({ + allowClear: false, + multiple: false, + placeholder: t('workflowengine', 'Select tag…'), + query: _.debounce(function(query) { + query.callback({ + results: OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin.collection.filterByName(query.term) + }); + }, 100, true), + id: function(element) { + return element.get('id'); + }, + initSelection: function(element, callback) { + var selection = ($(element).val() || []).split('|').sort(); + callback(selection); + }, + formatResult: function (tag) { + return OC.SystemTags.getDescriptiveTag(tag); + }, + formatSelection: function (tagId) { + tag = OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin.collection.get(tagId); + return OC.SystemTags.getDescriptiveTag(tag); + }, + escapeMarkup: function(m) { + return m; + } + }); + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin); diff --git a/apps/workflowengine/lib/Check/FileSystemTags.php b/apps/workflowengine/lib/Check/FileSystemTags.php new file mode 100644 index 0000000000000..77179631fc1da --- /dev/null +++ b/apps/workflowengine/lib/Check/FileSystemTags.php @@ -0,0 +1,155 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\Files\Cache\ICache; +use OCP\Files\Storage\IStorage; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagNotFoundException; +use OCP\WorkflowEngine\ICheck; + +class FileSystemTags implements ICheck { + + /** @var array */ + protected $fileIds; + + /** @var array */ + protected $fileSystemTags; + + /** @var ISystemTagManager */ + protected $systemTagManager; + + /** @var ISystemTagObjectMapper */ + protected $systemTagObjectMapper; + + /** @var IStorage */ + protected $storage; + + /** @var string */ + protected $path; + + /** + * @param ISystemTagManager $systemTagManager + * @param ISystemTagObjectMapper $systemTagObjectMapper + */ + public function __construct(ISystemTagManager $systemTagManager, ISystemTagObjectMapper $systemTagObjectMapper) { + $this->systemTagManager = $systemTagManager; + $this->systemTagObjectMapper = $systemTagObjectMapper; + } + + /** + * @param IStorage $storage + * @param string $path + */ + public function setFileInfo(IStorage $storage, $path) { + $this->storage = $storage; + $this->path = $path; + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $systemTags = $this->getSystemTags(); + return ($operator === 'is') === in_array($value, $systemTags); + } + + /** + * @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); + } + + try { + $this->systemTagManager->getTagsByIds($value); + } catch (TagNotFoundException $e) { + throw new \UnexpectedValueException('Tag does not exist', 2); + } catch (\InvalidArgumentException $e) { + throw new \UnexpectedValueException('Tag does not exist', 3); + } + } + + /** + * Get the ids of the assigned system tags + * @return string[] + */ + protected function getSystemTags() { + $cache = $this->storage->getCache(); + $fileIds = $this->getFileIds($cache, $this->path); + + $systemTags = []; + foreach ($fileIds as $i => $fileId) { + if (isset($this->fileSystemTags[$fileId])) { + $systemTags[] = $this->fileSystemTags[$fileId]; + unset($fileIds[$i]); + } + } + + if (!empty($fileIds)) { + $mappedSystemTags = $this->systemTagObjectMapper->getTagIdsForObjects($fileIds, 'files'); + foreach ($mappedSystemTags as $fileId => $fileSystemTags) { + $this->fileSystemTags[$fileId] = $fileSystemTags; + $systemTags[] = $fileSystemTags; + } + } + + $systemTags = call_user_func_array('array_merge', $systemTags); + $systemTags = array_unique($systemTags); + return $systemTags; + } + + /** + * Get the file ids of the given path and its parents + * @param ICache $cache + * @param string $path + * @return int[] + */ + protected function getFileIds(ICache $cache, $path) { + $cacheId = $cache->getNumericStorageId(); + if (isset($this->fileIds[$cacheId][$path])) { + return $this->fileIds[$cacheId][$path]; + } + + if ($path !== dirname($path)) { + $parentIds = $this->getFileIds($cache, dirname($path)); + } else { + return []; + } + + $fileId = $cache->getId($path); + if ($fileId !== -1) { + $parentIds[] = $cache->getId($path); + } + + $this->fileIds[$cacheId][$path] = $parentIds; + + return $parentIds; + } +} From 627f24396c5e32856006e9d9529b751c720bffc8 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 27 Jul 2016 15:57:00 +0200 Subject: [PATCH 19/32] Add file size as check --- apps/workflowengine/js/filesizeplugin.js | 51 ++++++++ .../lib/AppInfo/Application.php | 1 + apps/workflowengine/lib/Check/FileSize.php | 113 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 apps/workflowengine/js/filesizeplugin.js create mode 100644 apps/workflowengine/lib/Check/FileSize.php diff --git a/apps/workflowengine/js/filesizeplugin.js b/apps/workflowengine/js/filesizeplugin.js new file mode 100644 index 0000000000000..add2e57821ceb --- /dev/null +++ b/apps/workflowengine/js/filesizeplugin.js @@ -0,0 +1,51 @@ +/** + * @copyright Copyright (c) 2016 Joas Schilling + * + * @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 . + * + */ + +(function() { + + OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; + + OCA.WorkflowEngine.Plugins.FileSizePlugin = { + getCheck: function() { + return { + 'class': 'OCA\\WorkflowEngine\\Check\\FileSize', + 'name': t('workflowengine', 'File size'), + 'operators': [ + {'operator': 'less', 'name': t('workflowengine', 'less')}, + {'operator': '!greater', 'name': t('workflowengine', 'less or equals')}, + {'operator': '!less', 'name': t('workflowengine', 'greater or equals')}, + {'operator': 'greater', 'name': t('workflowengine', 'greater')} + ] + }; + }, + render: function(element, classname, value) { + if (classname !== 'OCA\\WorkflowEngine\\Check\\FileSize') { + return; + } + + $(element).css('width', '250px') + .attr('placeholder', t('workflowengine', '12 MB')) + ; + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.FileSizePlugin); diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index e5b884c0a2aef..64e03081a3427 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -55,6 +55,7 @@ function() { 'admin', // Check plugins + 'filesizeplugin', 'filesystemtagsplugin', 'usergroupmembershipplugin', ]); diff --git a/apps/workflowengine/lib/Check/FileSize.php b/apps/workflowengine/lib/Check/FileSize.php new file mode 100644 index 0000000000000..70071757c12c2 --- /dev/null +++ b/apps/workflowengine/lib/Check/FileSize.php @@ -0,0 +1,113 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\Files\Storage\IStorage; +use OCP\IRequest; +use OCP\Util; +use OCP\WorkflowEngine\ICheck; + +class FileSize implements ICheck { + + /** @var int */ + protected $size; + + /** @var IRequest */ + protected $request; + + /** + * @param IRequest $request + */ + public function __construct(IRequest $request) { + $this->request = $request; + } + + /** + * @param IStorage $storage + * @param string $path + */ + public function setFileInfo(IStorage $storage, $path) { + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $size = $this->getFileSizeFromHeader(); + + $value = Util::computerFileSize($value); + if ($size !== false) { + switch ($operator) { + case 'less': + return $size < $value; + case '!less': + return $size >= $value; + case 'greater': + return $size > $value; + case '!greater': + return $size <= $value; + } + } + return false; + } + + /** + * @param string $operator + * @param string $value + * @throws \UnexpectedValueException + */ + public function validateCheck($operator, $value) { + if (!in_array($operator, ['less', '!less', 'greater', '!greater'])) { + throw new \UnexpectedValueException('Invalid operator', 1); + } + + if (!preg_match('/^[0-9]+[ ]?[kmgt]?b$/i', $value)) { + throw new \UnexpectedValueException('Invalid file size', 2); + } + } + + /** + * @return string + */ + protected function getFileSizeFromHeader() { + if ($this->size !== null) { + return $this->size; + } + + $size = $this->request->getHeader('OC-Total-Length'); + if ($size === null) { + if (in_array($this->request->getMethod(), ['POST', 'PUT'])) { + $size = $this->request->getHeader('Content-Length'); + } + } + + if ($size === null) { + $size = false; + } + + $this->size = $size; + return $this->size; + } +} From d146df5820a582b19f4a81b1e0bdbfd5df3bd7b6 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 28 Jul 2016 11:44:22 +0200 Subject: [PATCH 20/32] Add file mime type --- apps/workflowengine/css/admin.css | 4 + apps/workflowengine/js/admin.js | 2 +- apps/workflowengine/js/filemimetypeplugin.js | 72 +++++++++ apps/workflowengine/js/filesizeplugin.js | 15 +- .../workflowengine/js/filesystemtagsplugin.js | 4 +- .../js/usergroupmembershipplugin.js | 4 +- .../lib/AppInfo/Application.php | 1 + .../lib/Check/AbstractStringCheck.php | 110 +++++++++++++ .../workflowengine/lib/Check/FileMimeType.php | 82 ++++++++++ .../tests/Check/AbstractStringCheckTest.php | 149 ++++++++++++++++++ 10 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 apps/workflowengine/js/filemimetypeplugin.js create mode 100644 apps/workflowengine/lib/Check/AbstractStringCheck.php create mode 100644 apps/workflowengine/lib/Check/FileMimeType.php create mode 100644 apps/workflowengine/tests/Check/AbstractStringCheckTest.php diff --git a/apps/workflowengine/css/admin.css b/apps/workflowengine/css/admin.css index 1d94fced003d0..70185615ad642 100644 --- a/apps/workflowengine/css/admin.css +++ b/apps/workflowengine/css/admin.css @@ -46,3 +46,7 @@ margin-right: 5px; } +.workflowengine .invalid-input { + border-color: #aa0000; +} + diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index e6df4b75f70e2..ce85c8c008bde 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -296,7 +296,7 @@ _.each(OCA.WorkflowEngine.availablePlugins, function(plugin) { if (_.isFunction(plugin.render)) { - plugin.render(valueElement, check['class'], check['value']); + plugin.render(valueElement, check); } }); }, this); diff --git a/apps/workflowengine/js/filemimetypeplugin.js b/apps/workflowengine/js/filemimetypeplugin.js new file mode 100644 index 0000000000000..33cbbd7fd7e27 --- /dev/null +++ b/apps/workflowengine/js/filemimetypeplugin.js @@ -0,0 +1,72 @@ +/** + * @copyright Copyright (c) 2016 Joas Schilling + * + * @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 . + * + */ + +(function() { + + OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; + + OCA.WorkflowEngine.Plugins.FileMimeTypePlugin = { + getCheck: function() { + return { + 'class': 'OCA\\WorkflowEngine\\Check\\FileMimeType', + 'name': t('workflowengine', 'File mime type (upload)'), + 'operators': [ + {'operator': 'is', 'name': t('workflowengine', 'is')}, + {'operator': '!is', 'name': t('workflowengine', 'is not')}, + {'operator': 'matches', 'name': t('workflowengine', 'matches')}, + {'operator': '!matches', 'name': t('workflowengine', 'does not match')} + ] + }; + }, + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\FileMimeType') { + return; + } + + var placeholder = t('workflowengine', 'text/plain'); + if (check['operator'] === 'matches' || check['operator'] === '!matches') { + placeholder = t('workflowengine', '/^text\\/(plain|html)$/i'); + + if (this._validateRegex(check['value'])) { + $(element).removeClass('invalid-input'); + } else { + $(element).addClass('invalid-input'); + } + } + + $(element).css('width', '250px') + .attr('placeholder', placeholder) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }); + }, + + _validateRegex: function(string) { + var regexRegex = /^\/(.*)\/([gui]{0,3})$/, + result = regexRegex.exec(string); + return result !== null; + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.FileMimeTypePlugin); diff --git a/apps/workflowengine/js/filesizeplugin.js b/apps/workflowengine/js/filesizeplugin.js index add2e57821ceb..0efa9d00edf06 100644 --- a/apps/workflowengine/js/filesizeplugin.js +++ b/apps/workflowengine/js/filesizeplugin.js @@ -27,7 +27,7 @@ getCheck: function() { return { 'class': 'OCA\\WorkflowEngine\\Check\\FileSize', - 'name': t('workflowengine', 'File size'), + 'name': t('workflowengine', 'File size (upload)'), 'operators': [ {'operator': 'less', 'name': t('workflowengine', 'less')}, {'operator': '!greater', 'name': t('workflowengine', 'less or equals')}, @@ -36,14 +36,19 @@ ] }; }, - render: function(element, classname, value) { - if (classname !== 'OCA\\WorkflowEngine\\Check\\FileSize') { + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\FileSize') { return; } + var placeholder = '12 MB'; // Do not translate!!! $(element).css('width', '250px') - .attr('placeholder', t('workflowengine', '12 MB')) - ; + .attr('placeholder', placeholder) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }); } }; })(); diff --git a/apps/workflowengine/js/filesystemtagsplugin.js b/apps/workflowengine/js/filesystemtagsplugin.js index 6f2f231c5e71f..026345571e793 100644 --- a/apps/workflowengine/js/filesystemtagsplugin.js +++ b/apps/workflowengine/js/filesystemtagsplugin.js @@ -41,8 +41,8 @@ ] }; }, - render: function(element, classname, value) { - if (classname !== 'OCA\\WorkflowEngine\\Check\\FileSystemTags') { + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\FileSystemTags') { return; } diff --git a/apps/workflowengine/js/usergroupmembershipplugin.js b/apps/workflowengine/js/usergroupmembershipplugin.js index 528a7bd3e3d05..1c09e7d5ccd18 100644 --- a/apps/workflowengine/js/usergroupmembershipplugin.js +++ b/apps/workflowengine/js/usergroupmembershipplugin.js @@ -34,8 +34,8 @@ ] }; }, - render: function(element, classname, value) { - if (classname !== 'OCA\\WorkflowEngine\\Check\\UserGroupMembership') { + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\UserGroupMembership') { return; } diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index 64e03081a3427..c1dc65fe8104c 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -55,6 +55,7 @@ function() { 'admin', // Check plugins + 'filemimetypeplugin', 'filesizeplugin', 'filesystemtagsplugin', 'usergroupmembershipplugin', diff --git a/apps/workflowengine/lib/Check/AbstractStringCheck.php b/apps/workflowengine/lib/Check/AbstractStringCheck.php new file mode 100644 index 0000000000000..77576266fcfeb --- /dev/null +++ b/apps/workflowengine/lib/Check/AbstractStringCheck.php @@ -0,0 +1,110 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\Files\Storage\IStorage; +use OCP\WorkflowEngine\ICheck; + +abstract class AbstractStringCheck implements ICheck { + + /** @var array[] Nested array: [Pattern => [ActualValue => Regex Result]] */ + protected $matches; + + /** + * @param IStorage $storage + * @param string $path + */ + public function setFileInfo(IStorage $storage, $path) { + // Nothing changes here with a different path + } + + /** + * @return string + */ + abstract protected function getActualValue(); + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $actualValue = $this->getActualValue(); + return $this->executeStringCheck($operator, $value, $actualValue); + } + + /** + * @param string $operator + * @param string $checkValue + * @param string $actualValue + * @return bool + */ + protected function executeStringCheck($operator, $checkValue, $actualValue) { + if ($operator === 'is') { + return $checkValue === $actualValue; + } else if ($operator === '!is') { + return $checkValue !== $actualValue; + } else { + $match = $this->match($checkValue, $actualValue); + if ($operator === 'matches') { + return $match === 1; + } else { + return $match === 0; + } + } + } + + /** + * @param string $operator + * @param string $value + * @throws \UnexpectedValueException + */ + public function validateCheck($operator, $value) { + if (!in_array($operator, ['is', '!is', 'matches', '!matches'])) { + throw new \UnexpectedValueException('Invalid operator', 1); + } + + if (in_array($operator, ['matches', '!matches']) && + @preg_match($value, null) === false) { + throw new \UnexpectedValueException('Invalid regex', 2); + } + } + + /** + * @param string $pattern + * @param string $subject + * @return int|bool + */ + protected function match($pattern, $subject) { + $patternHash = md5($pattern); + $subjectHash = md5($subject); + if (isset($this->matches[$patternHash][$subjectHash])) { + return $this->matches[$patternHash][$subjectHash]; + } + if (!isset($this->matches[$patternHash])) { + $this->matches[$patternHash] = []; + } + $this->matches[$patternHash][$subjectHash] = preg_match($pattern, $subject); + return $this->matches[$patternHash][$subjectHash]; + } +} diff --git a/apps/workflowengine/lib/Check/FileMimeType.php b/apps/workflowengine/lib/Check/FileMimeType.php new file mode 100644 index 0000000000000..c774d30a233f2 --- /dev/null +++ b/apps/workflowengine/lib/Check/FileMimeType.php @@ -0,0 +1,82 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\Files\IMimeTypeDetector; +use OCP\IRequest; + +class FileMimeType extends AbstractStringCheck { + + /** @var string */ + protected $mimeType; + + /** @var IRequest */ + protected $request; + + /** @var IMimeTypeDetector */ + protected $mimeTypeDetector; + + /** + * @param IRequest $request + * @param IMimeTypeDetector $mimeTypeDetector + */ + public function __construct(IRequest $request, IMimeTypeDetector $mimeTypeDetector) { + $this->request = $request; + $this->mimeTypeDetector = $mimeTypeDetector; + } + + /** + * @return string + */ + protected function getActualValue() { + if ($this->mimeType !== null) { + return $this->mimeType; + } + + $this->mimeType = ''; + if ($this->isWebDAVRequest()) { + if ($this->request->getMethod() === 'PUT') { + $path = $this->request->getPathInfo(); + $this->mimeType = $this->mimeTypeDetector->detectPath($path); + } + } else if (in_array($this->request->getMethod(), ['POST', 'PUT'])) { + $files = $this->request->getUploadedFile('files'); + if (isset($files['type'][0])) { + $this->mimeType = $files['type'][0]; + } + } + return $this->mimeType; + } + + /** + * @return bool + */ + protected function isWebDAVRequest() { + return substr($this->request->getScriptName(), 0 - strlen('/remote.php')) === '/remote.php' && ( + $this->request->getPathInfo() === '/webdav' || + strpos($this->request->getPathInfo(), '/webdav/') === 0 || + $this->request->getPathInfo() === '/dav/files' || + strpos($this->request->getPathInfo(), '/dav/files/') === 0 + ); + } +} diff --git a/apps/workflowengine/tests/Check/AbstractStringCheckTest.php b/apps/workflowengine/tests/Check/AbstractStringCheckTest.php new file mode 100644 index 0000000000000..43818ab8e2663 --- /dev/null +++ b/apps/workflowengine/tests/Check/AbstractStringCheckTest.php @@ -0,0 +1,149 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Tests\Check; + + +class AbstractStringCheckTest extends \Test\TestCase { + + public function dataExecuteStringCheck() { + return [ + ['is', 'same', 'same', true], + ['is', 'different', 'not the same', false], + ['!is', 'same', 'same', false], + ['!is', 'different', 'not the same', true], + + ['matches', '/match/', 'match', true], + ['matches', '/different/', 'not the same', false], + ['!matches', '/match/', 'match', false], + ['!matches', '/different/', 'not the same', true], + ]; + } + + /** + * @dataProvider dataExecuteStringCheck + * @param string $operation + * @param string $checkValue + * @param string $actualValue + * @param bool $expected + */ + public function testExecuteStringCheck($operation, $checkValue, $actualValue, $expected) { + $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') + ->setMethods([ + 'setPath', + 'executeCheck', + 'getActualValue', + ]) + ->getMock(); + + /** @var \OCA\WorkflowEngine\Check\AbstractStringCheck $check */ + $this->assertEquals($expected, $this->invokePrivate($check, 'executeStringCheck', [$operation, $checkValue, $actualValue])); + } + + public function dataValidateCheck() { + return [ + ['is', '/Invalid(Regex/'], + ['!is', '/Invalid(Regex/'], + ['matches', '/Valid(Regex)/'], + ['!matches', '/Valid(Regex)/'], + ]; + } + + /** + * @dataProvider dataValidateCheck + * @param string $operator + * @param string $value + */ + public function testValidateCheck($operator, $value) { + $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') + ->setMethods([ + 'setPath', + 'executeCheck', + 'getActualValue', + ]) + ->getMock(); + + /** @var \OCA\WorkflowEngine\Check\AbstractStringCheck $check */ + $check->validateCheck($operator, $value); + } + + public function dataValidateCheckInvalid() { + return [ + ['!!is', '', 1, 'Invalid operator'], + ['less', '', 1, 'Invalid operator'], + ['matches', '/Invalid(Regex/', 2, 'Invalid regex'], + ['!matches', '/Invalid(Regex/', 2, 'Invalid regex'], + ]; + } + + /** + * @dataProvider dataValidateCheckInvalid + * @param $operator + * @param $value + * @param $exceptionCode + * @param $exceptionMessage + */ + public function testValidateCheckInvalid($operator, $value, $exceptionCode, $exceptionMessage) { + $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') + ->setMethods([ + 'setPath', + 'executeCheck', + 'getActualValue', + ]) + ->getMock(); + + try { + /** @var \OCA\WorkflowEngine\Check\AbstractStringCheck $check */ + $check->validateCheck($operator, $value); + } catch (\UnexpectedValueException $e) { + $this->assertEquals($exceptionCode, $e->getCode()); + $this->assertEquals($exceptionMessage, $e->getMessage()); + } + } + + public function dataMatch() { + return [ + ['/valid/', 'valid', [], true], + ['/valid/', 'valid', [md5('/valid/') => [md5('valid') => false]], false], // Cache hit + ]; + } + + /** + * @dataProvider dataMatch + * @param string $pattern + * @param string $subject + * @param array[] $matches + * @param bool $expected + */ + public function testMatch($pattern, $subject, $matches, $expected) { + $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') + ->setMethods([ + 'setPath', + 'executeCheck', + 'getActualValue', + ]) + ->getMock(); + + $this->invokePrivate($check, 'matches', [$matches]); + + $this->assertEquals($expected, $this->invokePrivate($check, 'match', [$pattern, $subject])); + } +} From 45c74cdc5af792b3a1f5df2edeed227e8a978256 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 28 Jul 2016 13:01:50 +0200 Subject: [PATCH 21/32] User agent check --- apps/workflowengine/js/admin.js | 2 +- .../js/requestuseragentplugin.js | 118 ++++++++++++++++++ .../lib/AppInfo/Application.php | 1 + .../lib/Check/RequestUserAgent.php | 71 +++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 apps/workflowengine/js/requestuseragentplugin.js create mode 100644 apps/workflowengine/lib/Check/RequestUserAgent.php diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index ce85c8c008bde..ec921f8c224f4 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -242,7 +242,7 @@ // model change will trigger render this.model.set({'checks': checks}); }, - deleteCheck: function() { + deleteCheck: function(event) { console.log(arguments); var id = $(event.target.parentElement).data('id'), checks = JSON.parse(JSON.stringify(this.model.get('checks'))); diff --git a/apps/workflowengine/js/requestuseragentplugin.js b/apps/workflowengine/js/requestuseragentplugin.js new file mode 100644 index 0000000000000..8413d52ac43a5 --- /dev/null +++ b/apps/workflowengine/js/requestuseragentplugin.js @@ -0,0 +1,118 @@ +/** + * @copyright Copyright (c) 2016 Joas Schilling + * + * @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 . + * + */ + +(function() { + + OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; + + OCA.WorkflowEngine.Plugins.RequestUserAgentPlugin = { + predefinedValues: ['android', 'ios', 'desktop'], + getCheck: function() { + return { + 'class': 'OCA\\WorkflowEngine\\Check\\RequestUserAgent', + 'name': t('workflowengine', 'Request user agent'), + 'operators': [ + {'operator': 'is', 'name': t('workflowengine', 'is')}, + {'operator': '!is', 'name': t('workflowengine', 'is not')}, + {'operator': 'matches', 'name': t('workflowengine', 'matches')}, + {'operator': '!matches', 'name': t('workflowengine', 'does not match')} + ] + }; + }, + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestUserAgent') { + return; + } + + var placeholder = t('workflowengine', 'Mozilla/5.0 User Agent'); + + if (check['operator'] === 'matches' || check['operator'] === '!matches') { + placeholder = t('workflowengine', '/^Mozilla\\/5\\.0 (.?)$/i'); + } + + $(element).css('width', '250px') + .attr('placeholder', placeholder) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }); + + if (check['operator'] === 'matches' || check['operator'] === '!matches') { + if (this._validateRegex(check['value'])) { + $(element).removeClass('invalid-input'); + } else { + $(element).addClass('invalid-input'); + } + } else { + var self = this, + data = [ + { + text: t('workflowengine', 'Sync clients'), + children: [ + {id: 'android', text: t('workflowengine', 'Android client')}, + {id: 'ios', text: t('workflowengine', 'iOS client')}, + {id: 'desktop', text: t('workflowengine', 'Desktop client')} + ] + } + ]; + if (this.predefinedValues.indexOf(check['value']) === -1) { + data.unshift({ + id: check['value'], + text: check['value'] + }) + } + + $(element).select2({ + data: data, + createSearchChoice: function(term) { + if (self.predefinedValues.indexOf(check['value']) === -1) { + return { + id: term, + text: term + }; + } + }, + id: function(element) { + return element.id; + }, + formatResult: function (tag) { + return tag.text; + }, + formatSelection: function (tag) { + return tag.text; + }, + escapeMarkup: function(m) { + return m; + } + }) + } + }, + + _validateRegex: function(string) { + var regexRegex = /^\/(.*)\/([gui]{0,3})$/, + result = regexRegex.exec(string); + return result !== null; + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestUserAgentPlugin); diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index c1dc65fe8104c..82a07aa59e2eb 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -58,6 +58,7 @@ function() { 'filemimetypeplugin', 'filesizeplugin', 'filesystemtagsplugin', + 'requestuseragentplugin', 'usergroupmembershipplugin', ]); }, diff --git a/apps/workflowengine/lib/Check/RequestUserAgent.php b/apps/workflowengine/lib/Check/RequestUserAgent.php new file mode 100644 index 0000000000000..241b19136a7ee --- /dev/null +++ b/apps/workflowengine/lib/Check/RequestUserAgent.php @@ -0,0 +1,71 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\IRequest; + +class RequestUserAgent extends AbstractStringCheck { + + /** @var IRequest */ + protected $request; + + /** + * @param IRequest $request + */ + public function __construct(IRequest $request) { + $this->request = $request; + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $actualValue = $this->getActualValue(); + if (in_array($operator, ['is', '!is'])) { + switch ($value) { + case 'android': + $operator = $operator === 'is' ? 'matches' : '!matches'; + $value = IRequest::USER_AGENT_CLIENT_ANDROID; + break; + case 'ios': + $operator = $operator === 'is' ? 'matches' : '!matches'; + $value = IRequest::USER_AGENT_CLIENT_IOS; + break; + case 'desktop': + $operator = $operator === 'is' ? 'matches' : '!matches'; + $value = IRequest::USER_AGENT_CLIENT_DESKTOP; + break; + } + } + return $this->executeStringCheck($operator, $value, $actualValue); + } + + /** + * @return string + */ + protected function getActualValue() { + return (string) $this->request->getHeader('User-Agent'); + } +} From 01d269bb4dc4005dc2f9cb1b6c801adc83c7963e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 28 Jul 2016 13:47:42 +0200 Subject: [PATCH 22/32] Add Request URL --- apps/workflowengine/js/requesturlplugin.js | 117 ++++++++++++++++++ .../lib/AppInfo/Application.php | 1 + apps/workflowengine/lib/Check/RequestURL.php | 89 +++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 apps/workflowengine/js/requesturlplugin.js create mode 100644 apps/workflowengine/lib/Check/RequestURL.php diff --git a/apps/workflowengine/js/requesturlplugin.js b/apps/workflowengine/js/requesturlplugin.js new file mode 100644 index 0000000000000..5f21d2a59fc41 --- /dev/null +++ b/apps/workflowengine/js/requesturlplugin.js @@ -0,0 +1,117 @@ +/** + * @copyright Copyright (c) 2016 Joas Schilling + * + * @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 . + * + */ + +(function() { + + OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; + + OCA.WorkflowEngine.Plugins.RequestURLPlugin = { + predefinedValues: ['webdav'], + getCheck: function() { + return { + 'class': 'OCA\\WorkflowEngine\\Check\\RequestURL', + 'name': t('workflowengine', 'Request URL'), + 'operators': [ + {'operator': 'is', 'name': t('workflowengine', 'is')}, + {'operator': '!is', 'name': t('workflowengine', 'is not')}, + {'operator': 'matches', 'name': t('workflowengine', 'matches')}, + {'operator': '!matches', 'name': t('workflowengine', 'does not match')} + ] + }; + }, + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestURL') { + return; + } + + var placeholder = t('workflowengine', 'https://localhost/index.php'); + + if (check['operator'] === 'matches' || check['operator'] === '!matches') { + placeholder = t('workflowengine', '/^https\\:\\/\\/localhost\\/index\\.php$/i'); + } + + $(element).css('width', '250px') + .attr('placeholder', placeholder) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }); + + if (check['operator'] === 'matches' || check['operator'] === '!matches') { + if (this._validateRegex(check['value'])) { + $(element).removeClass('invalid-input'); + } else { + $(element).addClass('invalid-input'); + } + } else { + var self = this, + data = [ + { + text: t('workflowengine', 'Predefined URLs'), + children: [ + {id: 'webdav', text: t('workflowengine', 'Files WebDAV')} + ] + } + ]; + if (this.predefinedValues.indexOf(check['value']) === -1) { + data.unshift({ + id: check['value'], + text: check['value'] + }) + } + + + $(element).select2({ + data: data, + createSearchChoice: function(term) { + if (self.predefinedValues.indexOf(check['value']) === -1) { + return { + id: term, + text: term + }; + } + }, + id: function(element) { + return element.id; + }, + formatResult: function (tag) { + return tag.text; + }, + formatSelection: function (tag) { + return tag.text; + }, + escapeMarkup: function(m) { + return m; + } + }) + } + }, + + _validateRegex: function(string) { + var regexRegex = /^\/(.*)\/([gui]{0,3})$/, + result = regexRegex.exec(string); + return result !== null; + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestURLPlugin); diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index 82a07aa59e2eb..b024518c4c538 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -59,6 +59,7 @@ function() { 'filesizeplugin', 'filesystemtagsplugin', 'requestuseragentplugin', + 'requesturlplugin', 'usergroupmembershipplugin', ]); }, diff --git a/apps/workflowengine/lib/Check/RequestURL.php b/apps/workflowengine/lib/Check/RequestURL.php new file mode 100644 index 0000000000000..0cae3cf7e5619 --- /dev/null +++ b/apps/workflowengine/lib/Check/RequestURL.php @@ -0,0 +1,89 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\IRequest; + +class RequestURL extends AbstractStringCheck { + + /** @var string */ + protected $url; + + /** @var IRequest */ + protected $request; + + /** + * @param IRequest $request + */ + public function __construct(IRequest $request) { + $this->request = $request; + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $actualValue = $this->getActualValue(); + if (in_array($operator, ['is', '!is'])) { + switch ($value) { + case 'webdav': + if ($operator === 'is') { + return $this->isWebDAVRequest(); + } else { + return !$this->isWebDAVRequest(); + } + } + } + return $this->executeStringCheck($operator, $value, $actualValue); + } + + /** + * @return string + */ + protected function getActualValue() { + if ($this->url !== null) { + return $this->url; + } + + $this->url = $this->request->getServerProtocol() . '://';// E.g. http(s) + :// + $this->url .= $this->request->getServerHost();// E.g. localhost + $this->url .= $this->request->getScriptName();// E.g. /nextcloud/index.php + $this->url .= $this->request->getPathInfo();// E.g. /apps/files_texteditor/ajax/loadfile + + return $this->url; // E.g. https://localhost/nextcloud/index.php/apps/files_texteditor/ajax/loadfile + } + + /** + * @return bool + */ + protected function isWebDAVRequest() { + return substr($this->request->getScriptName(), 0 - strlen('/remote.php')) === '/remote.php' && ( + $this->request->getPathInfo() === '/webdav' || + strpos($this->request->getPathInfo(), '/webdav/') === 0 || + $this->request->getPathInfo() === '/dav/files' || + strpos($this->request->getPathInfo(), '/dav/files/') === 0 + ); + } +} From af3eaa86acd7fe817cccad9c8c421507866cdc31 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 28 Jul 2016 14:16:01 +0200 Subject: [PATCH 23/32] Add remote address --- .../js/requestremoteaddressplugin.js | 83 ++++++++++ .../lib/AppInfo/Application.php | 3 +- .../lib/Check/RequestRemoteAddress.php | 148 ++++++++++++++++++ .../tests/Check/RequestRemoteAddressTest.php | 123 +++++++++++++++ 4 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 apps/workflowengine/js/requestremoteaddressplugin.js create mode 100644 apps/workflowengine/lib/Check/RequestRemoteAddress.php create mode 100644 apps/workflowengine/tests/Check/RequestRemoteAddressTest.php diff --git a/apps/workflowengine/js/requestremoteaddressplugin.js b/apps/workflowengine/js/requestremoteaddressplugin.js new file mode 100644 index 0000000000000..a66d6f51f0f8f --- /dev/null +++ b/apps/workflowengine/js/requestremoteaddressplugin.js @@ -0,0 +1,83 @@ +/** + * @copyright Copyright (c) 2016 Joas Schilling + * + * @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 . + * + */ + +(function() { + + OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; + + OCA.WorkflowEngine.Plugins.RequestRemoteAddressPlugin = { + getCheck: function() { + return { + 'class': 'OCA\\WorkflowEngine\\Check\\RequestRemoteAddress', + 'name': t('workflowengine', 'Request remote address'), + 'operators': [ + {'operator': 'matchesIPv4', 'name': t('workflowengine', 'matches IPv4')}, + {'operator': '!matchesIPv4', 'name': t('workflowengine', 'does not match IPv4')}, + {'operator': 'matchesIPv6', 'name': t('workflowengine', 'matches IPv6')}, + {'operator': '!matchesIPv6', 'name': t('workflowengine', 'does not match IPv6')} + ] + }; + }, + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestRemoteAddress') { + return; + } + + var placeholder = '127.0.0.1/32'; // Do not translate!!! + if (check['operator'] === 'matchesIPv6' || check['operator'] === '!matchesIPv6') { + placeholder = '::1/128'; // Do not translate!!! + if (this._validateIPv6(check['value'])) { + $(element).removeClass('invalid-input'); + } else { + $(element).addClass('invalid-input'); + } + } else { + if (this._validateIPv4(check['value'])) { + $(element).removeClass('invalid-input'); + } else { + $(element).addClass('invalid-input'); + } + } + + $(element).css('width', '300px') + .attr('placeholder', placeholder) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }); + }, + + _validateIPv4: function(string) { + var regexRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|[1-2][0-9]|[1-9])$/, + result = regexRegex.exec(string); + return result !== null; + }, + + _validateIPv6: function(string) { + var regexRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(1([01][0-9]|2[0-8])|[1-9][0-9]|[0-9])$/, + result = regexRegex.exec(string); + return result !== null; + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestRemoteAddressPlugin); diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index b024518c4c538..14efbaa45f4a3 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -58,8 +58,9 @@ function() { 'filemimetypeplugin', 'filesizeplugin', 'filesystemtagsplugin', - 'requestuseragentplugin', + 'requestremoteaddressplugin', 'requesturlplugin', + 'requestuseragentplugin', 'usergroupmembershipplugin', ]); }, diff --git a/apps/workflowengine/lib/Check/RequestRemoteAddress.php b/apps/workflowengine/lib/Check/RequestRemoteAddress.php new file mode 100644 index 0000000000000..7897fcbd9d3d0 --- /dev/null +++ b/apps/workflowengine/lib/Check/RequestRemoteAddress.php @@ -0,0 +1,148 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\Files\Storage\IStorage; +use OCP\IRequest; +use OCP\WorkflowEngine\ICheck; + +class RequestRemoteAddress implements ICheck { + + /** @var IRequest */ + protected $request; + + /** + * @param IRequest $request + */ + public function __construct(IRequest $request) { + $this->request = $request; + } + + /** + * @param IStorage $storage + * @param string $path + */ + public function setFileInfo(IStorage $storage, $path) { + // A different path doesn't change time, so nothing to do here. + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $actualValue = $this->request->getRemoteAddress(); + $decodedValue = explode('/', $value); + + if ($operator === 'matchesIPv4') { + return $this->matchIPv4($actualValue, $decodedValue[0], $decodedValue[1]); + } else if ($operator === '!matchesIPv4') { + return !$this->matchIPv4($actualValue, $decodedValue[0], $decodedValue[1]); + } else if ($operator === 'matchesIPv6') { + return $this->matchIPv6($actualValue, $decodedValue[0], $decodedValue[1]); + } else { + return !$this->matchIPv6($actualValue, $decodedValue[0], $decodedValue[1]); + } + } + + /** + * @param string $operator + * @param string $value + * @throws \UnexpectedValueException + */ + public function validateCheck($operator, $value) { + if (!in_array($operator, ['matchesIPv4', '!matchesIPv4', 'matchesIPv6', '!matchesIPv6'])) { + throw new \UnexpectedValueException('Invalid operator', 1); + } + + $decodedValue = explode('/', $value); + if (sizeof($decodedValue) !== 2) { + throw new \UnexpectedValueException('Invalid IP range', 2); + } + + if (in_array($operator, ['matchesIPv4', '!matchesIPv4'])) { + if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + throw new \UnexpectedValueException('Invalid IPv4 range', 3); + } + if ($decodedValue[1] > 32 || $decodedValue[1] <= 0) { + throw new \UnexpectedValueException('Invalid IPv4 range', 4); + } + } else { + if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + throw new \UnexpectedValueException('Invalid IPv6 range', 3); + } + if ($decodedValue[1] > 128 || $decodedValue[1] <= 0) { + throw new \UnexpectedValueException('Invalid IPv6 range', 4); + } + } + } + + /** + * Based on http://stackoverflow.com/a/594134 + * @param string $ip + * @param string $rangeIp + * @param int $bits + * @return bool + */ + protected function matchIPv4($ip, $rangeIp, $bits) { + $rangeDecimal = ip2long($rangeIp); + $ipDecimal = ip2long($ip); + $mask = -1 << (32 - $bits); + return ($ipDecimal & $mask) === ($rangeDecimal & $mask); + } + + /** + * Based on http://stackoverflow.com/a/7951507 + * @param string $ip + * @param string $rangeIp + * @param int $bits + * @return bool + */ + protected function matchIPv6($ip, $rangeIp, $bits) { + $ipNet = inet_pton($ip); + $binaryIp = $this->ipv6ToBits($ipNet); + $ipNetBits = substr($binaryIp, 0, $bits); + + $rangeNet = inet_pton($rangeIp); + $binaryRange = $this->ipv6ToBits($rangeNet); + $rangeNetBits = substr($binaryRange, 0, $bits); + + return $ipNetBits === $rangeNetBits; + } + + /** + * Based on http://stackoverflow.com/a/7951507 + * @param string $packedIp + * @return string + */ + protected function ipv6ToBits($packedIp) { + $unpackedIp = unpack('A16', $packedIp); + $unpackedIp = str_split($unpackedIp[1]); + $binaryIp = ''; + foreach ($unpackedIp as $char) { + $binaryIp .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT); + } + return str_pad($binaryIp, 128, '0', STR_PAD_RIGHT); + } +} diff --git a/apps/workflowengine/tests/Check/RequestRemoteAddressTest.php b/apps/workflowengine/tests/Check/RequestRemoteAddressTest.php new file mode 100644 index 0000000000000..ec8798794df4c --- /dev/null +++ b/apps/workflowengine/tests/Check/RequestRemoteAddressTest.php @@ -0,0 +1,123 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Tests\Check; + + +class RequestRemoteAddressTest extends \Test\TestCase { + + /** @var \OCP\IRequest|\PHPUnit_Framework_MockObject_MockObject */ + protected $request; + + protected function setUp() { + parent::setUp(); + + $this->request = $this->getMockBuilder('OCP\IRequest') + ->getMock(); + } + + public function dataExecuteCheckIPv4() { + return [ + ['127.0.0.1/32', '127.0.0.1', true], + ['127.0.0.1/32', '127.0.0.0', false], + ['127.0.0.1/31', '127.0.0.0', true], + ['127.0.0.1/32', '127.0.0.2', false], + ['127.0.0.1/31', '127.0.0.2', false], + ['127.0.0.1/30', '127.0.0.2', true], + ]; + } + + /** + * @dataProvider dataExecuteCheckIPv4 + * @param string $value + * @param string $ip + * @param bool $expected + */ + public function testExecuteCheckMatchesIPv4($value, $ip, $expected) { + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + + $this->request->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn($ip); + + $this->assertEquals($expected, $check->executeCheck('matchesIPv4', $value)); + } + + /** + * @dataProvider dataExecuteCheckIPv4 + * @param string $value + * @param string $ip + * @param bool $expected + */ + public function testExecuteCheckNotMatchesIPv4($value, $ip, $expected) { + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + + $this->request->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn($ip); + + $this->assertEquals(!$expected, $check->executeCheck('!matchesIPv4', $value)); + } + + public function dataExecuteCheckIPv6() { + return [ + ['::1/128', '::1', true], + ['::2/128', '::3', false], + ['::2/127', '::3', true], + ['::1/128', '::2', false], + ['::1/127', '::2', false], + ['::1/126', '::2', true], + ['1234::1/127', '1234::', true], + ]; + } + + /** + * @dataProvider dataExecuteCheckIPv6 + * @param string $value + * @param string $ip + * @param bool $expected + */ + public function testExecuteCheckMatchesIPv6($value, $ip, $expected) { + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + + $this->request->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn($ip); + + $this->assertEquals($expected, $check->executeCheck('matchesIPv6', $value)); + } + + /** + * @dataProvider dataExecuteCheckIPv6 + * @param string $value + * @param string $ip + * @param bool $expected + */ + public function testExecuteCheckNotMatchesIPv6($value, $ip, $expected) { + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + + $this->request->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn($ip); + + $this->assertEquals(!$expected, $check->executeCheck('!matchesIPv6', $value)); + } +} From f1869cd1837e70c8d5feacc127a06d98ead1dc03 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 28 Jul 2016 15:09:57 +0200 Subject: [PATCH 24/32] Add request time --- apps/workflowengine/js/requesttimeplugin.js | 54 +++++++ .../lib/AppInfo/Application.php | 1 + apps/workflowengine/lib/Check/RequestTime.php | 125 +++++++++++++++ .../tests/Check/RequestTimeTest.php | 146 ++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 apps/workflowengine/js/requesttimeplugin.js create mode 100644 apps/workflowengine/lib/Check/RequestTime.php create mode 100644 apps/workflowengine/tests/Check/RequestTimeTest.php diff --git a/apps/workflowengine/js/requesttimeplugin.js b/apps/workflowengine/js/requesttimeplugin.js new file mode 100644 index 0000000000000..aee0e773c9e1e --- /dev/null +++ b/apps/workflowengine/js/requesttimeplugin.js @@ -0,0 +1,54 @@ +/** + * @copyright Copyright (c) 2016 Joas Schilling + * + * @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 . + * + */ + +(function() { + + OCA.WorkflowEngine = OCA.WorkflowEngine || {}; + OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; + + OCA.WorkflowEngine.Plugins.RequestTimePlugin = { + getCheck: function() { + return { + 'class': 'OCA\\WorkflowEngine\\Check\\RequestTime', + 'name': t('workflowengine', 'Request time'), + 'operators': [ + {'operator': 'in', 'name': t('workflowengine', 'between')}, + {'operator': '!in', 'name': t('workflowengine', 'not between')} + ] + }; + }, + render: function(element, check) { + if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestTime') { + return; + } + + var placeholder = '["10:00 Europe\\/Berlin","16:00 Europe\\/Berlin"]'; // FIXME need a time picker JS plugin + $(element).css('width', '300px') + .attr('placeholder', placeholder) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}, undefined, {escape: false})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }); + } + }; +})(); + +OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestTimePlugin); diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index 14efbaa45f4a3..7adc4f6864d11 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -59,6 +59,7 @@ function() { 'filesizeplugin', 'filesystemtagsplugin', 'requestremoteaddressplugin', + 'requesttimeplugin', 'requesturlplugin', 'requestuseragentplugin', 'usergroupmembershipplugin', diff --git a/apps/workflowengine/lib/Check/RequestTime.php b/apps/workflowengine/lib/Check/RequestTime.php new file mode 100644 index 0000000000000..4457f9d038c6f --- /dev/null +++ b/apps/workflowengine/lib/Check/RequestTime.php @@ -0,0 +1,125 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\Storage\IStorage; +use OCP\WorkflowEngine\ICheck; + +class RequestTime implements ICheck { + + const REGEX_TIME = '([0-1][0-9]|2[0-3]):([0-5][0-9])'; + const REGEX_TIMEZONE = '([a-zA-Z]+(?:\\\\\\/[a-zA-Z\-\_]+)+)'; + + /** @var bool[] */ + protected $cachedResults; + + /** @var ITimeFactory */ + protected $timeFactory; + + /** + * @param ITimeFactory $timeFactory + */ + public function __construct(ITimeFactory $timeFactory) { + $this->timeFactory = $timeFactory; + } + + /** + * @param IStorage $storage + * @param string $path + */ + public function setFileInfo(IStorage $storage, $path) { + // A different path doesn't change time, so nothing to do here. + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $valueHash = md5($value); + + if (isset($this->cachedResults[$valueHash])) { + return $this->cachedResults[$valueHash]; + } + + $timestamp = $this->timeFactory->getTime(); + + $values = json_decode($value, true); + $timestamp1 = $this->getTimestamp($timestamp, $values[0]); + $timestamp2 = $this->getTimestamp($timestamp, $values[1]); + + if ($timestamp1 < $timestamp2) { + $in = $timestamp1 <= $timestamp && $timestamp <= $timestamp2; + } else { + $in = $timestamp1 <= $timestamp || $timestamp <= $timestamp2; + } + + return ($operator === 'in') ? $in : !$in; + } + + /** + * @param int $currentTimestamp + * @param string $value Format: "H:i e" + * @return int + */ + protected function getTimestamp($currentTimestamp, $value) { + list($time1, $timezone1) = explode(' ', $value); + list($hour1, $minute1) = explode(':', $time1); + $date1 = new \DateTime('now', new \DateTimeZone($timezone1)); + $date1->setTimestamp($currentTimestamp); + $date1->setTime($hour1, $minute1); + + return $date1->getTimestamp(); + } + + /** + * @param string $operator + * @param string $value + * @throws \UnexpectedValueException + */ + public function validateCheck($operator, $value) { + if (!in_array($operator, ['in', '!in'])) { + throw new \UnexpectedValueException('Invalid operator', 1); + } + + $regexValue = '\"' . self::REGEX_TIME . ' ' . self::REGEX_TIMEZONE . '\"'; + $result = preg_match('/^\[' . $regexValue . ',' . $regexValue . '\]$/', $value, $matches); + if (!$result) { + throw new \UnexpectedValueException('Invalid time limits', 2); + } + + try { + new \DateTimeZone(stripslashes($matches[3])); + } catch(\Exception $e) { + throw new \UnexpectedValueException('Invalid timezone1', 3); + } + + try { + new \DateTimeZone(stripslashes($matches[6])); + } catch(\Exception $e) { + throw new \UnexpectedValueException('Invalid timezone2', 3); + } + } +} diff --git a/apps/workflowengine/tests/Check/RequestTimeTest.php b/apps/workflowengine/tests/Check/RequestTimeTest.php new file mode 100644 index 0000000000000..ca279cca0c3a6 --- /dev/null +++ b/apps/workflowengine/tests/Check/RequestTimeTest.php @@ -0,0 +1,146 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Tests\Check; + + +class RequestTimeTest extends \Test\TestCase { + + /** @var \OCP\AppFramework\Utility\ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ + protected $timeFactory; + + protected function setUp() { + parent::setUp(); + + $this->timeFactory = $this->getMockBuilder('OCP\AppFramework\Utility\ITimeFactory') + ->getMock(); + } + + public function dataExecuteCheck() { + return [ + [json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin']), 1467870105, false], // 2016-07-07T07:41:45+02:00 + [json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin']), 1467873705, true], // 2016-07-07T08:41:45+02:00 + [json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin']), 1467902505, true], // 2016-07-07T16:41:45+02:00 + [json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin']), 1467906105, false], // 2016-07-07T17:41:45+02:00 + [json_encode(['17:00 Europe/Berlin', '08:00 Europe/Berlin']), 1467870105, true], // 2016-07-07T07:41:45+02:00 + [json_encode(['17:00 Europe/Berlin', '08:00 Europe/Berlin']), 1467873705, false], // 2016-07-07T08:41:45+02:00 + [json_encode(['17:00 Europe/Berlin', '08:00 Europe/Berlin']), 1467902505, false], // 2016-07-07T16:41:45+02:00 + [json_encode(['17:00 Europe/Berlin', '08:00 Europe/Berlin']), 1467906105, true], // 2016-07-07T17:41:45+02:00 + + [json_encode(['08:00 Australia/Adelaide', '17:00 Australia/Adelaide']), 1467843105, false], // 2016-07-07T07:41:45+09:30 + [json_encode(['08:00 Australia/Adelaide', '17:00 Australia/Adelaide']), 1467846705, true], // 2016-07-07T08:41:45+09:30 + [json_encode(['08:00 Australia/Adelaide', '17:00 Australia/Adelaide']), 1467875505, true], // 2016-07-07T16:41:45+09:30 + [json_encode(['08:00 Australia/Adelaide', '17:00 Australia/Adelaide']), 1467879105, false], // 2016-07-07T17:41:45+09:30 + [json_encode(['17:00 Australia/Adelaide', '08:00 Australia/Adelaide']), 1467843105, true], // 2016-07-07T07:41:45+09:30 + [json_encode(['17:00 Australia/Adelaide', '08:00 Australia/Adelaide']), 1467846705, false], // 2016-07-07T08:41:45+09:30 + [json_encode(['17:00 Australia/Adelaide', '08:00 Australia/Adelaide']), 1467875505, false], // 2016-07-07T16:41:45+09:30 + [json_encode(['17:00 Australia/Adelaide', '08:00 Australia/Adelaide']), 1467879105, true], // 2016-07-07T17:41:45+09:30 + + [json_encode(['08:00 Pacific/Niue', '17:00 Pacific/Niue']), 1467916905, false], // 2016-07-07T07:41:45-11:00 + [json_encode(['08:00 Pacific/Niue', '17:00 Pacific/Niue']), 1467920505, true], // 2016-07-07T08:41:45-11:00 + [json_encode(['08:00 Pacific/Niue', '17:00 Pacific/Niue']), 1467949305, true], // 2016-07-07T16:41:45-11:00 + [json_encode(['08:00 Pacific/Niue', '17:00 Pacific/Niue']), 1467952905, false], // 2016-07-07T17:41:45-11:00 + [json_encode(['17:00 Pacific/Niue', '08:00 Pacific/Niue']), 1467916905, true], // 2016-07-07T07:41:45-11:00 + [json_encode(['17:00 Pacific/Niue', '08:00 Pacific/Niue']), 1467920505, false], // 2016-07-07T08:41:45-11:00 + [json_encode(['17:00 Pacific/Niue', '08:00 Pacific/Niue']), 1467949305, false], // 2016-07-07T16:41:45-11:00 + [json_encode(['17:00 Pacific/Niue', '08:00 Pacific/Niue']), 1467952905, true], // 2016-07-07T17:41:45-11:00 + ]; + } + + /** + * @dataProvider dataExecuteCheck + * @param string $value + * @param int $timestamp + * @param bool $expected + */ + public function testExecuteCheckIn($value, $timestamp, $expected) { + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + + $this->timeFactory->expects($this->once()) + ->method('getTime') + ->willReturn($timestamp); + + $this->assertEquals($expected, $check->executeCheck('in', $value)); + } + + /** + * @dataProvider dataExecuteCheck + * @param string $value + * @param int $timestamp + * @param bool $expected + */ + public function testExecuteCheckNotIn($value, $timestamp, $expected) { + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + + $this->timeFactory->expects($this->once()) + ->method('getTime') + ->willReturn($timestamp); + + $this->assertEquals(!$expected, $check->executeCheck('!in', $value)); + } + + public function dataValidateCheck() { + return [ + ['in', json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin'])], + ['!in', json_encode(['08:00 Europe/Berlin', '17:00 America/North_Dakota/Beulah'])], + ['in', json_encode(['08:00 America/Port-au-Prince', '17:00 America/Argentina/San_Luis'])], + ]; + } + + /** + * @dataProvider dataValidateCheck + * @param string $operator + * @param string $value + */ + public function testValidateCheck($operator, $value) { + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + $check->validateCheck($operator, $value); + } + + public function dataValidateCheckInvalid() { + return [ + ['!!in', json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin']), 1, 'Invalid operator'], + ['in', json_encode(['28:00 Europe/Berlin', '17:00 Europe/Berlin']), 2, 'Invalid time limits'], + ['in', json_encode(['08:00 Europa/Berlin', '17:00 Europe/Berlin']), 3, 'Invalid timezone1'], + ['in', json_encode(['08:00 Europe/Berlin', '17:00 Europa/Berlin']), 3, 'Invalid timezone2'], + ['in', json_encode(['08:00 Europe/Bearlin', '17:00 Europe/Berlin']), 3, 'Invalid timezone1'], + ['in', json_encode(['08:00 Europe/Berlin', '17:00 Europe/Bearlin']), 3, 'Invalid timezone2'], + ]; + } + + /** + * @dataProvider dataValidateCheckInvalid + * @param string $operator + * @param string $value + * @param int $exceptionCode + * @param string $exceptionMessage + */ + public function testValidateCheckInvalid($operator, $value, $exceptionCode, $exceptionMessage) { + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + + try { + $check->validateCheck($operator, $value); + } catch (\UnexpectedValueException $e) { + $this->assertEquals($exceptionCode, $e->getCode()); + $this->assertEquals($exceptionMessage, $e->getMessage()); + } + } +} From e978c39b13e2731b74d8b64836592089e947692d Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Fri, 29 Jul 2016 10:44:38 +0200 Subject: [PATCH 25/32] add a UI to render proper time picker --- apps/workflowengine/js/requesttimeplugin.js | 105 ++++++++++++++++++-- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/apps/workflowengine/js/requesttimeplugin.js b/apps/workflowengine/js/requesttimeplugin.js index aee0e773c9e1e..96cb8a40f7f9d 100644 --- a/apps/workflowengine/js/requesttimeplugin.js +++ b/apps/workflowengine/js/requesttimeplugin.js @@ -24,6 +24,11 @@ OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {}; OCA.WorkflowEngine.Plugins.RequestTimePlugin = { + timezones: [ + "Europe/Berlin", + "Europe/London" + ], + _$element: null, getCheck: function() { return { 'class': 'OCA\\WorkflowEngine\\Check\\RequestTime', @@ -39,14 +44,98 @@ return; } - var placeholder = '["10:00 Europe\\/Berlin","16:00 Europe\\/Berlin"]'; // FIXME need a time picker JS plugin - $(element).css('width', '300px') - .attr('placeholder', placeholder) - .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}, undefined, {escape: false})) - .addClass('has-tooltip') - .tooltip({ - placement: 'bottom' - }); + var startTime = '16:00', + endTime = '18:00', + timezone = 'Europe/London', + $element = $(element); + if (_.isString(check['value']) && check['value'] !== '') { + var value = JSON.parse(check['value']), + splittedStart = value[0].split(' ', 2), + splittedEnd = value[1].split(' ', 2); + + startTime = splittedStart[0]; + endTime = splittedEnd[0]; + timezone = splittedStart[1]; + } + var valueJSON = JSON.stringify([startTime + ' ' + timezone, endTime + ' ' + timezone]); + if (check['value'] !== valueJSON) { + check['value'] = valueJSON; + $element.val(valueJSON); + } + + $element.css('display', 'none'); + + $('') + .attr('type', 'text') + .attr('placeholder', t('workflowengine', 'Start')) + .addClass('start') + .val(startTime) + .insertBefore($element); + $('') + .attr('type', 'text') + .attr('placeholder', t('workflowengine', 'End')) + .addClass('end') + .val(endTime) + .insertBefore($element); + + var timezoneSelect = $('') .attr('type', 'text') .attr('placeholder', t('workflowengine', 'Start')) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: '16:00'})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }) .addClass('start') .val(startTime) .insertBefore($element); $('') .attr('type', 'text') .attr('placeholder', t('workflowengine', 'End')) + .attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: '16:00'})) + .addClass('has-tooltip') + .tooltip({ + placement: 'bottom' + }) .addClass('end') .val(endTime) .insertBefore($element); - var timezoneSelect = $('') + .attr('type', 'hidden') + .css('width', '250px') + .insertBefore($element) + .val(timezone); + + timezoneInput.select2({ + allowClear: false, + multiple: false, + placeholder: t('workflowengine', 'Select timezone…'), + ajax: { + url: OC.generateUrl('apps/workflowengine/timezones'), + dataType: 'json', + quietMillis: 100, + data: function (term) { + if (term === '') { + // Default search in the same continent... + term = jstz.determine().name().split('/'); + term = term[0]; + } + return { + search: term + }; + }, + results: function (response) { + var results = []; + $.each(response, function(timezone) { + results.push({ id: timezone }); + }); + + return { + results: results, + more: false + }; + } + }, + initSelection: function (element, callback) { + callback(element.val()); + }, + formatResult: function (element) { + return '' + element.id + ''; + }, + formatSelection: function (element) { + if (!_.isUndefined(element.id)) { + element = element.id; + } + return '' + element + ''; } - - timezoneSelect.append(timezoneElement); }); - timezoneSelect.insertBefore($element); + + // Has to be added after select2 for `event.target.classList` + timezoneInput.addClass('timezone'); $element.parent() .on('change', '.start', _.bind(this.update, this)) @@ -136,6 +188,7 @@ } this._$element.val(JSON.stringify(data)); + this._$element.trigger('change'); } }; })(); diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php index 7adc4f6864d11..b5e769d01d72c 100644 --- a/apps/workflowengine/lib/AppInfo/Application.php +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -30,6 +30,7 @@ public function __construct() { parent::__construct('workflowengine'); $this->getContainer()->registerAlias('FlowOperationsController', 'OCA\WorkflowEngine\Controller\FlowOperations'); + $this->getContainer()->registerAlias('RequestTimeController', 'OCA\WorkflowEngine\Controller\RequestTime'); } /** @@ -51,6 +52,8 @@ function() { 'systemtags/systemtagscollection', ]); + vendor_script('jsTimezoneDetect/jstz'); + script('workflowengine', [ 'admin', diff --git a/apps/workflowengine/lib/Check/RequestTime.php b/apps/workflowengine/lib/Check/RequestTime.php index 4457f9d038c6f..a114819d450f0 100644 --- a/apps/workflowengine/lib/Check/RequestTime.php +++ b/apps/workflowengine/lib/Check/RequestTime.php @@ -29,7 +29,7 @@ class RequestTime implements ICheck { const REGEX_TIME = '([0-1][0-9]|2[0-3]):([0-5][0-9])'; - const REGEX_TIMEZONE = '([a-zA-Z]+(?:\\\\\\/[a-zA-Z\-\_]+)+)'; + const REGEX_TIMEZONE = '([a-zA-Z]+(?:\\/[a-zA-Z\-\_]+)+)'; /** @var bool[] */ protected $cachedResults; @@ -110,16 +110,15 @@ public function validateCheck($operator, $value) { throw new \UnexpectedValueException('Invalid time limits', 2); } - try { - new \DateTimeZone(stripslashes($matches[3])); - } catch(\Exception $e) { - throw new \UnexpectedValueException('Invalid timezone1', 3); + $values = json_decode($value, true); + $time1 = \DateTime::createFromFormat('H:i e', $values[0]); + if ($time1 === false) { + throw new \UnexpectedValueException('Invalid start time given', 3); } - try { - new \DateTimeZone(stripslashes($matches[6])); - } catch(\Exception $e) { - throw new \UnexpectedValueException('Invalid timezone2', 3); + $time2 = \DateTime::createFromFormat('H:i e', $values[1]); + if ($time2 === false) { + throw new \UnexpectedValueException('Invalid end time given', 3); } } } diff --git a/apps/workflowengine/lib/Controller/RequestTime.php b/apps/workflowengine/lib/Controller/RequestTime.php new file mode 100644 index 0000000000000..e596ee32a9ef0 --- /dev/null +++ b/apps/workflowengine/lib/Controller/RequestTime.php @@ -0,0 +1,53 @@ + + * + * @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 . + * + */ + +namespace OCA\WorkflowEngine\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; + +class RequestTime extends Controller { + + /** + * @NoCSRFRequired + * @NoAdminRequired + * + * @param string $search + * @return JSONResponse + */ + public function getTimezones($search = '') { + $timezones = \DateTimeZone::listIdentifiers(); + + if ($search !== '') { + $timezones = array_filter($timezones, function ($timezone) use ($search) { + return strpos(strtolower($timezone), strtolower($search)) !== false; + }); + } + + $timezones = array_slice($timezones, 0, 10); + + $response = []; + foreach ($timezones as $timezone) { + $response[$timezone] = $timezone; + } + return new JSONResponse($response); + } +} From cc719c9bb7ee8e2298dcb22c95c594586c8bf13d Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 1 Aug 2016 17:56:33 +0200 Subject: [PATCH 27/32] Translate the errors --- .../lib/Check/AbstractStringCheck.php | 15 ++++- .../workflowengine/lib/Check/FileMimeType.php | 5 +- apps/workflowengine/lib/Check/FileSize.php | 12 +++- .../lib/Check/FileSystemTags.php | 14 ++-- .../lib/Check/RequestRemoteAddress.php | 20 ++++-- apps/workflowengine/lib/Check/RequestTime.php | 15 +++-- apps/workflowengine/lib/Check/RequestURL.php | 5 +- .../lib/Check/RequestUserAgent.php | 5 +- .../lib/Check/UserGroupMembership.php | 4 +- .../tests/Check/AbstractStringCheckTest.php | 64 +++++++++---------- .../tests/Check/RequestRemoteAddressTest.php | 23 +++++-- .../tests/Check/RequestTimeTest.php | 42 ++++++++---- 12 files changed, 149 insertions(+), 75 deletions(-) diff --git a/apps/workflowengine/lib/Check/AbstractStringCheck.php b/apps/workflowengine/lib/Check/AbstractStringCheck.php index 77576266fcfeb..0fd728e349664 100644 --- a/apps/workflowengine/lib/Check/AbstractStringCheck.php +++ b/apps/workflowengine/lib/Check/AbstractStringCheck.php @@ -23,6 +23,7 @@ use OCP\Files\Storage\IStorage; +use OCP\IL10N; use OCP\WorkflowEngine\ICheck; abstract class AbstractStringCheck implements ICheck { @@ -30,6 +31,16 @@ abstract class AbstractStringCheck implements ICheck { /** @var array[] Nested array: [Pattern => [ActualValue => Regex Result]] */ protected $matches; + /** @var IL10N */ + protected $l; + + /** + * @param IL10N $l + */ + public function __construct(IL10N $l) { + $this->l = $l; + } + /** * @param IStorage $storage * @param string $path @@ -81,12 +92,12 @@ protected function executeStringCheck($operator, $checkValue, $actualValue) { */ public function validateCheck($operator, $value) { if (!in_array($operator, ['is', '!is', 'matches', '!matches'])) { - throw new \UnexpectedValueException('Invalid operator', 1); + throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1); } if (in_array($operator, ['matches', '!matches']) && @preg_match($value, null) === false) { - throw new \UnexpectedValueException('Invalid regex', 2); + throw new \UnexpectedValueException($this->l->t('The given regular expression is invalid'), 2); } } diff --git a/apps/workflowengine/lib/Check/FileMimeType.php b/apps/workflowengine/lib/Check/FileMimeType.php index c774d30a233f2..1de9a70a17d54 100644 --- a/apps/workflowengine/lib/Check/FileMimeType.php +++ b/apps/workflowengine/lib/Check/FileMimeType.php @@ -23,6 +23,7 @@ use OCP\Files\IMimeTypeDetector; +use OCP\IL10N; use OCP\IRequest; class FileMimeType extends AbstractStringCheck { @@ -37,10 +38,12 @@ class FileMimeType extends AbstractStringCheck { protected $mimeTypeDetector; /** + * @param IL10N $l * @param IRequest $request * @param IMimeTypeDetector $mimeTypeDetector */ - public function __construct(IRequest $request, IMimeTypeDetector $mimeTypeDetector) { + public function __construct(IL10N $l, IRequest $request, IMimeTypeDetector $mimeTypeDetector) { + parent::__construct($l); $this->request = $request; $this->mimeTypeDetector = $mimeTypeDetector; } diff --git a/apps/workflowengine/lib/Check/FileSize.php b/apps/workflowengine/lib/Check/FileSize.php index 70071757c12c2..1744793dec72e 100644 --- a/apps/workflowengine/lib/Check/FileSize.php +++ b/apps/workflowengine/lib/Check/FileSize.php @@ -23,6 +23,7 @@ use OCP\Files\Storage\IStorage; +use OCP\IL10N; use OCP\IRequest; use OCP\Util; use OCP\WorkflowEngine\ICheck; @@ -32,13 +33,18 @@ class FileSize implements ICheck { /** @var int */ protected $size; + /** @var IL10N */ + protected $l; + /** @var IRequest */ protected $request; /** + * @param IL10N $l * @param IRequest $request */ - public function __construct(IRequest $request) { + public function __construct(IL10N $l, IRequest $request) { + $this->l = $l; $this->request = $request; } @@ -80,11 +86,11 @@ public function executeCheck($operator, $value) { */ public function validateCheck($operator, $value) { if (!in_array($operator, ['less', '!less', 'greater', '!greater'])) { - throw new \UnexpectedValueException('Invalid operator', 1); + throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1); } if (!preg_match('/^[0-9]+[ ]?[kmgt]?b$/i', $value)) { - throw new \UnexpectedValueException('Invalid file size', 2); + throw new \UnexpectedValueException($this->l->t('The given file size is invalid'), 2); } } diff --git a/apps/workflowengine/lib/Check/FileSystemTags.php b/apps/workflowengine/lib/Check/FileSystemTags.php index 77179631fc1da..e9b5a945967a1 100644 --- a/apps/workflowengine/lib/Check/FileSystemTags.php +++ b/apps/workflowengine/lib/Check/FileSystemTags.php @@ -24,6 +24,7 @@ use OCP\Files\Cache\ICache; use OCP\Files\Storage\IStorage; +use OCP\IL10N; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\TagNotFoundException; @@ -37,6 +38,9 @@ class FileSystemTags implements ICheck { /** @var array */ protected $fileSystemTags; + /** @var IL10N */ + protected $l; + /** @var ISystemTagManager */ protected $systemTagManager; @@ -50,10 +54,12 @@ class FileSystemTags implements ICheck { protected $path; /** + * @param IL10N $l * @param ISystemTagManager $systemTagManager * @param ISystemTagObjectMapper $systemTagObjectMapper */ - public function __construct(ISystemTagManager $systemTagManager, ISystemTagObjectMapper $systemTagObjectMapper) { + public function __construct(IL10N $l, ISystemTagManager $systemTagManager, ISystemTagObjectMapper $systemTagObjectMapper) { + $this->l = $l; $this->systemTagManager = $systemTagManager; $this->systemTagObjectMapper = $systemTagObjectMapper; } @@ -84,15 +90,15 @@ public function executeCheck($operator, $value) { */ public function validateCheck($operator, $value) { if (!in_array($operator, ['is', '!is'])) { - throw new \UnexpectedValueException('Invalid operator', 1); + throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1); } try { $this->systemTagManager->getTagsByIds($value); } catch (TagNotFoundException $e) { - throw new \UnexpectedValueException('Tag does not exist', 2); + throw new \UnexpectedValueException($this->l->t('The given tag id is invalid'), 2); } catch (\InvalidArgumentException $e) { - throw new \UnexpectedValueException('Tag does not exist', 3); + throw new \UnexpectedValueException($this->l->t('The given tag id is invalid'), 3); } } diff --git a/apps/workflowengine/lib/Check/RequestRemoteAddress.php b/apps/workflowengine/lib/Check/RequestRemoteAddress.php index 7897fcbd9d3d0..de9738fb63127 100644 --- a/apps/workflowengine/lib/Check/RequestRemoteAddress.php +++ b/apps/workflowengine/lib/Check/RequestRemoteAddress.php @@ -23,18 +23,24 @@ use OCP\Files\Storage\IStorage; +use OCP\IL10N; use OCP\IRequest; use OCP\WorkflowEngine\ICheck; class RequestRemoteAddress implements ICheck { + /** @var IL10N */ + protected $l; + /** @var IRequest */ protected $request; /** + * @param IL10N $l * @param IRequest $request */ - public function __construct(IRequest $request) { + public function __construct(IL10N $l, IRequest $request) { + $this->l = $l; $this->request = $request; } @@ -73,27 +79,27 @@ public function executeCheck($operator, $value) { */ public function validateCheck($operator, $value) { if (!in_array($operator, ['matchesIPv4', '!matchesIPv4', 'matchesIPv6', '!matchesIPv6'])) { - throw new \UnexpectedValueException('Invalid operator', 1); + throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1); } $decodedValue = explode('/', $value); if (sizeof($decodedValue) !== 2) { - throw new \UnexpectedValueException('Invalid IP range', 2); + throw new \UnexpectedValueException($this->l->t('The given IP range is invalid'), 2); } if (in_array($operator, ['matchesIPv4', '!matchesIPv4'])) { if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - throw new \UnexpectedValueException('Invalid IPv4 range', 3); + throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv4'), 3); } if ($decodedValue[1] > 32 || $decodedValue[1] <= 0) { - throw new \UnexpectedValueException('Invalid IPv4 range', 4); + throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv4'), 4); } } else { if (!filter_var($decodedValue[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - throw new \UnexpectedValueException('Invalid IPv6 range', 3); + throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv6'), 3); } if ($decodedValue[1] > 128 || $decodedValue[1] <= 0) { - throw new \UnexpectedValueException('Invalid IPv6 range', 4); + throw new \UnexpectedValueException($this->l->t('The given IP range is not valid for IPv6'), 4); } } } diff --git a/apps/workflowengine/lib/Check/RequestTime.php b/apps/workflowengine/lib/Check/RequestTime.php index a114819d450f0..2aa79e77673cb 100644 --- a/apps/workflowengine/lib/Check/RequestTime.php +++ b/apps/workflowengine/lib/Check/RequestTime.php @@ -24,6 +24,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\Storage\IStorage; +use OCP\IL10N; use OCP\WorkflowEngine\ICheck; class RequestTime implements ICheck { @@ -34,13 +35,17 @@ class RequestTime implements ICheck { /** @var bool[] */ protected $cachedResults; + /** @var IL10N */ + protected $l; + /** @var ITimeFactory */ protected $timeFactory; /** * @param ITimeFactory $timeFactory */ - public function __construct(ITimeFactory $timeFactory) { + public function __construct(IL10N $l, ITimeFactory $timeFactory) { + $this->l = $l; $this->timeFactory = $timeFactory; } @@ -101,24 +106,24 @@ protected function getTimestamp($currentTimestamp, $value) { */ public function validateCheck($operator, $value) { if (!in_array($operator, ['in', '!in'])) { - throw new \UnexpectedValueException('Invalid operator', 1); + throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1); } $regexValue = '\"' . self::REGEX_TIME . ' ' . self::REGEX_TIMEZONE . '\"'; $result = preg_match('/^\[' . $regexValue . ',' . $regexValue . '\]$/', $value, $matches); if (!$result) { - throw new \UnexpectedValueException('Invalid time limits', 2); + throw new \UnexpectedValueException($this->l->t('The given time span is invalid'), 2); } $values = json_decode($value, true); $time1 = \DateTime::createFromFormat('H:i e', $values[0]); if ($time1 === false) { - throw new \UnexpectedValueException('Invalid start time given', 3); + throw new \UnexpectedValueException($this->l->t('The given start time is invalid'), 3); } $time2 = \DateTime::createFromFormat('H:i e', $values[1]); if ($time2 === false) { - throw new \UnexpectedValueException('Invalid end time given', 3); + throw new \UnexpectedValueException($this->l->t('The given end time is invalid'), 4); } } } diff --git a/apps/workflowengine/lib/Check/RequestURL.php b/apps/workflowengine/lib/Check/RequestURL.php index 0cae3cf7e5619..36d41c101f207 100644 --- a/apps/workflowengine/lib/Check/RequestURL.php +++ b/apps/workflowengine/lib/Check/RequestURL.php @@ -22,6 +22,7 @@ namespace OCA\WorkflowEngine\Check; +use OCP\IL10N; use OCP\IRequest; class RequestURL extends AbstractStringCheck { @@ -33,9 +34,11 @@ class RequestURL extends AbstractStringCheck { protected $request; /** + * @param IL10N $l * @param IRequest $request */ - public function __construct(IRequest $request) { + public function __construct(IL10N $l, IRequest $request) { + parent::__construct($l); $this->request = $request; } diff --git a/apps/workflowengine/lib/Check/RequestUserAgent.php b/apps/workflowengine/lib/Check/RequestUserAgent.php index 241b19136a7ee..7a8d4a71acfee 100644 --- a/apps/workflowengine/lib/Check/RequestUserAgent.php +++ b/apps/workflowengine/lib/Check/RequestUserAgent.php @@ -22,6 +22,7 @@ namespace OCA\WorkflowEngine\Check; +use OCP\IL10N; use OCP\IRequest; class RequestUserAgent extends AbstractStringCheck { @@ -30,9 +31,11 @@ class RequestUserAgent extends AbstractStringCheck { protected $request; /** + * @param IL10N $l * @param IRequest $request */ - public function __construct(IRequest $request) { + public function __construct(IL10N $l, IRequest $request) { + parent::__construct($l); $this->request = $request; } diff --git a/apps/workflowengine/lib/Check/UserGroupMembership.php b/apps/workflowengine/lib/Check/UserGroupMembership.php index 6390c57fbea2b..fd6ba00d09245 100644 --- a/apps/workflowengine/lib/Check/UserGroupMembership.php +++ b/apps/workflowengine/lib/Check/UserGroupMembership.php @@ -89,11 +89,11 @@ public function executeCheck($operator, $value) { */ public function validateCheck($operator, $value) { if (!in_array($operator, ['is', '!is'])) { - throw new \UnexpectedValueException($this->l->t('Operator %s is invalid', $operator), 1); + throw new \UnexpectedValueException($this->l->t('The given operator is invalid'), 1); } if (!$this->groupManager->groupExists($value)) { - throw new \UnexpectedValueException($this->l->t('Group %s does not exist', $value), 2); + throw new \UnexpectedValueException($this->l->t('The given group does not exist'), 2); } } diff --git a/apps/workflowengine/tests/Check/AbstractStringCheckTest.php b/apps/workflowengine/tests/Check/AbstractStringCheckTest.php index 43818ab8e2663..91da8931604c5 100644 --- a/apps/workflowengine/tests/Check/AbstractStringCheckTest.php +++ b/apps/workflowengine/tests/Check/AbstractStringCheckTest.php @@ -24,6 +24,30 @@ class AbstractStringCheckTest extends \Test\TestCase { + protected function getCheckMock() { + $l = $this->getMockBuilder('OCP\IL10N') + ->disableOriginalConstructor() + ->getMock(); + $l->expects($this->any()) + ->method('t') + ->willReturnCallback(function($string, $args) { + return sprintf($string, $args); + }); + + $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') + ->setConstructorArgs([ + $l, + ]) + ->setMethods([ + 'setPath', + 'executeCheck', + 'getActualValue', + ]) + ->getMock(); + + return $check; + } + public function dataExecuteStringCheck() { return [ ['is', 'same', 'same', true], @@ -46,13 +70,7 @@ public function dataExecuteStringCheck() { * @param bool $expected */ public function testExecuteStringCheck($operation, $checkValue, $actualValue, $expected) { - $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') - ->setMethods([ - 'setPath', - 'executeCheck', - 'getActualValue', - ]) - ->getMock(); + $check = $this->getCheckMock(); /** @var \OCA\WorkflowEngine\Check\AbstractStringCheck $check */ $this->assertEquals($expected, $this->invokePrivate($check, 'executeStringCheck', [$operation, $checkValue, $actualValue])); @@ -73,13 +91,7 @@ public function dataValidateCheck() { * @param string $value */ public function testValidateCheck($operator, $value) { - $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') - ->setMethods([ - 'setPath', - 'executeCheck', - 'getActualValue', - ]) - ->getMock(); + $check = $this->getCheckMock(); /** @var \OCA\WorkflowEngine\Check\AbstractStringCheck $check */ $check->validateCheck($operator, $value); @@ -87,10 +99,10 @@ public function testValidateCheck($operator, $value) { public function dataValidateCheckInvalid() { return [ - ['!!is', '', 1, 'Invalid operator'], - ['less', '', 1, 'Invalid operator'], - ['matches', '/Invalid(Regex/', 2, 'Invalid regex'], - ['!matches', '/Invalid(Regex/', 2, 'Invalid regex'], + ['!!is', '', 1, 'The given operator is invalid'], + ['less', '', 1, 'The given operator is invalid'], + ['matches', '/Invalid(Regex/', 2, 'The given regular expression is invalid'], + ['!matches', '/Invalid(Regex/', 2, 'The given regular expression is invalid'], ]; } @@ -102,13 +114,7 @@ public function dataValidateCheckInvalid() { * @param $exceptionMessage */ public function testValidateCheckInvalid($operator, $value, $exceptionCode, $exceptionMessage) { - $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') - ->setMethods([ - 'setPath', - 'executeCheck', - 'getActualValue', - ]) - ->getMock(); + $check = $this->getCheckMock(); try { /** @var \OCA\WorkflowEngine\Check\AbstractStringCheck $check */ @@ -134,13 +140,7 @@ public function dataMatch() { * @param bool $expected */ public function testMatch($pattern, $subject, $matches, $expected) { - $check = $this->getMockBuilder('OCA\WorkflowEngine\Check\AbstractStringCheck') - ->setMethods([ - 'setPath', - 'executeCheck', - 'getActualValue', - ]) - ->getMock(); + $check = $this->getCheckMock(); $this->invokePrivate($check, 'matches', [$matches]); diff --git a/apps/workflowengine/tests/Check/RequestRemoteAddressTest.php b/apps/workflowengine/tests/Check/RequestRemoteAddressTest.php index ec8798794df4c..efe8f6372ddbb 100644 --- a/apps/workflowengine/tests/Check/RequestRemoteAddressTest.php +++ b/apps/workflowengine/tests/Check/RequestRemoteAddressTest.php @@ -27,6 +27,21 @@ class RequestRemoteAddressTest extends \Test\TestCase { /** @var \OCP\IRequest|\PHPUnit_Framework_MockObject_MockObject */ protected $request; + /** + * @return \OCP\IL10N|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getL10NMock() { + $l = $this->getMockBuilder('OCP\IL10N') + ->disableOriginalConstructor() + ->getMock(); + $l->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($string, $args) { + return sprintf($string, $args); + }); + return $l; + } + protected function setUp() { parent::setUp(); @@ -52,7 +67,7 @@ public function dataExecuteCheckIPv4() { * @param bool $expected */ public function testExecuteCheckMatchesIPv4($value, $ip, $expected) { - $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->getL10NMock(), $this->request); $this->request->expects($this->once()) ->method('getRemoteAddress') @@ -68,7 +83,7 @@ public function testExecuteCheckMatchesIPv4($value, $ip, $expected) { * @param bool $expected */ public function testExecuteCheckNotMatchesIPv4($value, $ip, $expected) { - $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->getL10NMock(), $this->request); $this->request->expects($this->once()) ->method('getRemoteAddress') @@ -96,7 +111,7 @@ public function dataExecuteCheckIPv6() { * @param bool $expected */ public function testExecuteCheckMatchesIPv6($value, $ip, $expected) { - $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->getL10NMock(), $this->request); $this->request->expects($this->once()) ->method('getRemoteAddress') @@ -112,7 +127,7 @@ public function testExecuteCheckMatchesIPv6($value, $ip, $expected) { * @param bool $expected */ public function testExecuteCheckNotMatchesIPv6($value, $ip, $expected) { - $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->request); + $check = new \OCA\WorkflowEngine\Check\RequestRemoteAddress($this->getL10NMock(), $this->request); $this->request->expects($this->once()) ->method('getRemoteAddress') diff --git a/apps/workflowengine/tests/Check/RequestTimeTest.php b/apps/workflowengine/tests/Check/RequestTimeTest.php index ca279cca0c3a6..c07b4bf87754e 100644 --- a/apps/workflowengine/tests/Check/RequestTimeTest.php +++ b/apps/workflowengine/tests/Check/RequestTimeTest.php @@ -27,6 +27,21 @@ class RequestTimeTest extends \Test\TestCase { /** @var \OCP\AppFramework\Utility\ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $timeFactory; + /** + * @return \OCP\IL10N|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getL10NMock() { + $l = $this->getMockBuilder('OCP\IL10N') + ->disableOriginalConstructor() + ->getMock(); + $l->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($string, $args) { + return sprintf($string, $args); + }); + return $l; + } + protected function setUp() { parent::setUp(); @@ -72,7 +87,7 @@ public function dataExecuteCheck() { * @param bool $expected */ public function testExecuteCheckIn($value, $timestamp, $expected) { - $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->getL10NMock(), $this->timeFactory); $this->timeFactory->expects($this->once()) ->method('getTime') @@ -88,7 +103,7 @@ public function testExecuteCheckIn($value, $timestamp, $expected) { * @param bool $expected */ public function testExecuteCheckNotIn($value, $timestamp, $expected) { - $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->getL10NMock(), $this->timeFactory); $this->timeFactory->expects($this->once()) ->method('getTime') @@ -99,9 +114,9 @@ public function testExecuteCheckNotIn($value, $timestamp, $expected) { public function dataValidateCheck() { return [ - ['in', json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin'])], - ['!in', json_encode(['08:00 Europe/Berlin', '17:00 America/North_Dakota/Beulah'])], - ['in', json_encode(['08:00 America/Port-au-Prince', '17:00 America/Argentina/San_Luis'])], + ['in', '["08:00 Europe/Berlin","17:00 Europe/Berlin"]'], + ['!in', '["08:00 Europe/Berlin","17:00 America/North_Dakota/Beulah"]'], + ['in', '["08:00 America/Port-au-Prince","17:00 America/Argentina/San_Luis"]'], ]; } @@ -111,18 +126,19 @@ public function dataValidateCheck() { * @param string $value */ public function testValidateCheck($operator, $value) { - $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->getL10NMock(), $this->timeFactory); $check->validateCheck($operator, $value); } public function dataValidateCheckInvalid() { return [ - ['!!in', json_encode(['08:00 Europe/Berlin', '17:00 Europe/Berlin']), 1, 'Invalid operator'], - ['in', json_encode(['28:00 Europe/Berlin', '17:00 Europe/Berlin']), 2, 'Invalid time limits'], - ['in', json_encode(['08:00 Europa/Berlin', '17:00 Europe/Berlin']), 3, 'Invalid timezone1'], - ['in', json_encode(['08:00 Europe/Berlin', '17:00 Europa/Berlin']), 3, 'Invalid timezone2'], - ['in', json_encode(['08:00 Europe/Bearlin', '17:00 Europe/Berlin']), 3, 'Invalid timezone1'], - ['in', json_encode(['08:00 Europe/Berlin', '17:00 Europe/Bearlin']), 3, 'Invalid timezone2'], + ['!!in', '["08:00 Europe/Berlin","17:00 Europe/Berlin"]', 1, 'The given operator is invalid'], + ['in', '["28:00 Europe/Berlin","17:00 Europe/Berlin"]', 2, 'The given time span is invalid'], + ['in', '["08:00 Europe/Berlin","27:00 Europe/Berlin"]', 2, 'The given time span is invalid'], + ['in', '["08:00 Europa/Berlin","17:00 Europe/Berlin"]', 3, 'The given start time is invalid'], + ['in', '["08:00 Europe/Berlin","17:00 Europa/Berlin"]', 4, 'The given end time is invalid'], + ['in', '["08:00 Europe/Bearlin","17:00 Europe/Berlin"]', 3, 'The given start time is invalid'], + ['in', '["08:00 Europe/Berlin","17:00 Europe/Bearlin"]', 4, 'The given end time is invalid'], ]; } @@ -134,7 +150,7 @@ public function dataValidateCheckInvalid() { * @param string $exceptionMessage */ public function testValidateCheckInvalid($operator, $value, $exceptionCode, $exceptionMessage) { - $check = new \OCA\WorkflowEngine\Check\RequestTime($this->timeFactory); + $check = new \OCA\WorkflowEngine\Check\RequestTime($this->getL10NMock(), $this->timeFactory); try { $check->validateCheck($operator, $value); From c12c083ebacbcd98705b8a5d06af15deff80db14 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 1 Aug 2016 18:09:22 +0200 Subject: [PATCH 28/32] Tags need to be loaded before --- apps/workflowengine/js/filesystemtagsplugin.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/workflowengine/js/filesystemtagsplugin.js b/apps/workflowengine/js/filesystemtagsplugin.js index 026345571e793..684b854f68af0 100644 --- a/apps/workflowengine/js/filesystemtagsplugin.js +++ b/apps/workflowengine/js/filesystemtagsplugin.js @@ -26,11 +26,6 @@ OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin = { getCheck: function() { this.collection = OC.SystemTags.collection; - this.collection.fetch({ - success: function() { - console.log('done loading tags'); - } - }); return { 'class': 'OCA\\WorkflowEngine\\Check\\FileSystemTags', From 7d71535f57b5bde6b6a61db4cdf9161548ca3bfc Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 2 Aug 2016 09:56:19 +0200 Subject: [PATCH 29/32] Allow to define the operation --- apps/workflowengine/js/admin.js | 17 +++++++---------- apps/workflowengine/js/filesystemtagsplugin.js | 4 ++-- apps/workflowengine/templates/admin.php | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js index 447237c5600f0..2c6fa54d87dba 100644 --- a/apps/workflowengine/js/admin.js +++ b/apps/workflowengine/js/admin.js @@ -137,6 +137,7 @@ 'change .check-operator': 'checkChanged', 'change .check-value': 'checkChanged', 'change .operation-name': 'operationChanged', + 'change .operation-operation': 'operationChanged', 'click .button-reset': 'reset', 'click .button-save': 'save', 'click .button-add': 'add', @@ -269,7 +270,7 @@ return; } - if (key !== 'name') { + if (key !== 'name' && key !== 'operation') { console.warn('key "' + key + '" is no valid attribute'); return; } @@ -308,6 +309,8 @@ }, 7000, this.$el.find('.msg.success')); this.message = ''; } + + return this.$el; } }); @@ -324,10 +327,7 @@ events: { 'click .button-add-operation': 'add' }, - initialize: function() { - this._initialize('OCA\\WorkflowEngine\\Operation'); - }, - _initialize: function(classname) { + initialize: function(classname) { if (!OCA.WorkflowEngine.availablePlugins.length) { OCA.WorkflowEngine.availablePlugins = OC.Plugins.getPlugins('OCA.WorkflowEngine.CheckPlugins'); _.each(OCA.WorkflowEngine.availablePlugins, function(plugin) { @@ -346,11 +346,8 @@ var operation = this.collection.create(); this.renderOperation(operation); }, - renderOperation: function(operation){ - var subView = new OCA.WorkflowEngine.OperationView({ - model: operation - }), - operationsElement = this.$el.find('.operations'); + renderOperation: function(subView){ + var operationsElement = this.$el.find('.operations'); operationsElement.append(subView.$el); subView.render(); }, diff --git a/apps/workflowengine/js/filesystemtagsplugin.js b/apps/workflowengine/js/filesystemtagsplugin.js index 684b854f68af0..bca890e3c319d 100644 --- a/apps/workflowengine/js/filesystemtagsplugin.js +++ b/apps/workflowengine/js/filesystemtagsplugin.js @@ -49,7 +49,7 @@ placeholder: t('workflowengine', 'Select tag…'), query: _.debounce(function(query) { query.callback({ - results: OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin.collection.filterByName(query.term) + results: OC.SystemTags.collection.filterByName(query.term) }); }, 100, true), id: function(element) { @@ -63,7 +63,7 @@ return OC.SystemTags.getDescriptiveTag(tag); }, formatSelection: function (tagId) { - tag = OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin.collection.get(tagId); + var tag = OC.SystemTags.collection.get(tagId); return OC.SystemTags.getDescriptiveTag(tag); }, escapeMarkup: function(m) { diff --git a/apps/workflowengine/templates/admin.php b/apps/workflowengine/templates/admin.php index 4eabf783bbae0..935e8c70f17ca 100644 --- a/apps/workflowengine/templates/admin.php +++ b/apps/workflowengine/templates/admin.php @@ -36,7 +36,7 @@ {{#if operation.id}} {{/if}} - {{operation.class}} - ID: {{operation.id}} - operation: {{operation.operation}} +
{{#each operation.checks}} From 065763f117cf788ca1b93a8ca37711900d20543f Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 2 Aug 2016 10:41:13 +0200 Subject: [PATCH 30/32] No multi support, less magic --- apps/workflowengine/js/filesystemtagsplugin.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/workflowengine/js/filesystemtagsplugin.js b/apps/workflowengine/js/filesystemtagsplugin.js index bca890e3c319d..dc6f608d85a3e 100644 --- a/apps/workflowengine/js/filesystemtagsplugin.js +++ b/apps/workflowengine/js/filesystemtagsplugin.js @@ -56,8 +56,7 @@ return element.get('id'); }, initSelection: function(element, callback) { - var selection = ($(element).val() || []).split('|').sort(); - callback(selection); + callback($(element).val()); }, formatResult: function (tag) { return OC.SystemTags.getDescriptiveTag(tag); From 8d23405602f4d970277829548967e7d9abe6950e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 2 Aug 2016 12:52:46 +0200 Subject: [PATCH 31/32] Ooops --- apps/workflowengine/lib/Controller/RequestTime.php | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/workflowengine/lib/Controller/RequestTime.php b/apps/workflowengine/lib/Controller/RequestTime.php index e596ee32a9ef0..dd0efa89b9158 100644 --- a/apps/workflowengine/lib/Controller/RequestTime.php +++ b/apps/workflowengine/lib/Controller/RequestTime.php @@ -27,7 +27,6 @@ class RequestTime extends Controller { /** - * @NoCSRFRequired * @NoAdminRequired * * @param string $search From fc7bd0382205ed80d4c80b4e58ad1efb0ed3539e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 3 Aug 2016 14:47:52 +0200 Subject: [PATCH 32/32] Add access control and automated tagging as shipped --- core/shipped.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/shipped.json b/core/shipped.json index 8d3056eb90833..20041be7e19fa 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -9,6 +9,8 @@ "federatedfilesharing", "federation", "files", + "files_accesscontrol", + "files_automatedtagging", "files_external", "files_pdfviewer", "files_sharing",