From 38c1eef93ef049ae112786824ed0448e41f22bef Mon Sep 17 00:00:00 2001 From: will Date: Tue, 4 Apr 2017 02:40:10 -0400 Subject: [PATCH] Adding survey editor update (#1662) * Adding survey editor update * Field locking fully working, next fix broken tests * Fixing tests * Update Vagrantfile * Remove extra files * Updating counts * Adding entity fields * Removing unneeded include, simplifying function * Adding task restriction * Adding form stage and attribute restriction * Fixed stage tests * Updated attribute filitering and added tests * Fixed linting * Fixing base data set * Fixing tests * removing extraneous char --- application/classes/Ushahidi/Core.php | 11 + .../classes/Ushahidi/Repository/Form.php | 17 ++ .../Ushahidi/Repository/Form/Attribute.php | 98 +++++++- .../Ushahidi/Repository/Form/Stage.php | 107 +++++++- .../classes/Ushahidi/Repository/Post.php | 58 ++++- .../Ushahidi/Repository/Post/Description.php | 2 +- .../Ushahidi/Repository/Post/Title.php | 2 +- .../Ushahidi/Repository/Post/Value.php | 13 +- .../Ushahidi/Repository/Post/ValueProxy.php | 6 +- .../Ushahidi/Validator/Post/Create.php | 7 + application/tests/datasets/ushahidi/Base.yml | 218 +++++++++++++++- application/tests/features/api.acl.feature | 234 ++++++++++++++++-- application/tests/features/api.posts.feature | 12 +- .../features/forms/api.attributes.feature | 2 +- ...add_private_response_to_form_attribute.php | 29 +++ ...20170322160413_add_hide_author_to_form.php | 29 +++ ...dd_description_field_to_form_attribute.php | 28 +++ ..._add_show_when_published_to_form_stage.php | 29 +++ src/Core/Entity/Form.php | 2 + src/Core/Entity/FormAttribute.php | 2 + src/Core/Entity/FormStage.php | 2 + src/Core/Traits/PostValueRestrictions.php | 96 +++++++ .../Usecase/Post/ValuesForPostRepository.php | 7 +- 23 files changed, 954 insertions(+), 57 deletions(-) create mode 100644 migrations/20170317174120_add_private_response_to_form_attribute.php create mode 100644 migrations/20170322160413_add_hide_author_to_form.php create mode 100644 migrations/20170322162237_add_description_field_to_form_attribute.php create mode 100644 migrations/20170331210813_add_show_when_published_to_form_stage.php create mode 100644 src/Core/Traits/PostValueRestrictions.php diff --git a/application/classes/Ushahidi/Core.php b/application/classes/Ushahidi/Core.php index f4091ed9e4..4774fbf91c 100644 --- a/application/classes/Ushahidi/Core.php +++ b/application/classes/Ushahidi/Core.php @@ -427,6 +427,17 @@ public static function init() 'upload' => $di->lazyGet('tool.uploader'), ]; + // Form Stage repository parameters + $di->params['Ushahidi_Repository_Form_Stage'] = [ + 'form_repo' => $di->lazyGet('repository.form') + ]; + + // Form Attribute repository parameters + $di->params['Ushahidi_Repository_Form_Attribute'] = [ + 'form_stage_repo' => $di->lazyGet('repository.form_stage'), + 'form_repo' => $di->lazyGet('repository.form') + ]; + // Post repository parameters $di->params['Ushahidi_Repository_Post'] = [ 'form_attribute_repo' => $di->lazyGet('repository.form_attribute'), diff --git a/application/classes/Ushahidi/Repository/Form.php b/application/classes/Ushahidi/Repository/Form.php index 61f14a66d4..03e21993f3 100644 --- a/application/classes/Ushahidi/Repository/Form.php +++ b/application/classes/Ushahidi/Repository/Form.php @@ -81,6 +81,23 @@ public function getTotalCount(Array $where = []) return $this->selectCount($where); } + /** + * Get value of Form property hide_author + * if no form is found return false + * @param $form_id + * @return Boolean + */ + public function isAuthorHidden($form_id) + { + $query = DB::select('hide_author') + ->from('forms') + ->where('id', '=', $form_id); + + $results = $query->execute($this->db)->as_array(); + + return count($results) > 0 ? $results[0]['hide_author'] : false; + } + /** * Get `everyone_can_create` and list of roles that have access to post to the form * @param $form_id diff --git a/application/classes/Ushahidi/Repository/Form/Attribute.php b/application/classes/Ushahidi/Repository/Form/Attribute.php index 27328d2745..e8b2796df2 100644 --- a/application/classes/Ushahidi/Repository/Form/Attribute.php +++ b/application/classes/Ushahidi/Repository/Form/Attribute.php @@ -13,6 +13,10 @@ use Ushahidi\Core\SearchData; use Ushahidi\Core\Entity\FormAttribute; use Ushahidi\Core\Entity\FormAttributeRepository; +use Ushahidi\Core\Entity\FormStageRepository; +use Ushahidi\Core\Entity\FormRepository; +use Ushahidi\Core\Traits\PostValueRestrictions; +use Ushahidi\Core\Traits\UserContext; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Exception\UnsatisfiedDependencyException; @@ -20,15 +24,69 @@ class Ushahidi_Repository_Form_Attribute extends Ushahidi_Repository implements FormAttributeRepository { + use UserContext; + + use PostValueRestrictions; + + protected $form_stage_repo; + + protected $form_repo; + + protected $form_id; + // Use the JSON transcoder to encode properties use Ushahidi_JsonTranscodeRepository; + /** + * Construct + * @param Database $db + * @param FormStageRepository $form_stage_repo + * @param FormRepository $form_repo + */ + public function __construct( + Database $db, + FormStageRepository $form_stage_repo, + FormRepository $form_repo + ) + { + parent::__construct($db); + + $this->form_stage_repo = $form_stage_repo; + $this->form_repo = $form_repo; + + } + // Ushahidi_JsonTranscodeRepository protected function getJsonProperties() { return ['options', 'config']; } + protected function getFormId($form_stage_id) + { + $form_id = $this->form_stage_repo->getFormByStageId($form_stage_id); + if ($form_id) { + return $form_id; + } + return null; + } + + // Override selectQuery to fetch attribute 'key' too + protected function selectQuery(Array $where = [], $form_id = null, $form_stage_id = null) + { + $query = parent::selectQuery($where); + + if (!$form_id && $form_stage_id) { + $form_id = $this->getFormId(); + } + + if ($this->isRestricted($form_id)) { + $query->where('response_private', '=', '0'); + } + + return $query; + } + // CreateRepository public function create(Entity $entity) { @@ -43,6 +101,37 @@ public function create(Entity $entity) return $this->executeInsertAttribute($this->removeNullValues($record)); } + // Override SearchRepository + public function setSearchParams(SearchData $search) + { + $form_id = null; + if ($search->form_id) { + $form_id = $search->form_id; + } + + $this->search_query = $this->selectQuery([], $form_id); + + $sorting = $search->getSorting(); + + if (!empty($sorting['orderby'])) { + $this->search_query->order_by( + $this->getTable() . '.' . $sorting['orderby'], + Arr::get($sorting, 'order') + ); + } + + if (!empty($sorting['offset'])) { + $this->search_query->offset($sorting['offset']); + } + + if (!empty($sorting['limit'])) { + $this->search_query->limit($sorting['limit']); + } + + // apply the unique conditions of the search + $this->setSearchConditions($search); + } + // SearchRepository protected function setSearchConditions(SearchData $search) { @@ -84,7 +173,8 @@ public function getSearchFields() // FormAttributeRepository public function getByKey($key, $form_id = null, $include_no_form = false) { - $query = $this->selectQuery() + + $query = $this->selectQuery([], $form_id) ->select('form_attributes.*') ->join('form_stages', 'LEFT') ->on('form_stages.id', '=', 'form_attributes.form_stage_id') @@ -122,7 +212,7 @@ public function getByForm($form_id) { $query = $this->selectQuery([ 'form_stages.form_id' => $form_id, - ]) + ], $form_id) ->select('form_attributes.*') ->join('form_stages', 'INNER') ->on('form_stages.id', '=', 'form_attributes.form_stage_id'); @@ -135,10 +225,12 @@ public function getByForm($form_id) // FormAttributeRepository public function getRequired($stage_id) { + $form_id = $this->getFormId($stage_id); + $query = $this->selectQuery([ 'form_attributes.form_stage_id' => $stage_id, 'form_attributes.required' => true - ]) + ], $form_id) ->select('form_attributes.*'); $results = $query->execute($this->db); diff --git a/application/classes/Ushahidi/Repository/Form/Stage.php b/application/classes/Ushahidi/Repository/Form/Stage.php index 7bdb52760b..1699c143c0 100644 --- a/application/classes/Ushahidi/Repository/Form/Stage.php +++ b/application/classes/Ushahidi/Repository/Form/Stage.php @@ -13,16 +13,54 @@ use Ushahidi\Core\SearchData; use Ushahidi\Core\Entity\FormStage; use Ushahidi\Core\Entity\FormStageRepository; +use Ushahidi\Core\Entity\FormRepository; +use Ushahidi\Core\Traits\PostValueRestrictions; +use Ushahidi\Core\Traits\UserContext; class Ushahidi_Repository_Form_Stage extends Ushahidi_Repository implements FormStageRepository { + use UserContext; + + use PostValueRestrictions; + + protected $form_id; + protected $form_repo; + + /** + * Construct + * @param Database $db + * @param FormRepository $form_repo + */ + public function __construct( + Database $db, + FormRepository $form_repo + ) + { + parent::__construct($db); + + $this->form_repo = $form_repo; + + } + // Ushahidi_Repository protected function getTable() { return 'form_stages'; } + // Override selectQuery to fetch attribute 'key' too + protected function selectQuery(Array $where = [], $form_id = null) + { + $query = parent::selectQuery($where); + + if ($this->isRestricted($form_id)) { + $query->where('show_when_published', '=', '1'); + } + + return $query; + } + // CreateRepository // ReadRepository public function getEntity(Array $data = null) @@ -36,6 +74,37 @@ public function getSearchFields() return ['form_id', 'label']; } + // Override SearchRepository + public function setSearchParams(SearchData $search) + { + $form_id = null; + if ($search->form_id) { + $form_id = $search->form_id; + } + + $this->search_query = $this->selectQuery([], $form_id); + + $sorting = $search->getSorting(); + + if (!empty($sorting['orderby'])) { + $this->search_query->order_by( + $this->getTable() . '.' . $sorting['orderby'], + Arr::get($sorting, 'order') + ); + } + + if (!empty($sorting['offset'])) { + $this->search_query->offset($sorting['offset']); + } + + if (!empty($sorting['limit'])) { + $this->search_query->limit($sorting['limit']); + } + + // apply the unique conditions of the search + $this->setSearchConditions($search); + } + // Ushahidi_Repository protected function setSearchConditions(SearchData $search) { @@ -51,15 +120,49 @@ protected function setSearchConditions(SearchData $search) } } + public function getFormByStageId($id) + { + $query = DB::select('form_id') + ->from('form_stages') + ->where('id', '=', $id); + + $results = $query->execute($this->db); + + return count($results) > 0 ? $results[0]['form_id'] : false; + } + // FormStageRepository public function getByForm($form_id) { - $query = $this->selectQuery(compact($form_id)); + $query = $this->selectQuery(compact($form_id), $form_id); $results = $query->execute($this->db); return $this->getCollection($results->as_array()); } + /** + * Retrieve Hidden Stage IDs for a given form + * if no form is found return false + * @param $form_id + * @return Array + */ + public function getHiddenStageIds($form_id) + { + $stages = []; + + $query = DB::select('id') + ->from('form_stages') + ->where('show_when_published', '=', 0); + + $results = $query->execute($this->db)->as_array(); + + foreach($results as $stage) { + array_push($stages, $stage['id']); + } + + return $stages; + } + // FormStageRepository public function existsInForm($id, $form_id) { @@ -72,7 +175,7 @@ public function getRequired($form_id) $query = $this->selectQuery([ 'form_stages.form_id' => $form_id, 'form_stages.required' => true - ]) + ], $form_id) ->select('form_stages.*'); $results = $query->execute($this->db); diff --git a/application/classes/Ushahidi/Repository/Post.php b/application/classes/Ushahidi/Repository/Post.php index 4795240d00..6db977402e 100644 --- a/application/classes/Ushahidi/Repository/Post.php +++ b/application/classes/Ushahidi/Repository/Post.php @@ -28,6 +28,7 @@ use Ushahidi\Core\Traits\PermissionAccess; use Ushahidi\Core\Traits\AdminAccess; use Ushahidi\Core\Tool\Permissions\Permissionable; +use Ushahidi\Core\Traits\PostValueRestrictions; use Aura\DI\InstanceFactory; @@ -57,15 +58,22 @@ class Ushahidi_Repository_Post extends Ushahidi_Repository implements // Checks if user is Admin use AdminAccess; + // Check for value restrictions + // provides canUserReadPostsValues + use PostValueRestrictions; + protected $form_attribute_repo; protected $form_stage_repo; protected $form_repo; protected $post_value_factory; protected $bounding_box_factory; protected $tag_repo; + // By default remove all private responses + protected $restricted = true; protected $include_value_types = []; protected $include_attributes = []; + protected $exclude_stages = []; protected $listener; @@ -106,6 +114,18 @@ protected function getTable() // Ushahidi_Repository public function getEntity(Array $data = null) { + // Ensure we are dealing with a structured Post + if ($data['form_id']) + { + $user = $this->getUser(); + if ($this->canUserReadPostsValues(new Post($data), $user, $this->form_repo)) { + $this->restricted = false; + Kohana::$log->add(Log::ERROR, "here"); + } + // Get Hidden Stage Ids to be excluded from results + $this->exclude_stages = $this->form_stage_repo->getHiddenStageIds($data['form_id']); + } + if (!empty($data['id'])) { $data += [ @@ -115,6 +135,20 @@ public function getEntity(Array $data = null) 'completed_stages' => $this->getCompletedStagesForPost($data['id']), ]; } + // NOTE: This and the restriction above belong somewhere else, + // ideally in their own step + //Check if author information should be returned + if ($data['author_realname'] || $data['user_id'] || $data['author_email']) + { + + if (!$this->canUserSeeAuthor(new Post($data), $this->form_repo) && $this->restricted) + { + unset($data['author_realname']); + unset($data['author_email']); + unset($data['user_id']); + } + } + return new Post($data); } @@ -142,12 +176,11 @@ protected function selectQuery(Array $where = []) protected function getPostValues($id) { + // Get all the values for the post. These are the EAV values. $values = $this->post_value_factory ->proxy($this->include_value_types) - ->getAllForPost($id, $this->include_attributes); - - + ->getAllForPost($id, $this->include_attributes, $this->exclude_stages, $this->restricted); $output = []; foreach ($values as $value) { @@ -163,11 +196,18 @@ protected function getPostValues($id) protected function getCompletedStagesForPost($id) { - $result = DB::select('form_stage_id', 'completed') + $query = DB::select('form_stage_id', 'completed') ->from('form_stages_posts') ->where('post_id', '=', $id) - ->where('completed', '=', 1) - ->execute($this->db); + ->where('completed', '=', 1); + + if ($this->restricted) { + if ($this->exclude_stages) { + $query->where('form_stage_id', 'NOT IN', $this->exclude_stages); + } + } + + $result = $query->execute($this->db); return $result->as_array(NULL, 'form_stage_id'); } @@ -220,7 +260,7 @@ protected function setSearchConditions(SearchData $search) $query = $this->search_query; $table = $this->getTable(); - + // Filter by status $status = $search->getFilter('status', ['published']); // @@ -324,7 +364,7 @@ protected function setSearchConditions(SearchData $search) // Convert to UTC (needed in case date came with a tz) $date_after->setTimezone(new DateTimeZone('UTC')); $query->where("$table.post_date", '>=', $date_after->format('Y-m-d H:i:s')); - } + } if ($search->date_before) { @@ -528,7 +568,7 @@ protected function setSearchConditions(SearchData $search) ->where("$table.status", '=', 'published') ->or_where("$table.user_id", '=', $user->id) ->and_where_close(); - } + } } // SearchRepository diff --git a/application/classes/Ushahidi/Repository/Post/Description.php b/application/classes/Ushahidi/Repository/Post/Description.php index 8e077c74c3..36d7633216 100644 --- a/application/classes/Ushahidi/Repository/Post/Description.php +++ b/application/classes/Ushahidi/Repository/Post/Description.php @@ -14,7 +14,7 @@ class Ushahidi_Repository_Post_Description extends Ushahidi_Repository_Post_Text { - public function getAllForPost($post_id, Array $include_attributes = []) + public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false) { return []; } diff --git a/application/classes/Ushahidi/Repository/Post/Title.php b/application/classes/Ushahidi/Repository/Post/Title.php index 9e13a2fe34..e34c52a2af 100644 --- a/application/classes/Ushahidi/Repository/Post/Title.php +++ b/application/classes/Ushahidi/Repository/Post/Title.php @@ -14,7 +14,7 @@ class Ushahidi_Repository_Post_Title extends Ushahidi_Repository_Post_Varchar { - public function getAllForPost($post_id, Array $include_attributes = []) + public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false) { return []; } diff --git a/application/classes/Ushahidi/Repository/Post/Value.php b/application/classes/Ushahidi/Repository/Post/Value.php index aac659efcc..b0e31bfacf 100644 --- a/application/classes/Ushahidi/Repository/Post/Value.php +++ b/application/classes/Ushahidi/Repository/Post/Value.php @@ -40,7 +40,9 @@ protected function selectQuery(Array $where = []) // Select 'key' too $query->select( $this->getTable().'.*', - 'form_attributes.key' + 'form_attributes.key', + 'form_attributes.form_stage_id', + 'form_attributes.response_private' ) ->join('form_attributes')->on('form_attribute_id', '=', 'form_attributes.id'); @@ -55,7 +57,7 @@ public function get($id, $post_id = null, $form_attribute_id = null) } // ValuesForPostRepository - public function getAllForPost($post_id, Array $include_attributes = []) + public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false) { $query = $this->selectQuery(compact('post_id')); @@ -63,6 +65,13 @@ public function getAllForPost($post_id, Array $include_attributes = []) $query->where('form_attributes.key', 'IN', $include_attributes); } + if ($restricted) { + $query->where('form_attributes.response_private', '!=', '1'); + if ($exclude_stages) { + $query->where('form_attributes.form_stage_id', 'NOT IN', $exclude_stages); + } + } + $results = $query->execute($this->db); return $this->getCollection($results->as_array()); } diff --git a/application/classes/Ushahidi/Repository/Post/ValueProxy.php b/application/classes/Ushahidi/Repository/Post/ValueProxy.php index cafa242e95..b564a70ca6 100644 --- a/application/classes/Ushahidi/Repository/Post/ValueProxy.php +++ b/application/classes/Ushahidi/Repository/Post/ValueProxy.php @@ -23,12 +23,12 @@ public function __construct(Ushahidi_Repository_Post_ValueFactory $factory, Arra } // ValuesForPostRepository - public function getAllForPost($post_id, Array $include_attributes = []) + public function getAllForPost($post_id, Array $include_attributes = [], Array $exclude_stages = [], $restricted = false) { $results = []; - $this->factory->each(function ($repo) use ($post_id, $include_attributes, &$results) { - $results = array_merge($results, $repo->getAllForPost($post_id, $include_attributes)); + $this->factory->each(function ($repo) use ($post_id, $include_attributes, &$results, $exclude_stages, $restricted) { + $results = array_merge($results, $repo->getAllForPost($post_id, $include_attributes, $exclude_stages, $restricted)); }, $this->include_types); return $results; diff --git a/application/classes/Ushahidi/Validator/Post/Create.php b/application/classes/Ushahidi/Validator/Post/Create.php index 82367d3ac8..0eafdf8b77 100644 --- a/application/classes/Ushahidi/Validator/Post/Create.php +++ b/application/classes/Ushahidi/Validator/Post/Create.php @@ -185,6 +185,10 @@ public function checkApprovalRequired (Validation $validation, $status, $fullDat return; } + if ($status === 'draft' && !isset($fullData['id'])) { + return; + } + $user = $this->getUser(); // Do we have permission to publish this post? $userCanChangeStatus = ($this->isUserAdmin($user) or $this->hasPermission($user)); @@ -195,6 +199,9 @@ public function checkApprovalRequired (Validation $validation, $status, $fullDat $requireApproval = $this->repo->doesPostRequireApproval($fullData['form_id']); + Kohana::$log->add(Log::ERROR, print_r($requireApproval, true)); + Kohana::$log->add(Log::ERROR, print_r($status, true)); + // Are we trying to change publish a post that requires approval? if ($requireApproval && $status !== 'draft') { $validation->error('status', 'postNeedsApprovalBeforePublishing'); diff --git a/application/tests/datasets/ushahidi/Base.yml b/application/tests/datasets/ushahidi/Base.yml index 6101d43007..e72e69a85f 100644 --- a/application/tests/datasets/ushahidi/Base.yml +++ b/application/tests/datasets/ushahidi/Base.yml @@ -64,7 +64,8 @@ forms: name: "Test Form" type: "report" description: "Testing form" - require_approval: 1 + require_approval: 0 + hide_author: 0 everyone_can_create: 1 - id: 2 @@ -72,6 +73,7 @@ forms: type: "report" description: "Missing persons" require_approval: 0 + hide_author: 0 everyone_can_create: 0 - id: 3 @@ -79,33 +81,65 @@ forms: type: "report" description: "Test video embed" require_approval: 0 + hide_author: 0 + everyone_can_create: 0 + - + id: 4 + name: "Role Restriction" + type: "report" + description: "Test role restriction and private responses" + require_approval: 1 + hide_author: 1 everyone_can_create: 0 form_roles: - id: 1 form_id: 2 role_id: 1 + - + id: 2 + form_id: 4 + role_id: 2 + - + id: 3 + form_id: 4 + role_id: 3 form_stages: - id: 1 form_id: 1 label: "Main" + show_when_published: 1 - id: 2 form_id: 1 label: "2nd step" + show_when_published: 1 - id: 3 form_id: 1 label: "3rd step" + show_when_published: 1 - id: 4 form_id: 2 label: "Main" + show_when_published: 1 - id: 5 form_id: 3 label: "Post" + show_when_published: 1 + - + id: 6 + form_id: 4 + label: "restricted" + show_when_published: 0 + - + id: 7 + form_id: 4 + label: "Post" + show_when_published: 1 form_attributes: - id: 1 @@ -113,6 +147,7 @@ form_attributes: key: "test_varchar" type: "varchar" input: "text" + response_private: 0 required: 0 priority: 1 options: "" @@ -124,6 +159,7 @@ form_attributes: key: "test_point" type: "point" input: "location" + response_private: 0 required: 0 priority: 1 options: "" @@ -135,6 +171,7 @@ form_attributes: key: "full_name" type: "varchar" input: "text" + response_private: 0 required: 0 priority: 1 options: "" @@ -146,6 +183,7 @@ form_attributes: key: "description" type: "description" input: "textarea" + response_private: 0 required: 0 priority: 0 options: "" @@ -157,6 +195,7 @@ form_attributes: key: "date_of_birth" type: "datetime" input: "date" + response_private: 0 required: 0 priority: 3 options: "" @@ -168,6 +207,7 @@ form_attributes: key: "missing_date" type: "datetime" input: "date" + response_private: 0 required: 0 priority: 4 options: "" @@ -179,6 +219,7 @@ form_attributes: key: "last_location" type: "varchar" input: "text" + response_private: 0 required: 1 priority: 5 options: "" @@ -190,6 +231,7 @@ form_attributes: key: "last_location_point" type: "point" input: "location" + response_private: 0 required: 0 priority: 5 options: "" @@ -201,6 +243,7 @@ form_attributes: key: "geometry_test" type: "geometry" input: "text" + response_private: 0 required: 0 priority: 5 options: "" @@ -212,6 +255,7 @@ form_attributes: key: "missing_status" type: "varchar" input: "select" + response_private: 0 required: 0 options: '["information_sought","is_note_author","believed_alive","believed_missing","believed_dead"]' priority: 5 @@ -223,6 +267,7 @@ form_attributes: key: "links" type: "varchar" input: "text" + response_private: 0 required: 0 priority: 7 cardinality: 0 @@ -233,6 +278,7 @@ form_attributes: key: "second_point" type: "point" input: "location" + response_private: 0 required: 0 priority: 5 options: "" @@ -244,6 +290,7 @@ form_attributes: key: "person_status" type: "varchar" input: "select" + response_private: 0 required: 0 options: '["information_sought","is_note_author","believed_alive","believed_missing","believed_dead"]' priority: 5 @@ -255,6 +302,7 @@ form_attributes: key: "media_test" type: "media" input: "upload" + response_private: 0 required: 0 priority: 7 cardinality: 1 @@ -265,6 +313,7 @@ form_attributes: key: "possible_actions" type: "varchar" input: "checkbox" + response_private: 0 required: 0 options: '["ground_search","medical_evacuation"]' priority: 5 @@ -277,6 +326,7 @@ form_attributes: key: "video_field" type: "video" input: "video" + response_private: 0 required: 0 options: '[]' priority: 5 @@ -289,6 +339,7 @@ form_attributes: key: "title" type: "title" input: "text" + response_private: 0 required: 0 priority: 0 options: "" @@ -296,10 +347,95 @@ form_attributes: form_stage_id: 1 - id: 18 + label: "Test Field Level Locking 1" + key: "test_field_locking_hidden_1" + type: "text" + input: "text" + response_private: 1 + required: 0 + priority: 0 + options: "" + cardinality: 1 + form_stage_id: 6 + - + id: 19 + label: "Test Field Level Locking 2" + key: "test_field_locking_visible_1" + type: "varchar" + input: "text" + response_private: 0 + required: 0 + priority: 0 + options: "" + cardinality: 1 + form_stage_id: 6 + - + id: 20 + label: "Test Field Level Locking 3" + key: "test_field_locking_visible_2" + type: "title" + input: "text" + response_private: 0 + required: 0 + priority: 0 + options: "" + cardinality: 1 + form_stage_id: 6 + - + id: 21 + label: "Test Field Level Locking 4" + key: "test_field_locking_hidden_2" + type: "text" + input: "text" + response_private: 1 + required: 0 + priority: 0 + options: "" + cardinality: 1 + form_stage_id: 6 + - + id: 22 + label: "Test Field Level Locking 5" + key: "test_field_locking_visible_3" + type: "varchar" + input: "text" + response_private: 0 + required: 0 + priority: 0 + options: "" + cardinality: 1 + form_stage_id: 6 + - + id: 23 + label: "Test Field Level Locking 6" + key: "test_field_locking_hidden_3" + type: "text" + input: "text" + response_private: 1 + required: 0 + priority: 0 + options: "" + cardinality: 1 + form_stage_id: 7 + - + id: 24 + label: "Test Field Level Locking 7" + key: "test_field_locking_visible_4" + type: "varchar" + input: "text" + response_private: 0 + required: 0 + priority: 0 + options: "" + cardinality: 1 + form_stage_id: 7 + - + id: 25 label: "Test markdown" key: "markdown" type: "markdown" input: "text" + response_private: 0 required: 0 priority: 0 options: "" @@ -605,6 +741,36 @@ posts: created: 1412025016 published_to: '["user"]' post_date: "2014-09-29 14:10:16" + - + id: 121 + form_id: 4 + parent_id: + user_id: 3 + author_email: "test@ushahidi.com" + author_realname: "Test Name" + title: "Post published to members" + type: "report" + content: "Post published to members" + status: "published" + locale: "en_us" + created: 1412025016 + published_to: '[]' + post_date: "2014-09-29 14:10:16" + - + id: 122 + form_id: 4 + parent_id: + user_id: 3 + author_email: "test@ushahidi.com" + author_realname: "Test Name" + title: "Post published to members" + type: "report" + content: "Post published to members" + status: "published" + locale: "en_us" + created: 1412025016 + published_to: '["user"]' + post_date: "2014-09-29 14:10:16" post_datetime: post_decimal: post_geometry: @@ -678,6 +844,56 @@ post_varchar: post_id: 1 form_attribute_id: 11 value: "http://ushahidi.com" + - + id: 13 + post_id: 121 + form_attribute_id: 20 + value: "arabic string" + - + id: 14 + post_id: 121 + form_attribute_id: 21 + value: "http://google.com" + - + id: 15 + post_id: 121 + form_attribute_id: 22 + value: "http://ushahidi.com" + - + id: 16 + post_id: 121 + form_attribute_id: 23 + value: "arabic string" + - + id: 17 + post_id: 121 + form_attribute_id: 24 + value: "arabic string" + - + id: 18 + post_id: 122 + form_attribute_id: 20 + value: "arabic string" + - + id: 19 + post_id: 122 + form_attribute_id: 21 + value: "http://google.com" + - + id: 20 + post_id: 122 + form_attribute_id: 22 + value: "http://ushahidi.com" + - + id: 21 + post_id: 122 + form_attribute_id: 23 + value: "arabic string" + - + id: 22 + post_id: 122 + form_attribute_id: 24 + value: "arabic string" post_comments: tags: - diff --git a/application/tests/features/api.acl.feature b/application/tests/features/api.acl.feature index 89bbb1f913..9211bb4f16 100644 --- a/application/tests/features/api.acl.feature +++ b/application/tests/features/api.acl.feature @@ -1,12 +1,204 @@ @acl Feature: API Access Control Layer + + Scenario: Anonymous users can create posts + Given that I want to make a new "Post" + And that the request "Authorization" header is "Bearer testanon" + And that the request "data" is: + """ + { + "form_id": 1, + "status": "draft", + "title": "Test creating anonymous post", + "content": "testing post for oauth", + "locale": "en_us", + "values": { + "last_location" : ["Somewhere"] + } + } + """ + When I request "/posts" + Then the guzzle status code should be 204 + + Scenario: Anonymous user can not see restricted fields of public posts + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testanon" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response does not have a "values.test_field_locking_hidden_3" property + Then the guzzle status code should be 200 + + Scenario: Anonymous user can not see hidden tasks of public posts + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testanon" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response does not have a "values.test_field_locking_visible_2" property + Then the guzzle status code should be 200 + + Scenario: Anonymous user can not see hidden author field of public posts + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testanon" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response does not have a "author_realname" property + And the response does not have a "author_email" property + And the response does not have a "user_id" property + Then the guzzle status code should be 200 + + Scenario: User can not see hidden tasks of public posts + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testbasicuser" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response does not have a "values.test_field_locking_visible_2" property + Then the guzzle status code should be 200 + + Scenario: User can not see restricted fields of public posts + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testbasicuser" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response does not have a "values.test_field_locking_hidden_3" property + Then the guzzle status code should be 200 + + Scenario: User can not see hidden author field of public posts + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testanon" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response does not have a "author_realname" property + And the response does not have a "author_email" property + And the response does not have a "user" property + Then the guzzle status code should be 200 + + Scenario: User can see restricted fields of posts published to their role when survey restricted to their role + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testmanager" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response has a "values.test_field_locking_hidden_3" property + Then the guzzle status code should be 200 + + Scenario: User can see hidden author field of posts published to their role when survey restricted to their role + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testmanager" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response has a "author_realname" property + And the response has a "author_email" property + And the response has a "user" property + Then the guzzle status code should be 200 + + Scenario: User can not see hidden tasks of posts published to their role + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testbasicuser" + And that its "id" is "122" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response does not have a "values.test_field_locking_visible_2" property + Then the guzzle status code should be 200 + + Scenario: User can see restricted fields of posts published to their role + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testbasicuser" + And that its "id" is "122" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response does not have a "values.test_field_locking_hidden_3" property + Then the guzzle status code should be 200 + + Scenario: User can not see hidden author field of posts published to their role + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testbasicuser" + And that its "id" is "122" + When I request "/posts" + Then the response is JSON + And the response does not have a "author_realname" property + And the response does not have a "author_email" property + And the response does not have a "user" property + Then the guzzle status code should be 200 + + Scenario: Listing All Stages for a form with hidden stages + Given that I want to get all "Stages" + And that the request "Authorization" header is "Bearer testbasicuser" + When I request "/forms/4/stages" + Then the response is JSON + And the response has a "count" property + And the type of the "count" property is "numeric" + And the "count" property equals "1" + Then the guzzle status code should be 200 + + Scenario: Listing All Stages for a form with hidden stages with edit permission + Given that I want to get all "Stages" + And that the request "Authorization" header is "Bearer testmanager" + When I request "/forms/4/stages" + Then the response is JSON + And the response has a "count" property + And the type of the "count" property is "numeric" + And the "count" property equals "2" + Then the guzzle status code should be 200 + + Scenario: Listing All Attributes for a form with hidden stages + Given that I want to get all "Stages" + And that the request "Authorization" header is "Bearer testbasicuser" + When I request "/forms/4/attributes" + Then the response is JSON + And the response has a "count" property + And the type of the "count" property is "numeric" + And the "count" property equals "4" + Then the guzzle status code should be 200 + + Scenario: Listing All Attributes for a form with hidden stages with edit permission + Given that I want to get all "Stages" + And that the request "Authorization" header is "Bearer testmanager" + When I request "/forms/4/attributes" + Then the response is JSON + And the response has a "count" property + And the type of the "count" property is "numeric" + And the "count" property equals "7" + Then the guzzle status code should be 200 + + Scenario: User can see hidden tasks of posts published when survey restricted to their role + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testmanager" + And that its "id" is "121" + When I request "/posts" + Then the response is JSON + And the response has a "values" property + And the response has a "values.test_field_locking_visible_4" property + And the response has a "values.test_field_locking_visible_2" property + Then the guzzle status code should be 200 + Scenario: Anonymous user can access public posts Given that I want to get all "Posts" And that the request "Authorization" header is "Bearer testanon" When I request "/posts" Then the guzzle status code should be 200 And the response is JSON - And the "count" property equals "10" + And the "count" property equals "11" Scenario: All users can view public posts Given that I want to get all "Posts" @@ -15,7 +207,7 @@ Feature: API Access Control Layer When I request "/posts" Then the guzzle status code should be 200 And the response is JSON - And the "count" property equals "11" + And the "count" property equals "13" Scenario: User can view public and own private posts in collection Given that I want to get all "Posts" @@ -24,7 +216,7 @@ Feature: API Access Control Layer When I request "/posts" Then the guzzle status code should be 200 And the response is JSON - And the "count" property equals "13" + And the "count" property equals "15" Scenario: Admin can view all posts in collection Given that I want to get all "Posts" @@ -33,7 +225,7 @@ Feature: API Access Control Layer When I request "/posts" Then the guzzle status code should be 200 And the response is JSON - And the "count" property equals "16" + And the "count" property equals "19" Scenario: Admin user can view private posts Given that I want to find a "Post" @@ -103,27 +295,6 @@ Feature: API Access Control Layer And the response is JSON And the response has an "errors" property - Scenario: Anonymous users can create posts - Given that I want to make a new "Post" - And that the request "Authorization" header is "Bearer testanon" - And that the request "data" is: - """ - { - "form_id": 1, - "status": "draft", - "title": "Test creating anonymous post", - "content": "testing post for oauth", - "locale": "en_us", - "values": { - "last_location" : ["Somewhere"] - } - } - """ - When I request "/posts" - Then the guzzle status code should be 204 - - - Scenario: Anonymous users can not edit posts Given that I want to update a "Post" And that the request "Authorization" header is "Bearer testanon" @@ -365,6 +536,16 @@ Feature: API Access Control Layer And the response is JSON And the "id" property equals "120" + @resetFixture + Scenario: Anonymous can not view private responses + Given that I want to find a "Post" + And that the request "Authorization" header is "Bearer testanon" + And that its "id" is "121" + When I request "/posts" + Then the guzzle status code should be 200 + And the response is JSON + And the "id" property equals "121" + @resetFixture Scenario: Anonymous can not view post published to members Given that I want to find a "Post" @@ -434,7 +615,7 @@ Feature: API Access Control Layer When I request "/posts" Then the guzzle status code should be 200 And the response is JSON - And the "count" property equals "16" + And the "count" property equals "18" @rolesEnabled Scenario: User with Manage Posts permission can view private posts @@ -556,4 +737,3 @@ Feature: API Access Control Layer And the response has a "columns" property And the "columns.0" property equals "title" Then the guzzle status code should be 200 - diff --git a/application/tests/features/api.posts.feature b/application/tests/features/api.posts.feature index 8dd04435ae..f27105a74b 100644 --- a/application/tests/features/api.posts.feature +++ b/application/tests/features/api.posts.feature @@ -178,7 +178,7 @@ Feature: Testing the Posts API 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 + Scenario: Creating a Post with a form that does not require approval but try to set status should pass Given that I want to make a new "Post" And that the request "Authorization" header is "Bearer testbasicuser" And that the request "data" is: @@ -199,7 +199,7 @@ Feature: Testing the Posts API """ When I request "/posts" Then the response is JSON - Then the guzzle status code should be 422 + Then the guzzle status code should be 200 @create Scenario: Creating an Post with invalid data returns an error @@ -921,8 +921,8 @@ Feature: Testing the Posts API Then the response is JSON And the response has a "count" property And the type of the "count" property is "numeric" - And the "count" property equals "12" - And the "total_count" property equals "12" + And the "count" property equals "14" + And the "total_count" property equals "14" Then the guzzle status code should be 200 @resetFixture @search @@ -977,8 +977,8 @@ Feature: Testing the Posts API Then the response is JSON And the response has a "count" property And the type of the "count" property is "numeric" - And the "count" property equals "1" - And the "total_count" property equals "1" + And the "count" property equals "3" + And the "total_count" property equals "3" Then the guzzle status code should be 200 @resetFixture @search diff --git a/application/tests/features/forms/api.attributes.feature b/application/tests/features/forms/api.attributes.feature index b88bf72e6f..a1ed03473c 100644 --- a/application/tests/features/forms/api.attributes.feature +++ b/application/tests/features/forms/api.attributes.feature @@ -184,7 +184,7 @@ Feature: Testing the Form Attributes API Then the response is JSON And the response has a "count" property And the type of the "count" property is "numeric" - And the "count" property equals "20" + And the "count" property equals "27" Then the guzzle status code should be 200 Scenario: Search for point attributes diff --git a/migrations/20170317174120_add_private_response_to_form_attribute.php b/migrations/20170317174120_add_private_response_to_form_attribute.php new file mode 100644 index 0000000000..8af35a567d --- /dev/null +++ b/migrations/20170317174120_add_private_response_to_form_attribute.php @@ -0,0 +1,29 @@ +table('form_attributes') + ->addColumn('response_private', 'boolean', [ + 'default' => false, + 'null' => false, + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('form_attributes') + ->removeColumn('response_private') + ->update(); + } +} diff --git a/migrations/20170322160413_add_hide_author_to_form.php b/migrations/20170322160413_add_hide_author_to_form.php new file mode 100644 index 0000000000..6f89e3d820 --- /dev/null +++ b/migrations/20170322160413_add_hide_author_to_form.php @@ -0,0 +1,29 @@ +table('forms') + ->addColumn('hide_author', 'boolean', [ + 'default' => false, + 'null' => false, + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('forms') + ->removeColumn('hide_author') + ->update(); + } +} diff --git a/migrations/20170322162237_add_description_field_to_form_attribute.php b/migrations/20170322162237_add_description_field_to_form_attribute.php new file mode 100644 index 0000000000..c09fd577ca --- /dev/null +++ b/migrations/20170322162237_add_description_field_to_form_attribute.php @@ -0,0 +1,28 @@ +table('form_attributes') + ->addColumn('description', 'string', [ + 'null' => true + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('form_attributes') + ->removeColumn('description') + ->update(); + } +} diff --git a/migrations/20170331210813_add_show_when_published_to_form_stage.php b/migrations/20170331210813_add_show_when_published_to_form_stage.php new file mode 100644 index 0000000000..430c205f55 --- /dev/null +++ b/migrations/20170331210813_add_show_when_published_to_form_stage.php @@ -0,0 +1,29 @@ +table('form_stages') + ->addColumn('show_when_published', 'boolean', [ + 'null' => false, + 'default' => false + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('form_stages') + ->removeColumn('show_when_published') + ->update(); + } +} diff --git a/src/Core/Entity/Form.php b/src/Core/Entity/Form.php index 99dd8b2590..1ede6c6225 100644 --- a/src/Core/Entity/Form.php +++ b/src/Core/Entity/Form.php @@ -24,6 +24,7 @@ class Form extends StaticEntity protected $disabled; protected $created; protected $updated; + protected $hide_author; protected $require_approval; protected $everyone_can_create; protected $can_create; @@ -46,6 +47,7 @@ protected function getDefinition() 'disabled' => 'bool', 'created' => 'int', 'updated' => 'int', + 'hide_author' => 'bool', 'require_approval' => 'bool', 'everyone_can_create' => 'bool', 'can_create' => 'array', diff --git a/src/Core/Entity/FormAttribute.php b/src/Core/Entity/FormAttribute.php index fac76c818b..28c16724b2 100644 --- a/src/Core/Entity/FormAttribute.php +++ b/src/Core/Entity/FormAttribute.php @@ -28,6 +28,7 @@ class FormAttribute extends StaticEntity protected $cardinality; protected $config = []; protected $form_stage_id; + protected $response_private; // StatefulData protected function getDerived() @@ -55,6 +56,7 @@ protected function getDefinition() 'config' => '*json', 'form_stage' => false, /* alias */ 'form_stage_id' => 'int', + 'response_private' => 'bool', ]; } diff --git a/src/Core/Entity/FormStage.php b/src/Core/Entity/FormStage.php index ba74a88c3a..a42dd1c8d3 100644 --- a/src/Core/Entity/FormStage.php +++ b/src/Core/Entity/FormStage.php @@ -22,6 +22,7 @@ class FormStage extends StaticEntity protected $icon; protected $type; protected $required; + protected $show_when_published; protected $description; // DataTransformer @@ -30,6 +31,7 @@ protected function getDefinition() return [ 'id' => 'int', 'description' => 'string', + 'show_when_published' => 'boolean', 'type' => 'string', 'form_id' => 'int', 'label' => 'string', diff --git a/src/Core/Traits/PostValueRestrictions.php b/src/Core/Traits/PostValueRestrictions.php new file mode 100644 index 0000000000..2c409543c0 --- /dev/null +++ b/src/Core/Traits/PostValueRestrictions.php @@ -0,0 +1,96 @@ + + * @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\Traits; + +use Ushahidi\Core\Entity\User; +use Ushahidi\Core\Entity\Post; +use Ushahidi\Core\Entity\FormRepository; + +trait PostValueRestrictions +{ + + public function canUserSeeAuthor(Post $post, FormRepository $form_repo) + { + if ($post->form_id) { + return !$form_repo->isAuthorHidden($post->form_id); + } + + return true; + } + + protected function isUserOfRole($roles, $user) + { + if ($roles) { + return in_array($user->role, $roles); + } + + // If no visibility info, assume public + return true; + } + + protected function isPostPublishedToUser(Post $post, $user) + { + // Anon users can not see restricted fields + if (!$user->getId()) { + return false; + } + + if ($post->status === 'published' && $this->isUserOfRole($post->published_to, $user)) { + return true; + } + return false; + } + + public function isRestricted($form_id) + { + + $user = $this->getUser(); + if ($form_id) { + return !$this->canUserEditForm($form_id, $user, $this->form_repo); + } + + return false; + } + + /** + * Test whether the post instance requires value restriction + * @param Post $post + * @return Boolean + */ + public function canUserReadPostsValues(Post $post, $user, FormRepository $form_repo) + { + if ($this->canUserEditForm($post->form_id, $user, $form_repo) && $this->isPostPublishedToUser($post, $user)) { + return true; + } + return false; + } + + /* FormRole */ + protected function canUserEditForm($form_id, $user, $form_repo) + { + // 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 ($form_id) { + $roles = $form_repo->getRolesThatCanCreatePosts($form_id); + if ($roles['everyone_can_create'] > 0) { + return true; + } + if (is_array($roles['roles'])) { + return $this->isUserOfRole($roles['roles'], $user); + } + } + + return false; + } +} diff --git a/src/Core/Usecase/Post/ValuesForPostRepository.php b/src/Core/Usecase/Post/ValuesForPostRepository.php index c0fd608109..d0f7153634 100644 --- a/src/Core/Usecase/Post/ValuesForPostRepository.php +++ b/src/Core/Usecase/Post/ValuesForPostRepository.php @@ -19,7 +19,12 @@ interface ValuesForPostRepository * @param Array $include_attributes * @return [Ushahidi\Core\Entity\PostValue, ...] */ - public function getAllForPost($post_id, Array $include_attributes = []); + public function getAllForPost( + $post_id, + Array $include_attributes = [], + Array $exclude_stages = [], + $restricted = false + ); /** * @param int $post_id