diff --git a/application/classes/Controller/Api/Forms/Roles.php b/application/classes/Controller/Api/Forms/Roles.php new file mode 100644 index 0000000000..4d7554b067 --- /dev/null +++ b/application/classes/Controller/Api/Forms/Roles.php @@ -0,0 +1,51 @@ + + * @package Ushahidi\Application\Controllers + * @copyright 2013 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +class Controller_API_Forms_Roles extends Ushahidi_Rest { + + protected $_action_map = array + ( + Http_Request::GET => 'get', + Http_Request::PUT => 'put', // Typically Update.. + Http_Request::OPTIONS => 'options' + ); + + protected function _scope() + { + return 'forms'; + } + + protected function _resource() + { + return 'form_roles'; + } + + // Ushahidi_Rest + public function action_get_index_collection() + { + parent::action_get_index_collection(); + + $this->_usecase->setIdentifiers($this->request->param()); + $this->_usecase->setFilters($this->request->query() + [ + 'form_id' => $this->request->param('form_id') + ]); + } + + // Ushahidi_Rest + public function action_put_index_collection() + { + $this->_usecase = service('factory.usecase') + ->get($this->_resource(), 'update_collection') + ->setIdentifiers($this->_identifiers()) + ->setPayload($this->_payload()); + } + +} diff --git a/application/classes/Ushahidi/Core.php b/application/classes/Ushahidi/Core.php index ea93760a80..d51f45b38c 100644 --- a/application/classes/Ushahidi/Core.php +++ b/application/classes/Ushahidi/Core.php @@ -197,6 +197,10 @@ public static function init() 'create' => $di->lazyNew('Ushahidi_Validator_Form_Attribute_Create'), 'update' => $di->lazyNew('Ushahidi_Validator_Form_Attribute_Update'), ]; + $di->params['Ushahidi\Factory\ValidatorFactory']['map']['form_roles'] = [ + 'create' => $di->lazyNew('Ushahidi_Validator_Form_Role_Create'), + 'update_collection' => $di->lazyNew('Ushahidi_Validator_Form_Role_Update'), + ]; $di->params['Ushahidi\Factory\ValidatorFactory']['map']['form_stages'] = [ 'create' => $di->lazyNew('Ushahidi_Validator_Form_Stage_Create'), 'update' => $di->lazyNew('Ushahidi_Validator_Form_Stage_Update'), @@ -275,6 +279,7 @@ public static function init() 'dataproviders' => $di->lazyNew('Ushahidi_Formatter_Dataprovider'), 'forms' => $di->lazyNew('Ushahidi_Formatter_Form'), 'form_attributes' => $di->lazyNew('Ushahidi_Formatter_Form_Attribute'), + 'form_roles' => $di->lazyNew('Ushahidi_Formatter_Form_Role'), 'form_stages' => $di->lazyNew('Ushahidi_Formatter_Form_Stage'), 'layers' => $di->lazyNew('Ushahidi_Formatter_Layer'), 'media' => $di->lazyNew('Ushahidi_Formatter_Media'), @@ -301,6 +306,7 @@ public static function init() 'dataprovider', 'form', 'form_attribute', + 'form_role', 'form_stage', 'layer', 'media', @@ -400,6 +406,7 @@ public static function init() $di->set('repository.contact', $di->lazyNew('Ushahidi_Repository_Contact')); $di->set('repository.dataprovider', $di->lazyNew('Ushahidi_Repository_Dataprovider')); $di->set('repository.form', $di->lazyNew('Ushahidi_Repository_Form')); + $di->set('repository.form_role', $di->lazyNew('Ushahidi_Repository_Form_Role')); $di->set('repository.form_stage', $di->lazyNew('Ushahidi_Repository_Form_Stage')); $di->set('repository.form_attribute', $di->lazyNew('Ushahidi_Repository_Form_Attribute')); $di->set('repository.layer', $di->lazyNew('Ushahidi_Repository_Layer')); @@ -446,6 +453,7 @@ public static function init() $di->params['Ushahidi_Repository_Post'] = [ 'form_attribute_repo' => $di->lazyGet('repository.form_attribute'), 'form_stage_repo' => $di->lazyGet('repository.form_stage'), + 'form_repo' => $di->lazyGet('repository.form'), 'post_value_factory' => $di->lazyGet('repository.post_value_factory'), 'bounding_box_factory' => $di->newFactory('Util_BoundingBox'), 'tag_repo' => $di->lazyGet('repository.tag') @@ -579,6 +587,10 @@ public static function init() $di->setter['Ushahidi_Validator_Form_Stage_Update'] = [ 'setFormRepo' => $di->lazyGet('repository.form'), ]; + $di->setter['Ushahidi_Validator_Form_Role_Update'] = [ + 'setFormRepo' => $di->lazyGet('repository.form'), + 'setRoleRepo' => $di->lazyGet('repository.role'), + ]; $di->setter['Ushahidi_Validator_Media_Create'] = [ 'setMaxBytes' => $di->lazy(function() { return \Kohana::$config->load('media.max_upload_bytes'); diff --git a/application/classes/Ushahidi/Formatter/Form/Role.php b/application/classes/Ushahidi/Formatter/Form/Role.php new file mode 100644 index 0000000000..7acfaee43c --- /dev/null +++ b/application/classes/Ushahidi/Formatter/Form/Role.php @@ -0,0 +1,31 @@ + + * @package Ushahidi\Application + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +use Ushahidi\Core\Traits\FormatterAuthorizerMetadata; + +class Ushahidi_Formatter_Form_Role extends Ushahidi_Formatter_API +{ + use FormatterAuthorizerMetadata; + + public function __invoke($entity) + { + $data = [ + 'id' => $entity->id, + 'url' => URL::site('forms/' . $entity->form_id . '/roles/' . $entity->id, Request::current()), + 'form_id' => $entity->form_id, + 'role_id' => $entity->role_id, + ]; + + $data = $this->add_metadata($data, $entity); + + return $data; + } +} diff --git a/application/classes/Ushahidi/Repository/Form.php b/application/classes/Ushahidi/Repository/Form.php index d11d096c7f..61f14a66d4 100644 --- a/application/classes/Ushahidi/Repository/Form.php +++ b/application/classes/Ushahidi/Repository/Form.php @@ -15,62 +15,105 @@ use Ushahidi\Core\SearchData; class Ushahidi_Repository_Form extends Ushahidi_Repository implements - FormRepository + FormRepository { - // Ushahidi_Repository - protected function getTable() - { - return 'forms'; - } - - // CreateRepository - // ReadRepository - public function getEntity(Array $data = null) - { - return new Form($data); - } - - // SearchRepository - public function getSearchFields() - { - return ['parent', 'q' /* LIKE name */]; - } - - // Ushahidi_Repository - protected function setSearchConditions(SearchData $search) - { - $query = $this->search_query; - - if ($search->parent) { - $query->where('parent_id', '=', $search->parent); - } - - if ($search->q) { - // Form text searching - $query->where('name', 'LIKE', "%{$search->q}%"); - } - } - - // CreateRepository - public function create(Entity $entity) - { - // todo ensure default group is created - return parent::create($entity->setState(['created' => time()])); - } - - // UpdateRepository - public function update(Entity $entity) - { - return parent::update($entity->setState(['updated' => time()])); - } - - /** - * Get total count of entities - * @param Array $where - * @return int - */ - public function getTotalCount(Array $where = []) - { - return $this->selectCount($where); - } + // Ushahidi_Repository + protected function getTable() + { + return 'forms'; + } + + // CreateRepository + // ReadRepository + public function getEntity(Array $data = null) + { + if (isset($data["id"])) { + $can_create = $this->getRolesThatCanCreatePosts($data['id']); + $data = $data + [ + 'can_create' => $can_create['roles'], + ]; + } + return new Form($data); + } + + // SearchRepository + public function getSearchFields() + { + return ['parent', 'q' /* LIKE name */]; + } + + // Ushahidi_Repository + protected function setSearchConditions(SearchData $search) + { + $query = $this->search_query; + + if ($search->parent) { + $query->where('parent_id', '=', $search->parent); + } + + if ($search->q) { + // Form text searching + $query->where('name', 'LIKE', "%{$search->q}%"); + } + } + + // CreateRepository + public function create(Entity $entity) + { + // todo ensure default group is created + + return parent::create($entity->setState(['created' => time()])); + } + + // UpdateRepository + public function update(Entity $entity) + { + return parent::update($entity->setState(['updated' => time()])); + } + + /** + * Get total count of entities + * @param Array $where + * @return int + */ + public function getTotalCount(Array $where = []) + { + return $this->selectCount($where); + } + + /** + * Get `everyone_can_create` and list of roles that have access to post to the form + * @param $form_id + * @return Array + */ + public function getRolesThatCanCreatePosts($form_id) + { + $query = DB::select('forms.everyone_can_create', 'roles.name') + ->distinct(TRUE) + ->from('forms') + ->join('form_roles', 'LEFT') + ->on('forms.id', '=', 'form_roles.form_id') + ->join('roles', 'LEFT') + ->on('roles.id', '=', 'form_roles.role_id') + ->where('forms.id', '=', $form_id); + + $results = $query->execute($this->db)->as_array(); + + $everyone_can_create = (count($results) == 0 ? 1 : $results[0]['everyone_can_create']); + + $roles = []; + + foreach($results as $role) { + if (!is_null($role['name'])) { + $roles[] = $role['name']; + } + } + + return [ + 'everyone_can_create' => $everyone_can_create, + 'roles' => $roles, + ]; + + } + } diff --git a/application/classes/Ushahidi/Repository/Form/Attribute.php b/application/classes/Ushahidi/Repository/Form/Attribute.php index 9c855cdf84..27328d2745 100644 --- a/application/classes/Ushahidi/Repository/Form/Attribute.php +++ b/application/classes/Ushahidi/Repository/Form/Attribute.php @@ -52,7 +52,7 @@ protected function setSearchConditions(SearchData $search) 'key', 'label', 'input', 'type' ] as $key) { if (isset($search->$key)) { - $query->where($key, '=', $search->$key); + $query->where('form_attributes.'.$key, '=', $search->$key); } } diff --git a/application/classes/Ushahidi/Repository/Form/Role.php b/application/classes/Ushahidi/Repository/Form/Role.php new file mode 100644 index 0000000000..b06592509d --- /dev/null +++ b/application/classes/Ushahidi/Repository/Form/Role.php @@ -0,0 +1,98 @@ + + * @package Ushahidi\Application + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +use Ushahidi\Core\Data; +use Ushahidi\Core\Entity; +use Ushahidi\Core\SearchData; +use Ushahidi\Core\Entity\FormRole; +use Ushahidi\Core\Entity\FormRoleRepository; + +class Ushahidi_Repository_Form_Role extends Ushahidi_Repository implements + FormRoleRepository +{ + // Ushahidi_Repository + protected function getTable() + { + return 'form_roles'; + } + + // CreateRepository + // ReadRepository + public function getEntity(Array $data = null) + { + return new FormRole($data); + } + + // SearchRepository + public function getSearchFields() + { + return ['form_id', 'roles']; + } + + // Ushahidi_Repository + protected function setSearchConditions(SearchData $search) + { + $query = $this->search_query; + + if ($search->form_id) { + $query->where('form_id', '=', $search->form_id); + } + + if ($search->roles) { + $query->where('role_id', 'in', $search->roles); + } + } + + // FormRoleRepository + public function updateCollection(Array $entities) + { + if (empty($entities)) { + return; + } + + // Delete all existing form roles records + // Assuming all entites have the same form id + $this->deleteAllForForm(current($entities)->form_id); + + $query = DB::insert($this->getTable()) + ->columns(array_keys(current($entities)->asArray())); + + foreach($entities as $entity) { + $query->values($entity->asArray()); + } + + $query->execute($this->db); + + return $entities; + } + + // FormRoleRepository + public function getByForm($form_id) + { + $query = $this->selectQuery(compact($form_id)); + $results = $query->execute($this->db); + + return $this->getCollection($results->as_array()); + } + + // ValuesForFormRoleRepository + public function deleteAllForForm($form_id) + { + return $this->executeDelete(compact('form_id')); + } + + // FormRoleRepository + public function existsInFormRole($role_id, $form_id) + { + return (bool) $this->selectCount(compact('role_id', 'form_id')); + } + +} diff --git a/application/classes/Ushahidi/Repository/Post.php b/application/classes/Ushahidi/Repository/Post.php index 4c940a54dd..8d1d14be9c 100644 --- a/application/classes/Ushahidi/Repository/Post.php +++ b/application/classes/Ushahidi/Repository/Post.php @@ -10,6 +10,7 @@ */ use Ushahidi\Core\Entity; +use Ushahidi\Core\Entity\FormRepository; use Ushahidi\Core\Entity\FormAttributeRepository; use Ushahidi\Core\Entity\FormStageRepository; use Ushahidi\Core\Entity\Post; @@ -52,6 +53,7 @@ class Ushahidi_Repository_Post extends Ushahidi_Repository implements protected $form_attribute_repo; protected $form_stage_repo; + protected $form_repo; protected $post_value_factory; protected $bounding_box_factory; protected $tag_repo; @@ -71,6 +73,7 @@ public function __construct( Database $db, FormAttributeRepository $form_attribute_repo, FormStageRepository $form_stage_repo, + FormRepository $form_repo, Ushahidi_Repository_Post_ValueFactory $post_value_factory, InstanceFactory $bounding_box_factory, UpdatePostTagRepository $tag_repo @@ -80,6 +83,7 @@ public function __construct( $this->form_attribute_repo = $form_attribute_repo; $this->form_stage_repo = $form_stage_repo; + $this->form_repo = $form_repo; $this->post_value_factory = $post_value_factory; $this->bounding_box_factory = $bounding_box_factory; $this->tag_repo = $tag_repo; @@ -209,7 +213,11 @@ protected function setSearchConditions(SearchData $search) $status = $search->getFilter('status', 'published'); if ($status !== 'all') { - $query->where("$table.status", '=', $status); + if (!is_array($status)) { + $status = explode(',', $status); + } + + $query->where("$table.status", 'IN', $status); } foreach (['type', 'locale', 'slug'] as $key) @@ -979,4 +987,16 @@ public function getPostInSet($post_id, $set_id) return $this->getEntity($result); } + + // PostRepository + public function doesPostRequireApproval($formId) + { + if ($formId) + { + $form = $this->form_repo->get($formId); + return $form->require_approval; + } + + return true; + } } diff --git a/application/classes/Ushahidi/Repository/Role.php b/application/classes/Ushahidi/Repository/Role.php index 946ec4c69b..130382987b 100644 --- a/application/classes/Ushahidi/Repository/Role.php +++ b/application/classes/Ushahidi/Repository/Role.php @@ -156,6 +156,13 @@ public function exists($role = '') if (!$role) { return false; } return (bool) $this->selectCount(['name' => $role]); } + + // Ushahidi_Repository + public function idExists($role_id = null) + { + if (!$role_id) { return false; } + return (bool) $this->selectCount(['id' => $role_id]); + } // RoleRepository public function getByName($name) diff --git a/application/classes/Ushahidi/ValidationEngine.php b/application/classes/Ushahidi/ValidationEngine.php index 8825d6843b..7c81f78a6d 100644 --- a/application/classes/Ushahidi/ValidationEngine.php +++ b/application/classes/Ushahidi/ValidationEngine.php @@ -31,4 +31,22 @@ public function getData($key = null) return null; } + + public function setFullData(Array $fullData) + { + $this->bind(':fulldata', $fullData); + } + + public function getFullData($key = null) + { + if ($key === null) { + return $this->_bound[':fulldata']; + } + + if (array_key_exists($key, $this->_bound[':fulldata'])) { + return $this->_bound[':fulldata'][$key]; + } + + return null; + } } diff --git a/application/classes/Ushahidi/Validator/Config/Update.php b/application/classes/Ushahidi/Validator/Config/Update.php index 30fea171ea..6a69566df7 100644 --- a/application/classes/Ushahidi/Validator/Config/Update.php +++ b/application/classes/Ushahidi/Validator/Config/Update.php @@ -19,8 +19,7 @@ class Ushahidi_Validator_Config_Update extends Validator protected function getRules() { - $data = $this->validation_engine->getData(); - $config_group = isset($data['id']) ? $data['id'] : false; + $config_group = $this->validation_engine->getFullData('id'); switch($config_group) { case 'site': diff --git a/application/classes/Ushahidi/Validator/Form/Role/Create.php b/application/classes/Ushahidi/Validator/Form/Role/Create.php new file mode 100644 index 0000000000..783b6f2481 --- /dev/null +++ b/application/classes/Ushahidi/Validator/Form/Role/Create.php @@ -0,0 +1,18 @@ + + * @package Ushahidi\Application + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +use Ushahidi\Core\Entity; + +class Ushahidi_Validator_Form_Role_Create extends Ushahidi_Validator_Form_Role_Update +{ + protected $default_error_source = 'form_role'; + +} diff --git a/application/classes/Ushahidi/Validator/Form/Role/Update.php b/application/classes/Ushahidi/Validator/Form/Role/Update.php new file mode 100644 index 0000000000..1757ee2b50 --- /dev/null +++ b/application/classes/Ushahidi/Validator/Form/Role/Update.php @@ -0,0 +1,45 @@ + + * @package Ushahidi\Application + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +use Ushahidi\Core\Entity; +use Ushahidi\Core\Entity\FormRepository; +use Ushahidi\Core\Entity\RoleRepository; +use Ushahidi\Core\Tool\Validator; + +class Ushahidi_Validator_Form_Role_Update extends Validator +{ + protected $form_repo; + protected $role_repo; + protected $default_error_source = 'form_role'; + + public function setFormRepo(FormRepository $form_repo) + { + $this->form_repo = $form_repo; + } + + public function setRoleRepo(RoleRepository $role_repo) + { + $this->role_repo = $role_repo; + } + + protected function getRules() + { + return [ + 'form_id' => [ + ['digit'], + [[$this->form_repo, 'exists'], [':value']], + ], + 'role_id' => [ + [[$this->role_repo, 'idExists'], [':value']], + ], + ]; + } +} diff --git a/application/classes/Ushahidi/Validator/Form/Stage/Create.php b/application/classes/Ushahidi/Validator/Form/Stage/Create.php index eefc22cfec..51ad123c0d 100644 --- a/application/classes/Ushahidi/Validator/Form/Stage/Create.php +++ b/application/classes/Ushahidi/Validator/Form/Stage/Create.php @@ -24,6 +24,12 @@ protected function getRules() 'label' => [ ['not_empty'], ], + 'type' => [ + ['in_array', [':value', [ + 'post', + 'task' + ]]], + ], ]; } } diff --git a/application/classes/Ushahidi/Validator/Post/Create.php b/application/classes/Ushahidi/Validator/Post/Create.php index a9fc665e02..b7b0eddffd 100644 --- a/application/classes/Ushahidi/Validator/Post/Create.php +++ b/application/classes/Ushahidi/Validator/Post/Create.php @@ -18,11 +18,26 @@ use Ushahidi\Core\Entity\RoleRepository; use Ushahidi\Core\Entity\PostSearchData; use Ushahidi\Core\Tool\Validator; +use Ushahidi\Core\Traits\UserContext; +use Ushahidi\Core\Traits\PermissionAccess; +use Ushahidi\Core\Traits\AdminAccess; +use Ushahidi\Core\Traits\Permissions\ManagePosts; use Ushahidi\Core\Usecase\Post\UpdatePostRepository; use Ushahidi\Core\Usecase\Post\UpdatePostTagRepository; class Ushahidi_Validator_Post_Create extends Validator { + use UserContext; + + // Provides `hasPermission` + use PermissionAccess; + + // Checks if user is Admin + use AdminAccess; + + // Provides `getPermission` + use ManagePosts; + protected $repo; protected $attribute_repo; protected $stage_repo; @@ -69,10 +84,8 @@ public function __construct( protected function getRules() { - $input = $this->validation_engine->getData(); - $parent_id = isset($input['parent_id']) ? $input['parent_id'] : null; - $type = isset($input['type']) ? $input['type'] : null; - $form_id = isset($input['form_id']) ? $input['form_id'] : null; + $parent_id = $this->validation_engine->getFullData('parent_id'); + $type = $this->validation_engine->getFullData('type'); return [ 'title' => [ @@ -99,15 +112,15 @@ protected function getRules() [[$this->form_repo, 'exists'], [':value']], ], 'values' => [ - [[$this, 'checkValues'], [':validation', ':value', ':data']], - [[$this, 'checkRequiredAttributes'], [':validation', ':value', ':data']], + [[$this, 'checkValues'], [':validation', ':value', ':fulldata']], + [[$this, 'checkRequiredAttributes'], [':validation', ':value', ':fulldata']], ], 'tags' => [ [[$this, 'checkTags'], [':validation', ':value']], ], 'user_id' => [ [[$this->user_repo, 'exists'], [':value']], - [[$this, 'onlyAuthorOrUserSet'], [':value', ':data']], + [[$this, 'onlyAuthorOrUserSet'], [':value', ':fulldata']], ], 'author_email' => [ ['Valid::email'], @@ -117,9 +130,11 @@ protected function getRules() ], 'status' => [ ['in_array', [':value', [ + 'published', 'draft', - 'published' + 'archived' ]]], + [[$this, 'checkApprovalRequired'], [':validation', ':value', ':fulldata']], [[$this, 'checkPublishedLimit'], [':validation', ':value']] ], 'type' => [ @@ -133,24 +148,50 @@ protected function getRules() [[$this->role_repo, 'exists'], [':value']], ], 'completed_stages' => [ - [[$this, 'checkStageInForm'], [':validation', ':value', ':data']], - [[$this, 'checkRequiredStages'], [':validation', ':value', ':data']] + [[$this, 'checkStageInForm'], [':validation', ':value', ':fulldata']], + [[$this, 'checkRequiredStages'], [':validation', ':value', ':fulldata']] ] ]; } - public function checkPublishedLimit (Validation $validation, $status) - { - $config = \Kohana::$config->load('features.limits'); + public function checkPublishedLimit (Validation $validation, $status) + { + $config = \Kohana::$config->load('features.limits'); + + if ($config['posts'] !== TRUE && $status == 'published') { + $total_published = $this->repo->getPublishedTotal(); - if ($config['posts'] !== TRUE && $status == 'published') { - $total_published = $this->repo->getPublishedTotal(); + if ($total_published >= $config['posts']) { + $validation->error('status', 'publishedPostsLimitReached'); + } + } + } - if ($total_published >= $config['posts']) { - $validation->error('status', 'publishedPostsLimitReached'); - } - } - } + public function checkApprovalRequired (Validation $validation, $status, $fullData) + { + // Status hasn't changed, moving on + if (!$status) { + return; + } + + $user = $this->getUser(); + // Do we have permission to publish this post? + $userCanChangeStatus = ($this->isUserAdmin($user) or $this->hasPermission($user)); + // .. if yes, any status is ok. + if ($userCanChangeStatus) { + return; + } + + $requireApproval = $this->repo->doesPostRequireApproval($fullData['form_id']); + + // Are we trying to change publish a post that requires approval? + if ($requireApproval && $status !== 'draft') { + $validation->error('status', 'postNeedsApprovalBeforePublishing'); + // Are we trying to unpublish or archive an auto-approved post? + } elseif (!$requireApproval && $status !== 'published') { + $validation->error('status', 'postCanOnlyBeUnpublishedByAdmin'); + } + } public function checkTags(Validation $validation, $tags) { @@ -171,19 +212,19 @@ public function checkTags(Validation $validation, $tags) } } - public function checkValues(Validation $validation, $attributes, $data) + public function checkValues(Validation $validation, $attributes, $fullData) { if (!$attributes) { return; } - $post_id = ! empty($data['id']) ? $data['id'] : 0; + $post_id = ! empty($fullData['id']) ? $fullData['id'] : 0; foreach ($attributes as $key => $values) { // Check attribute exists - $attribute = $this->attribute_repo->getByKey($key, $data['form_id'], true); + $attribute = $this->attribute_repo->getByKey($key, $fullData['form_id'], true); if (! $attribute->id) { $validation->error('values', 'attributeDoesNotExist', [$key]); @@ -222,9 +263,9 @@ public function checkValues(Validation $validation, $attributes, $data) * * @param Validation $validation * @param Array $attributes - * @param Array $data + * @param Array $fullData */ - public function checkStageInForm(Validation $validation, $completed_stages, $data) + public function checkStageInForm(Validation $validation, $completed_stages, $fullData) { if (!$completed_stages) { @@ -234,7 +275,7 @@ public function checkStageInForm(Validation $validation, $completed_stages, $dat foreach ($completed_stages as $stage_id) { // Check stage exists in form - if (! $this->stage_repo->existsInForm($stage_id, $data['form_id'])) + if (! $this->stage_repo->existsInForm($stage_id, $fullData['form_id'])) { $validation->error('completed_stages', 'stageDoesNotExist', [$stage_id]); return; @@ -247,17 +288,17 @@ public function checkStageInForm(Validation $validation, $completed_stages, $dat * * @param Validation $validation * @param Array $attributes - * @param Array $data + * @param Array $fullData */ - public function checkRequiredStages(Validation $validation, $completed_stages, $data) + public function checkRequiredStages(Validation $validation, $completed_stages, $fullData) { $completed_stages = $completed_stages ? $completed_stages : []; // If post is being published - if ($data['status'] === 'published') + if ($fullData['status'] === 'published') { // Load the required stages - $required_stages = $this->stage_repo->getRequired($data['form_id']); + $required_stages = $this->stage_repo->getRequired($fullData['form_id']); foreach ($required_stages as $stage) { // Check the required stages have been completed @@ -275,18 +316,18 @@ public function checkRequiredStages(Validation $validation, $completed_stages, $ * * @param Validation $validation * @param Array $attributes - * @param Array $data + * @param Array $fullData */ - public function checkRequiredAttributes(Validation $validation, $attributes, $data) + public function checkRequiredAttributes(Validation $validation, $attributes, $fullData) { - if (empty($data['completed_stages'])) + if (empty($fullData['completed_stages'])) { return; } // If a stage is being marked completed // Check if the required attribute have been completed - foreach ($data['completed_stages'] as $stage_id) + foreach ($fullData['completed_stages'] as $stage_id) { // Load the required attributes $required_attributes = $this->attribute_repo->getRequired($stage_id); @@ -307,11 +348,11 @@ public function checkRequiredAttributes(Validation $validation, $attributes, $da /** * Check that only author or user info is set * @param int $user_id - * @param array $data + * @param array $fullData * @return Boolean */ - public function onlyAuthorOrUserSet($user_id, $data) + public function onlyAuthorOrUserSet($user_id, $fullData) { - return (empty($user_id) OR (empty($data['author_email']) AND empty($data['author_realname'])) ); + return (empty($user_id) OR (empty($fullData['author_email']) AND empty($fullData['author_realname'])) ); } } diff --git a/application/messages/form_role.php b/application/messages/form_role.php new file mode 100644 index 0000000000..707ca19d2c --- /dev/null +++ b/application/messages/form_role.php @@ -0,0 +1,7 @@ + [ + 'roleDoesNotExist' => 'role_id :value does not exist', + ] +]; diff --git a/application/messages/post.php b/application/messages/post.php index 29c8f3c164..d9b9b993c7 100644 --- a/application/messages/post.php +++ b/application/messages/post.php @@ -19,7 +19,8 @@ ], 'stageDoesNotExist' => 'Stage ":param1" does not exist', 'stageRequired' => 'Stage ":param1" is required before publishing', - + 'postNeedsApprovalBeforePublishing' => "Post needs approval by an administrator before it can be published", + 'postCanOnlyBeUnpublishedByAdmin' => "Post can only be unpublished by an administrator", 'values' => [ 'date' => 'The field :param1 must be a date, Given: :param2', 'decimal' => 'The field :param1 must be a decimal with 2 places, Given: :param2', diff --git a/application/tests/datasets/ushahidi/Base.yml b/application/tests/datasets/ushahidi/Base.yml index 30d5ee6a8a..cf7d7093c2 100644 --- a/application/tests/datasets/ushahidi/Base.yml +++ b/application/tests/datasets/ushahidi/Base.yml @@ -64,11 +64,20 @@ forms: name: "Test Form" type: "report" description: "Testing form" + require_approval: 1 + everyone_can_create: 1 - id: 2 name: "Missing people" type: "report" description: "Missing persons" + require_approval: 0 + everyone_can_create: 0 +form_roles: + - + id: 1 + form_id: 2 + role_id: 1 form_stages: - id: 1 diff --git a/application/tests/features/api.acl.feature b/application/tests/features/api.acl.feature index 4d1842a4de..89bbb1f913 100644 --- a/application/tests/features/api.acl.feature +++ b/application/tests/features/api.acl.feature @@ -154,7 +154,7 @@ Feature: API Access Control Layer When I request "/posts" Then the guzzle status code should be 200 And the response has an "id" property - + Scenario: Anonymous user can not access updates with private parent post Given that I want to get all "Updates" And that the request "Authorization" header is "Bearer testanon" @@ -435,7 +435,7 @@ Feature: API Access Control Layer Then the guzzle status code should be 200 And the response is JSON And the "count" property equals "16" - + @rolesEnabled Scenario: User with Manage Posts permission can view private posts Given that I want to find a "Post" @@ -462,7 +462,7 @@ Feature: API Access Control Layer When I request "/savedsearches" Then the response is JSON Then the guzzle status code should be 200 - + @rolesEnabled Scenario: User with with Manage Settings permission can update a config Given that I want to update a "Config" @@ -489,7 +489,7 @@ Feature: API Access Control Layer And the type of the "count" property is "numeric" And the "count" property equals "6" Then the guzzle status code should be 200 - + @rolesEnabled Scenario: User with Manage Settings permissions can see all Tags Given that I want to get all "Tags" @@ -520,7 +520,7 @@ Feature: API Access Control Layer And the type of the "id" property is "numeric" And the "disabled" property is false Then the guzzle status code should be 200 - + @rolesEnabled Scenario: User with Manage Users permission can create a user Given that I want to make a new "user" @@ -543,7 +543,7 @@ Feature: API Access Control Layer And the "role" property equals "user" And the response does not have a "password" property Then the guzzle status code should be 200 - + @rolesEnabled @dataImportEnabled Scenario: Uploading a CSV file with the Importer role Given that I want to make a new "CSV" diff --git a/application/tests/features/api.forms.feature b/application/tests/features/api.forms.feature index 16fb95e43d..dee814ba00 100644 --- a/application/tests/features/api.forms.feature +++ b/application/tests/features/api.forms.feature @@ -17,6 +17,12 @@ Feature: Testing the Forms API And the response has a "id" property And the type of the "id" property is "numeric" And the "disabled" property is false + And the "require_approval" property is true + And the "everyone_can_create" property is true + And the response has a "everyone_can_create" property + And the "everyone_can_create" property is true + And the response has a "can_create" property + And the "can_create" property is empty Then the guzzle status code should be 200 Scenario: Updating a Form @@ -27,7 +33,9 @@ Feature: Testing the Forms API "name":"Updated Test Form", "type":"report", "description":"This is a test form updated by BDD testing", - "disabled":true + "disabled":true, + "require_approval":false, + "everyone_can_create":false } """ And that its "id" is "1" @@ -39,6 +47,8 @@ Feature: Testing the Forms API And the response has a "name" property And the "name" property equals "Updated Test Form" And the "disabled" property is true + And the "require_approval" property is false + And the "everyone_can_create" property is false Then the guzzle status code should be 200 Scenario: Update a non-existent Form @@ -83,7 +93,130 @@ Feature: Testing the Forms API And the response has a "errors" property Then the guzzle status code should be 404 - Scenario: Deleting a Form + Scenario: POST method disabled for Form Roles + Given that I want to make a new "FormRole" + And that the request "data" is: + """ + { + "roles": [1] + } + """ + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 405 + + Scenario: DELETE method disabled for Form Roles + Given that I want to delete a "FormRole" + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 405 + + Scenario: Add 1 role to Form + Given that I want to update a "FormRole" + And that the request "data" is: + """ + { + "roles": [1] + } + """ + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "count" property + And the "count" property equals "1" + Then the guzzle status code should be 200 + + Scenario: Add 2 roles to Form + Given that I want to update a "FormRole" + And that the request "data" is: + """ + { + "roles": [1,2] + } + """ + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "count" property + And the "count" property equals "2" + Then the guzzle status code should be 200 + + Scenario: Finding a Form after roles have been set. + Given that I want to find a "Form" + And that its "id" is "1" + When I request "/forms" + Then the response is JSON + And the response has a "id" property + And the type of the "id" property is "numeric" + And the response has a "can_create" property + And the response has a "can_create.0" property + And the "can_create.0" property equals "user" + And the response has a "can_create.1" property + And the "can_create.1" property equals "admin" + Then the guzzle status code should be 200 + + Scenario: Remove roles from Form + Given that I want to update a "FormRole" + And that the request "data" is: + """ + { + "roles": [] + } + """ + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "count" property + And the "count" property equals "0" + Then the guzzle status code should be 200 + + Scenario: Finding all Form Roles + Given that I want to find a "FormRole" + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "count" property + And the type of the "count" property is "2" + Then the guzzle status code should be 200 + + Scenario: Fail to add 1 invalid Role to Form + Given that I want to update a "FormRole" + And that the request "data" is: + """ + { + "roles": [120] + } + """ + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 422 + + Scenario: Fail to add roles with 1 invalid Role id to Form + Given that I want to update a "FormRole" + And that the request "data" is: + """ + { + "roles": [1,2,120] + } + """ + When I request "/forms/1/roles" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 422 + + Scenario: Add roles to non-existent Form + Given that I want to update a "FormRole" + And that the request "data" is: + """ + { + "roles": [1] + } + """ + When I request "/forms/26/roles" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 404 + + Scenario: Delete a Form Given that I want to delete a "Form" And that its "id" is "1" When I request "/forms" diff --git a/application/tests/features/api.posts.feature b/application/tests/features/api.posts.feature index 56a91a8892..defef66f6a 100644 --- a/application/tests/features/api.posts.feature +++ b/application/tests/features/api.posts.feature @@ -92,8 +92,113 @@ Feature: Testing the Posts API And the "title" property equals "Test post" And the response has a "tags.0.id" property And the "values.missing_date.0" property equals "2016-05-31 00:00:00" + And the response has a "status" property + And the "status" property equals "draft" Then the guzzle status code should be 200 + @create + Scenario: Creating a Post anonymously with no form + Given that I want to make a new "Post" + And that the request "data" is: + """ + { + "title":"Anonymous Post", + "type":"report", + "status":"draft", + "locale":"en_US", + "values": + { + + }, + "tags":["explosion"], + "completed_stages":[] + } + """ + When I request "/posts" + Then the response is JSON + And the response has a "id" property + And the type of the "id" property is "numeric" + And the response has a "title" property + And the "title" property equals "Anonymous Post" + Then the guzzle status code should be 200 + + @create + Scenario: Creating a Post with a restricted Form with an Admin User + Given that I want to make a new "Post" + And that the request "Authorization" header is "Bearer testadminuser" + And that the request "data" is: + """ + { + "form":2, + "title":"Test post", + "type":"report", + "status":"draft", + "locale":"en_US", + "values": + { + + }, + "tags":["explosion"], + "completed_stages":[] + } + """ + When I request "/posts" + Then the response is JSON + And the response has a "id" property + And the type of the "id" property is "numeric" + Then the guzzle status code should be 200 + + @create + Scenario: Creating a Post with a form that does not require approval + Given that I want to make a new "Post" + And that the request "Authorization" header is "Bearer testbasicuser" + And that the request "data" is: + """ + { + "form":2, + "title":"Test post", + "type":"report", + "locale":"en_US", + "values": + { + + }, + "tags":["explosion"], + "completed_stages":[] + } + """ + When I request "/posts" + Then the response is JSON + And the response has a "id" property + And the type of the "id" property is "numeric" + And the response has a "status" property + And the "status" property equals "published" + Then the guzzle status code should be 200 + + @create + Scenario: Creating a Post with a form that does not require approval but try to set status should fail + Given that I want to make a new "Post" + And that the request "Authorization" header is "Bearer testbasicuser" + And that the request "data" is: + """ + { + "form":2, + "title":"Test post", + "type":"report", + "status":"draft", + "locale":"en_US", + "values": + { + + }, + "tags":["explosion"], + "completed_stages":[] + } + """ + When I request "/posts" + Then the response is JSON + Then the guzzle status code should be 422 + @create Scenario: Creating an Post with invalid data returns an error Given that I want to make a new "Post" @@ -199,6 +304,31 @@ Feature: Testing the Posts API And the response has a "errors" property Then the guzzle status code should be 422 + @create + Scenario: Creating a Post with a restricted Form returns an error + Given that I want to make a new "Post" + And that the request "Authorization" header is "Bearer testimporter" + And that the request "data" is: + """ + { + "form":2, + "title":"Test post", + "type":"report", + "status":"draft", + "locale":"en_US", + "values": + { + + }, + "tags":["explosion"], + "completed_stages":[] + } + """ + When I request "/posts" + Then the response is JSON + And the response has a "errors" property + Then the guzzle status code should be 403 + @create Scenario: Creating a Post with existing user by ID (authorized as admin user) Given that I want to make a new "Post" diff --git a/httpdocs/index.php b/httpdocs/index.php index f394d10c44..d9c54c2e82 100644 --- a/httpdocs/index.php +++ b/httpdocs/index.php @@ -20,12 +20,21 @@ class_exists('Minion_Task') OR die('Please enable the Minion module for CLI supp Minion_Task::factory(Minion_CLI::options())->execute(); } else -{ +{ + /** + * Some hosters set $_SERVER['ORIG_PATH_INFO'] instead of the more standard + * $_SERVER['PATH_INFO'], we check for that here. Otherwise, let Kohana try to + * automatically detect the URI. + */ + $server_path = TRUE; + if (array_key_exists('ORIG_PATH_INFO', $_SERVER)) { + $server_path = $_SERVER['ORIG_PATH_INFO']; + } /** - * Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO']. - * If no source is specified, the URI will be automatically detected. + * Execute the main request. Use the URI passed in the first parameter, if not specified + * (defaults to TRUE), we will try to automatically detect the URI. */ - echo Request::factory(TRUE, array(), FALSE) + echo Request::factory($server_path, array(), FALSE) ->execute() ->send_headers(TRUE) ->body(); diff --git a/migrations/20160720230341_add_require_approval_to_forms.php b/migrations/20160720230341_add_require_approval_to_forms.php new file mode 100644 index 0000000000..9d662e9991 --- /dev/null +++ b/migrations/20160720230341_add_require_approval_to_forms.php @@ -0,0 +1,31 @@ +table('forms') + ->addColumn('require_approval', 'boolean', [ + 'after' => 'disabled', + 'null' => false, + 'default' => true + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('forms') + ->removeColumn('require_approval') + ->update(); + } +} diff --git a/migrations/20160722231016_create_form_roles_table.php b/migrations/20160722231016_create_form_roles_table.php new file mode 100644 index 0000000000..73be3be549 --- /dev/null +++ b/migrations/20160722231016_create_form_roles_table.php @@ -0,0 +1,17 @@ +table('form_roles') + ->addColumn('form_id', 'integer', ['null' => false]) + ->addColumn('role_id', 'integer', ['null' => false]) + ->addForeignKey('form_id', 'forms', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']) + ->addForeignKey('role_id', 'roles', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']) + ->create() + ; + } +} diff --git a/migrations/20160726225434_add_form_everyone_can_create.php b/migrations/20160726225434_add_form_everyone_can_create.php new file mode 100644 index 0000000000..c2be7e9aa1 --- /dev/null +++ b/migrations/20160726225434_add_form_everyone_can_create.php @@ -0,0 +1,30 @@ +table('forms') + ->addColumn('everyone_can_create', 'boolean', [ + 'after' => 'require_approval', + 'null' => false, + 'default' => true + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('forms') + ->removeColumn('everyone_can_create') + ->update(); + } +} diff --git a/migrations/20160803210356_add_type_to_stage.php b/migrations/20160803210356_add_type_to_stage.php new file mode 100644 index 0000000000..366b0fcda2 --- /dev/null +++ b/migrations/20160803210356_add_type_to_stage.php @@ -0,0 +1,41 @@ +table('form_stages') + ->addColumn('type', 'string', [ + 'default' => 'task', + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('form_stages') + ->removeColumn('type') + ->update(); + } +} diff --git a/migrations/20160803225359_add_description_to_stage.php b/migrations/20160803225359_add_description_to_stage.php new file mode 100644 index 0000000000..cb7e2290df --- /dev/null +++ b/migrations/20160803225359_add_description_to_stage.php @@ -0,0 +1,41 @@ +table('form_stages') + ->addColumn('description', 'string', [ + 'null' => true, + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('form_stages') + ->removeColumn('description') + ->update(); + } +} diff --git a/migrations/20160809023850_return_publish_to_posts_to_review.php b/migrations/20160809023850_return_publish_to_posts_to_review.php new file mode 100644 index 0000000000..321a2e44ab --- /dev/null +++ b/migrations/20160809023850_return_publish_to_posts_to_review.php @@ -0,0 +1,31 @@ +execute("UPDATE posts SET published_to = '[]' + WHERE (published_to IS NULL OR published_to = '')"); + // Convert posts with `published_to` including `['user']` to published posts + $this->execute("UPDATE posts SET status = 'published', published_to = '[]' + WHERE status = 'published' AND published_to LIKE '%user%'"); + // Convert posts with `published_to` doesn't include `['user']` to draft posts + $this->execute("UPDATE posts SET status = 'draft', published_to = '[]' + WHERE status = 'published' AND published_to <> '[]' AND published_to NOT LIKE '%user%'"); + } + + /** + * Migrate Down. + */ + public function down() + { + // No way to revert this change + } +} diff --git a/spec/Core/Usecase/UpdateUsecaseSpec.php b/spec/Core/Usecase/UpdateUsecaseSpec.php index 125d0e5643..120c0e3439 100644 --- a/spec/Core/Usecase/UpdateUsecaseSpec.php +++ b/spec/Core/Usecase/UpdateUsecaseSpec.php @@ -80,13 +80,14 @@ function it_fails_when_validation_fails($auth, $repo, $valid, Entity $entity) $this->tryGetEntity($repo, $entity, $id); $entity_array = ['entity' => 'changed']; $entity->getChanged()->shouldBeCalled()->willReturn($entity_array); + $entity->asArray()->shouldBeCalled()->willReturn($entity_array); // ... if authorization passes $action = 'update'; $auth->isAllowed($entity, $action)->willReturn(true); // ... but validation fails - $valid->check($entity_array)->willReturn(false); + $valid->check($entity_array, $entity_array)->willReturn(false); // ... the exception requests the errors for the message $entity->getResource()->willReturn('widgets'); @@ -102,13 +103,14 @@ function it_updates_the_record($auth, $valid, $repo, $format, Entity $entity, En $this->tryGetEntity($repo, $entity, $id); $entity_array = ['entity' => 'changed']; $entity->getChanged()->shouldBeCalled()->willReturn($entity_array); + $entity->asArray()->shouldBeCalled()->willReturn($entity_array); // ... if authorization passes $action = 'update'; $auth->isAllowed($entity, $action)->willReturn(true); // ... and validation passes - $valid->check($entity_array)->willReturn(true); + $valid->check($entity_array, $entity_array)->willReturn(true); // ... store the changes $repo->update($entity)->shouldBeCalled(); diff --git a/src/Core/Entity/Form.php b/src/Core/Entity/Form.php index f7eec8d5c5..99dd8b2590 100644 --- a/src/Core/Entity/Form.php +++ b/src/Core/Entity/Form.php @@ -24,6 +24,9 @@ class Form extends StaticEntity protected $disabled; protected $created; protected $updated; + protected $require_approval; + protected $everyone_can_create; + protected $can_create; // DataTransformer protected function getDefinition() @@ -43,6 +46,9 @@ protected function getDefinition() 'disabled' => 'bool', 'created' => 'int', 'updated' => 'int', + 'require_approval' => 'bool', + 'everyone_can_create' => 'bool', + 'can_create' => 'array', ]; } diff --git a/src/Core/Entity/FormRole.php b/src/Core/Entity/FormRole.php new file mode 100644 index 0000000000..f0842fdfba --- /dev/null +++ b/src/Core/Entity/FormRole.php @@ -0,0 +1,37 @@ + + * @package Ushahidi\Platform + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +namespace Ushahidi\Core\Entity; + +use Ushahidi\Core\StaticEntity; + +class FormRole extends StaticEntity +{ + protected $id; + protected $form_id; + protected $role_id; + + // DataTransformer + protected function getDefinition() + { + return [ + 'id' => 'int', + 'form_id' => 'int', + 'role_id' => 'int' + ]; + } + + // Entity + public function getResource() + { + return 'form_roles'; + } +} diff --git a/src/Core/Entity/FormRoleRepository.php b/src/Core/Entity/FormRoleRepository.php new file mode 100644 index 0000000000..936c4918cf --- /dev/null +++ b/src/Core/Entity/FormRoleRepository.php @@ -0,0 +1,40 @@ + + * @package Ushahidi\Platform + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +namespace Ushahidi\Core\Entity; + +use Ushahidi\Core\Entity\Repository\EntityGet; +use Ushahidi\Core\Entity\Repository\EntityExists; + +interface FormRoleRepository extends + EntityGet, + EntityExists +{ + + /** + * @param int $form_id + * @return [Ushahidi\Core\Entity\FormRole, ...] + */ + public function getByForm($form_id); + + /** + * @param int $role_id + * @param int $form_id + * @return [Ushahidi\Core\Entity\FormRole, ...] + */ + public function existsInFormRole($role_id, $form_id); + + /** + * @param [Ushahidi\Core\Entity\FormRole, ...] $entities + * @return [Ushahidi\Core\Entity\FormRole, ...] + */ + public function updateCollection(Array $entities); +} diff --git a/src/Core/Entity/FormStage.php b/src/Core/Entity/FormStage.php index c0e92cf364..ba74a88c3a 100644 --- a/src/Core/Entity/FormStage.php +++ b/src/Core/Entity/FormStage.php @@ -20,13 +20,17 @@ class FormStage extends StaticEntity protected $label; protected $priority; protected $icon; + protected $type; protected $required; + protected $description; // DataTransformer protected function getDefinition() { return [ 'id' => 'int', + 'description' => 'string', + 'type' => 'string', 'form_id' => 'int', 'label' => 'string', 'priority' => 'int', diff --git a/src/Core/Entity/Post.php b/src/Core/Entity/Post.php index fe35d779ad..61b518e9d6 100644 --- a/src/Core/Entity/Post.php +++ b/src/Core/Entity/Post.php @@ -30,7 +30,7 @@ class Post extends StaticEntity protected $content; protected $author_email; protected $author_realname; - protected $status = 'draft'; + protected $status; protected $created; protected $updated; protected $locale; diff --git a/src/Core/Tool/Authorizer/FormRoleAuthorizer.php b/src/Core/Tool/Authorizer/FormRoleAuthorizer.php new file mode 100644 index 0000000000..595591648a --- /dev/null +++ b/src/Core/Tool/Authorizer/FormRoleAuthorizer.php @@ -0,0 +1,67 @@ + + * @package Ushahidi\Application + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +namespace Ushahidi\Core\Tool\Authorizer; + +use Ushahidi\Core\Entity; +use Ushahidi\Core\Entity\FormRepository; +use Ushahidi\Core\Tool\Authorizer; +use Ushahidi\Core\Traits\UserContext; + +// The `FormStageAuthorizer` class is responsible for access checks on `Forms` +class FormRoleAuthorizer implements Authorizer +{ + // The access checks are run under the context of a specific user + use UserContext; + + // It requires a `FormRepository` to load the owning form. + protected $form_repo; + + // It requires a `FormAuthorizer` to check privileges against the owning form. + protected $form_auth; + + /** + * @param FormRepository $form_repo + */ + public function __construct(FormRepository $form_repo, FormAuthorizer $form_auth) + { + $this->form_repo = $form_repo; + $this->form_auth = $form_auth; + } + + /* Authorizer */ + public function isAllowed(Entity $entity, $privilege) + { + $form = $this->getForm($entity); + + // All access is based on the form itself, not the role. + return $this->form_auth->isAllowed($form, $privilege); + } + + /* Authorizer */ + public function getAllowedPrivs(Entity $entity) + { + $form = $this->getForm($entity); + + // All access is based on the form itself, not the role. + return $this->form_auth->getAllowedPrivs($form); + } + + /** + * Get the form associated with this role. + * @param Entity $entity + * @return Form + */ + protected function getForm(Entity $entity) + { + return $this->form_repo->get($entity->form_id); + } +} diff --git a/src/Core/Tool/Authorizer/PostAuthorizer.php b/src/Core/Tool/Authorizer/PostAuthorizer.php index cfabbde9e6..8435512ea1 100644 --- a/src/Core/Tool/Authorizer/PostAuthorizer.php +++ b/src/Core/Tool/Authorizer/PostAuthorizer.php @@ -13,6 +13,8 @@ use Ushahidi\Core\Entity; use Ushahidi\Core\Entity\User; +use Ushahidi\Core\Entity\Form; +use Ushahidi\Core\Entity\FormRepository; use Ushahidi\Core\Entity\UserRepository; use Ushahidi\Core\Entity\PostRepository; use Ushahidi\Core\Tool\Authorizer; @@ -30,146 +32,173 @@ // The `PostAuthorizer` class is responsible for access checks on `Post` Entities class PostAuthorizer implements Authorizer, Permissionable { - // The access checks are run under the context of a specific user - use UserContext; - - // It uses methods from several traits to check access: - // - `OwnerAccess` to check if a user owns the post, the - // - `ParentAccess` to check if the user can access a parent post, - // - `AdminAccess` to check if the user has admin access - use AdminAccess, OwnerAccess, ParentAccess; - - // It uses `PrivAccess` to provide the `getAllowedPrivs` method. - use PrivAccess; - - // It uses `PrivateDeployment` to check whether a deployment is private - use PrivateDeployment; - - // Check that the user has the necessary permissions - // if roles are available for this deployment. - use PermissionAccess; - - // Provides `getPermission` - use ManagePosts; - - /** - * Get a list of all possible privilges. - * By default, returns standard HTTP REST methods. - * @return Array - */ - protected function getAllPrivs() - { - return ['read', 'create', 'update', 'delete', 'search', 'change_status']; - } - - // It requires a `PostRepository` to load parent posts too. - protected $post_repo; - - /** - * @param UserRepository $user_repo - * @param PostRepository $post_repo - */ - public function __construct(PostRepository $post_repo) - { - $this->post_repo = $post_repo; - } - - /* Authorizer */ - public function isAllowed(Entity $entity, $privilege) - { - // These checks are run within the user context. - $user = $this->getUser(); - - // Only logged in users have access if the deployment is private - if (!$this->hasAccess()) { - return false; - } - - // First check whether there is a role with the right permissions - if ($this->hasPermission($user)) { - return true; - } - - // Then we check if a user has the 'admin' role. If they do they're - // allowed access to everything (all entities and all privileges) - if ($this->isUserAdmin($user)) { - return true; - } - - // We check if the user has access to a parent post. This doesn't - // grant them access, but is used to deny access even if the child post - // is public. - if (! $this->isAllowedParent($entity, $privilege, $user)) { - return false; - } - - // Non-admin users are not allowed to create posts for other users. - // Post must be created for owner, or if the user is anonymous post must have no owner. - if ($privilege === 'create' - && !$this->isUserOwner($entity, $user) - && !$this->isUserAndOwnerAnonymous($entity, $user) - ) { - return false; - } - - // Non-admin users are not allowed to change post status - if (in_array($privilege, ['create', 'update']) && $entity->hasChanged('status')) { - return false; - } - - // All users are allowed to create and search posts. - if (in_array($privilege, ['create', 'search'])) { - return true; - } - - // If a post is published, then anyone with the appropriate role can read it - if ($privilege === 'read' && $this->isPostPublishedToUser($entity, $user)) { - return true; - } - - // If entity isn't loaded (ie. pre-flight check) then *anyone* can view it. - if ($privilege === 'read' && ! $entity->getId()) { - return true; - } - - // We check if the user is the owner of this post. If so, they are allowed - // to do almost anything, **except** change ownership and status of the post, which - // only admins can do. - if ($this->isUserOwner($entity, $user) && !$entity->hasChanged('user_id') - && $privilege !== 'change_status') { - return true; - } - - // If no other access checks succeed, we default to denying access - return false; - } - - protected function isPostPublishedToUser(Entity $entity, $user) - { - if ($entity->status === 'published' && $this->isUserOfRole($entity, $user)) { - return true; - } - return false; - } - - protected function isUserOfRole(Entity $entity, $user) - { - if ($entity->published_to) { - return in_array($user->role, $entity->published_to); - } - - // If no visibility info, assume public - return true; - } - - /* ParentAccess */ - protected function getParent(Entity $entity) - { - // If the post has a parent_id, we attempt to load it from the `PostRepository` - if ($entity->parent_id) { - return $this->post_repo->get($entity->parent_id); - } - - return false; - } + // The access checks are run under the context of a specific user + use UserContext; + + // It uses methods from several traits to check access: + // - `OwnerAccess` to check if a user owns the post, the + // - `ParentAccess` to check if the user can access a parent post, + // - `AdminAccess` to check if the user has admin access + use AdminAccess, OwnerAccess, ParentAccess; + + // It uses `PrivAccess` to provide the `getAllowedPrivs` method. + use PrivAccess; + + // It uses `PrivateDeployment` to check whether a deployment is private + use PrivateDeployment; + + // Check that the user has the necessary permissions + // if roles are available for this deployment. + use PermissionAccess; + + // Provides `getPermission` + use ManagePosts; + + /** + * Get a list of all possible privilges. + * By default, returns standard HTTP REST methods. + * @return Array + */ + protected function getAllPrivs() + { + return ['read', 'create', 'update', 'delete', 'search', 'change_status']; + } + + // It requires a `PostRepository` to load parent posts too. + protected $post_repo; + + // It requires a `FormRepository` to determine create access. + protected $form_repo; + + /** + * @param UserRepository $user_repo + * @param PostRepository $post_repo + */ + public function __construct(PostRepository $post_repo, FormRepository $form_repo) + { + $this->post_repo = $post_repo; + $this->form_repo = $form_repo; + } + + /* Authorizer */ + public function isAllowed(Entity $entity, $privilege) + { + // These checks are run within the user context. + $user = $this->getUser(); + + // Only logged in users have access if the deployment is private + if (!$this->hasAccess()) { + return false; + } + + // First check whether there is a role with the right permissions + if ($this->hasPermission($user)) { + return true; + } + + // Then we check if a user has the 'admin' role. If they do they're + // allowed access to everything (all entities and all privileges) + if ($this->isUserAdmin($user)) { + return true; + } + + // We check if the user has access to a parent post. This doesn't + // grant them access, but is used to deny access even if the child post + // is public. + if (! $this->isAllowedParent($entity, $privilege, $user)) { + return false; + } + + // Non-admin users are not allowed to create posts for other users. + // Post must be created for owner, or if the user is anonymous post must have no owner. + if ($privilege === 'create' + && !$this->isUserOwner($entity, $user) + && !$this->isUserAndOwnerAnonymous($entity, $user) + ) { + return false; + } + + // Non-admin users are not allowed to create posts for forms that have restricted access. + if (in_array($privilege, ['create', 'update']) + && $this->isFormRestricted($entity, $user) + ) { + return false; + } + + // All users are allowed to create and search posts. + if (in_array($privilege, ['create', 'search'])) { + return true; + } + + // If a post is published, then anyone with the appropriate role can read it + if ($privilege === 'read' && $this->isPostPublishedToUser($entity, $user)) { + return true; + } + + // If entity isn't loaded (ie. pre-flight check) then *anyone* can view it. + if ($privilege === 'read' && ! $entity->getId()) { + return true; + } + + // We check if the user is the owner of this post. If so, they are allowed + // to do almost anything, **except** change ownership and status of the post, which + // only admins can do. + if ($this->isUserOwner($entity, $user) && !$entity->hasChanged('user_id') + && $privilege !== 'change_status') { + return true; + } + + // If no other access checks succeed, we default to denying access + return false; + } + + protected function isPostPublishedToUser(Entity $entity, $user) + { + if ($entity->status === 'published' && $this->isUserOfRole($entity, $user)) { + return true; + } + return false; + } + + protected function isUserOfRole(Entity $entity, $user) + { + if ($entity->published_to) { + return in_array($user->role, $entity->published_to); + } + + // If no visibility info, assume public + return true; + } + + /* ParentAccess */ + protected function getParent(Entity $entity) + { + // If the post has a parent_id, we attempt to load it from the `PostRepository` + if ($entity->parent_id) { + return $this->post_repo->get($entity->parent_id); + } + + return false; + } + + /* FormRole */ + protected function isFormRestricted(Entity $entity, $user) + { + // If the $entity->form_id exists and the $form->everyone_can_create is False + // we check to see if the Form & Role Join exists in the `FormRoleRepository` + + if ($entity->form_id) { + $roles = $this->form_repo->getRolesThatCanCreatePosts($entity->form_id); + + if ($roles['everyone_can_create'] > 0) { + return false; + } + + if (is_array($roles['roles']) && in_array($user->role, $roles['roles'])) { + return false; + } + } + + return true; + } } diff --git a/src/Core/Tool/ValidationEngine.php b/src/Core/Tool/ValidationEngine.php index d4e0d08bff..01675ab762 100644 --- a/src/Core/Tool/ValidationEngine.php +++ b/src/Core/Tool/ValidationEngine.php @@ -28,6 +28,20 @@ public function setData(Array $data); */ public function getData($key = null); + /** + * Set or reset the full data array to be referenced for validation + * + * @param Array $data array of data in $key => $value format + */ + public function setFullData(Array $data); + + /** + * Get full data by its array key + * @param string $key + * @return mixed + */ + public function getFullData($key = null); + /** * Set rules that the validator will apply against the data * diff --git a/src/Core/Tool/Validator.php b/src/Core/Tool/Validator.php index 1488de54cf..681476b659 100644 --- a/src/Core/Tool/Validator.php +++ b/src/Core/Tool/Validator.php @@ -11,7 +11,7 @@ namespace Ushahidi\Core\Tool; -use Ushahidi\Core\Data; +use Ushahidi\Core\Entity; use Ushahidi\Core\Traits\GetSet; use Ushahidi\Core\Tool\Validation; use Ushahidi\Core\Tool\ValidationEngineTrait; @@ -36,11 +36,18 @@ abstract protected function getRules(); /** * Check the data against the rules returned by getRules() * - * @param Array $input an array of data to check in $key => $value format + * @param Array $data an array of changed values to check in $key => $value format + * @param Array $fullData an array of full entity data for reference during validation * @return bool */ - public function check(Array $data) + public function check(Array $data, Array $fullData = []) { + // If no full data is passed, fallback to changed values + if (!$fullData) { + $fullData = $data; + } + + $this->validation_engine->setFullData($fullData); $this->validation_engine->setData($data); $this->attachRules($this->getRules()); return $this->validation_engine->check(); diff --git a/src/Core/Usecase/Form/DeleteFormRole.php b/src/Core/Usecase/Form/DeleteFormRole.php new file mode 100644 index 0000000000..c653fd7e9b --- /dev/null +++ b/src/Core/Usecase/Form/DeleteFormRole.php @@ -0,0 +1,20 @@ + + * @package Ushahidi\Platform + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +namespace Ushahidi\Core\Usecase\Form; + +use Ushahidi\Core\Usecase\DeleteUsecase; + +class DeleteFormRole extends DeleteUsecase +{ + // - VerifyFormLoaded for checking that the form exists + use VerifyFormLoaded; +} diff --git a/src/Core/Usecase/Form/ReadFormRole.php b/src/Core/Usecase/Form/ReadFormRole.php new file mode 100644 index 0000000000..9c379d420f --- /dev/null +++ b/src/Core/Usecase/Form/ReadFormRole.php @@ -0,0 +1,20 @@ + + * @package Ushahidi\Platform + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +namespace Ushahidi\Core\Usecase\Form; + +use Ushahidi\Core\Usecase\ReadUsecase; + +class ReadFormRole extends ReadUsecase +{ + // - VerifyFormLoaded for checking that the form exists + use VerifyFormLoaded; +} diff --git a/src/Core/Usecase/Form/SearchFormRole.php b/src/Core/Usecase/Form/SearchFormRole.php new file mode 100644 index 0000000000..1c763a1716 --- /dev/null +++ b/src/Core/Usecase/Form/SearchFormRole.php @@ -0,0 +1,30 @@ + + * @package Ushahidi\Platform + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +namespace Ushahidi\Core\Usecase\Form; + +class SearchFormRole extends SearchFormAttribute +{ + /** + * Get filter parameters and default values that are used for paging. + * + * @return Array + */ + protected function getPagingFields() + { + return [ + 'orderby' => 'role_id', + 'order' => 'asc', + 'limit' => null, + 'offset' => 0 + ]; + } +} diff --git a/src/Core/Usecase/Form/UpdateFormRole.php b/src/Core/Usecase/Form/UpdateFormRole.php new file mode 100644 index 0000000000..fb35c6fd8f --- /dev/null +++ b/src/Core/Usecase/Form/UpdateFormRole.php @@ -0,0 +1,71 @@ + + * @package Ushahidi\Platform + * @copyright 2014 Ushahidi + * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License Version 3 (AGPL3) + */ + +namespace Ushahidi\Core\Usecase\Form; + +use Ushahidi\Core\Usecase\CreateUsecase; +use Ushahidi\Core\Traits\IdentifyRecords; +use Ushahidi\Core\Traits\VerifyEntityLoaded; +use Ushahidi\Core\Entity\FormRole; + +class UpdateFormRole extends CreateUsecase +{ + // - VerifyFormLoaded for checking that the form exists + use VerifyFormLoaded; + + // For form check: + // - IdentifyRecords + // - VerifyEntityLoaded + use IdentifyRecords, + VerifyEntityLoaded; + + /** + * Get an empty entity. + * + * @return Entity + */ + protected function getEntity() + { + return $this->repo->getEntity(); + } + + // Usecase + public function interact() + { + // First verify that the form even exists + $this->verifyFormExists(); + + // Fetch a default entity and ... + $entity = $this->getEntity(); + + // ... verify the current user has have permissions + $this->verifyCreateAuth($entity); + + + // Get each item in the collection + $entities = []; + $form_id = $this->getRequiredIdentifier('form_id'); + foreach ($this->getPayload('roles') as $role_id) { + // .. generate an entity for the item + $entity = $this->repo->getEntity(compact('role_id', 'form_id')); + // ... verify that the entity is in a valid state + $this->verifyValid($entity); + // ... and save it for later + $entities[] = $entity; + } + + // ... persist the new collection + $this->repo->updateCollection($entities); + + // ... and finally format it for output + return $this->formatter->__invoke($entities); + } +} diff --git a/src/Core/Usecase/Post/CreatePost.php b/src/Core/Usecase/Post/CreatePost.php index bc275ce38b..6712e9a4bf 100644 --- a/src/Core/Usecase/Post/CreatePost.php +++ b/src/Core/Usecase/Post/CreatePost.php @@ -33,13 +33,17 @@ protected function getEntity() $entity->setState(['user_id' => $this->auth->getUserId()]); } - return $entity; - } - - protected function verifyValid(Entity $entity) - { - if (!$this->validator->check($entity->getChanged())) { - $this->validatorError($entity); + // If status is not set.. + if (empty($entity->status)) { + // .. check if the post requires approval + // .. and set a default status + if ($this->repo->doesPostRequireApproval($entity->form_id)) { + $entity->setState(['status' => 'draft']); + } else { + $entity->setState(['status' => 'published']); + } } + + return $entity; } } diff --git a/src/Core/Usecase/Post/UpdatePost.php b/src/Core/Usecase/Post/UpdatePost.php index e3bf1f1091..63d2545572 100644 --- a/src/Core/Usecase/Post/UpdatePost.php +++ b/src/Core/Usecase/Post/UpdatePost.php @@ -34,20 +34,13 @@ protected function verifyValid(Entity $entity) { $changed = $entity->getChanged(); - if (isset($entity->id)) { - $changed['id'] = $entity->id; - } - - // Always pass form_id to validation - if (isset($entity->form_id)) { - $changed['form_id'] = $entity->form_id; - } // Always pass values to validation + if (isset($entity->values)) { $changed['values'] = $entity->values; } - if (!$this->validator->check($changed)) { + if (!$this->validator->check($changed, $entity->asArray())) { $this->validatorError($entity); } } diff --git a/src/Core/Usecase/UpdateUsecase.php b/src/Core/Usecase/UpdateUsecase.php index 22fe524f03..b1f6ce52c8 100644 --- a/src/Core/Usecase/UpdateUsecase.php +++ b/src/Core/Usecase/UpdateUsecase.php @@ -97,13 +97,7 @@ public function interact() // ValidatorTrait protected function verifyValid(Entity $entity) { - $changed = $entity->getChanged(); - - if (isset($entity->id)) { - $changed['id'] = $entity->id; - } - - if (!$this->validator->check($changed)) { + if (!$this->validator->check($entity->getChanged(), $entity->asArray())) { $this->validatorError($entity); } } diff --git a/src/Init.php b/src/Init.php index 9a730e3f6f..3dd82eafbe 100644 --- a/src/Init.php +++ b/src/Init.php @@ -124,6 +124,7 @@ function feature($name) 'dataproviders' => $di->lazyGet('authorizer.dataprovider'), 'forms' => $di->lazyGet('authorizer.form'), 'form_attributes' => $di->lazyGet('authorizer.form_attribute'), + 'form_roles' => $di->lazyGet('authorizer.form_role'), 'form_stages' => $di->lazyGet('authorizer.form_stage'), 'tags' => $di->lazyGet('authorizer.tag'), 'layers' => $di->lazyGet('authorizer.layer'), @@ -152,6 +153,7 @@ function feature($name) 'dataproviders' => $di->lazyGet('repository.dataprovider'), 'forms' => $di->lazyGet('repository.form'), 'form_attributes' => $di->lazyGet('repository.form_attribute'), + 'form_roles' => $di->lazyGet('repository.form_role'), 'form_stages' => $di->lazyGet('repository.form_stage'), 'layers' => $di->lazyGet('repository.layer'), 'media' => $di->lazyGet('repository.media'), @@ -163,7 +165,7 @@ function feature($name) 'savedsearches' => $di->lazyGet('repository.savedsearch'), 'users' => $di->lazyGet('repository.user'), 'notifications' => $di->lazyGet('repository.notification'), - 'contacts' => $di->lazyGet('repository.contact'), + 'contacts' => $di->lazyGet('repository.contact'), 'csv' => $di->lazyGet('repository.csv'), 'roles' => $di->lazyGet('repository.role'), 'permissions' => $di->lazyGet('repository.permission'), @@ -181,6 +183,7 @@ function feature($name) // is mapped by actions that return collections. $di->params['Ushahidi\Factory\FormatterFactory']['collections'] = [ 'search' => true, + 'update_collection' => true ]; // Data transfer objects are used to carry complex search filters between collaborators. @@ -231,6 +234,10 @@ function feature($name) 'delete' => $di->lazyNew('Ushahidi\Core\Usecase\Form\DeleteFormAttribute'), 'search' => $di->lazyNew('Ushahidi\Core\Usecase\Form\SearchFormAttribute'), ]; +$di->params['Ushahidi\Factory\UsecaseFactory']['map']['form_roles'] = [ + 'update_collection' => $di->lazyNew('Ushahidi\Core\Usecase\Form\UpdateFormRole'), + 'search' => $di->lazyNew('Ushahidi\Core\Usecase\Form\SearchFormRole'), +]; $di->params['Ushahidi\Factory\UsecaseFactory']['map']['form_stages'] = [ 'create' => $di->lazyNew('Ushahidi\Core\Usecase\Form\CreateFormStage'), 'read' => $di->lazyNew('Ushahidi\Core\Usecase\Form\ReadFormStage'), @@ -350,6 +357,11 @@ function feature($name) 'stage_repo' => $di->lazyGet('repository.form_stage'), 'stage_auth' => $di->lazyGet('authorizer.form_stage'), ]; +$di->set('authorizer.form_role', $di->lazyNew('Ushahidi\Core\Tool\Authorizer\FormRoleAuthorizer')); +$di->params['Ushahidi\Core\Tool\Authorizer\FormRoleAuthorizer'] = [ + 'form_repo' => $di->lazyGet('repository.form'), + 'form_auth' => $di->lazyGet('authorizer.form'), + ]; $di->set('authorizer.form_stage', $di->lazyNew('Ushahidi\Core\Tool\Authorizer\FormStageAuthorizer')); $di->params['Ushahidi\Core\Tool\Authorizer\FormStageAuthorizer'] = [ 'form_repo' => $di->lazyGet('repository.form'), @@ -371,6 +383,7 @@ function feature($name) $di->set('authorizer.post', $di->lazyNew('Ushahidi\Core\Tool\Authorizer\PostAuthorizer')); $di->params['Ushahidi\Core\Tool\Authorizer\PostAuthorizer'] = [ 'post_repo' => $di->lazyGet('repository.post'), + 'form_repo' => $di->lazyGet('repository.form'), ]; $di->set('authorizer.console', $di->lazyNew('Ushahidi\Console\Authorizer\ConsoleAuthorizer'));