diff --git a/.travis.yml b/.travis.yml index d3aa010937..00b520b42e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,7 @@ sudo: false -notifications: - hipchat: - rooms: - secure: fb2eYR4DVBestPl/DSG7lnUWT46Rmr9BfRsZFe4dhrS3ZetkZ1XZueF6SCtB4yukaJoDlA7y98FgQ7eGx6OGToc4ALnPpwd3BdfJ5RV/PK/6LG7u5Mp4+DfB4lG1q9IjuHF7dhBPSk2sRd8ewtwV5JR4SHKYfHxSR9Ekwmn1Xyo= - template: - - '%{repository}#%{build_number} (%{branch} - %{commit} - : %{author}): %{message}' - format: html - notify: true - slack: - secure: NxhzhKGYnZYZiWVZM4w1PlZpTy9bbmNlvbqjL+9d4PvK4hFzPwQU12CchhBPeXNGLWBHg/ti6Scn/t5XIS3YJrkk4ydWmJBht5UId8uFZ1A7GCUnNNPTt2RG55lbJJdZSj01rOGZjEQbE5RyckEjgRzhZjdZ+HsjoPaRzWIBrvE= +#notifications: +# slack: +# secure: NxhzhKGYnZYZiWVZM4w1PlZpTy9bbmNlvbqjL+9d4PvK4hFzPwQU12CchhBPeXNGLWBHg/ti6Scn/t5XIS3YJrkk4ydWmJBht5UId8uFZ1A7GCUnNNPTt2RG55lbJJdZSj01rOGZjEQbE5RyckEjgRzhZjdZ+HsjoPaRzWIBrvE= language: php php: - '5.5' diff --git a/application/classes/Ushahidi/Console/Webhook.php b/application/classes/Ushahidi/Console/Webhook.php index 3126844583..98f8f27be5 100644 --- a/application/classes/Ushahidi/Console/Webhook.php +++ b/application/classes/Ushahidi/Console/Webhook.php @@ -126,7 +126,7 @@ private function generateRequest($webhook_request) // This is an asynchronous request, we don't expect a result // this can be extended to allow for handling of the returned promise - $promise = $this->client->requestAsync('POST', $webhook->url, [ + $promise = $this->client->request('POST', $webhook->url, [ 'headers' => [ 'X-Platform-Signature' => $signature, 'Accept' => 'application/json' diff --git a/application/classes/Ushahidi/Core.php b/application/classes/Ushahidi/Core.php index 55f3c578f2..4774fbf91c 100644 --- a/application/classes/Ushahidi/Core.php +++ b/application/classes/Ushahidi/Core.php @@ -351,53 +351,8 @@ public static function init() // Set Formatter factory $di->params['Ushahidi\Factory\FormatterFactory']['factory'] = $di->newFactory('Ushahidi_Formatter_Collection'); - // Helpers, tools, etc - $di->set('tool.hasher.password', $di->lazyNew('Ushahidi_Hasher_Password')); - $di->set('tool.signer', $di->lazyNew('Ushahidi\Core\Tool\Signer')); - $di->set('tool.authenticator.password', $di->lazyNew('Ushahidi_Authenticator_Password')); - $di->set('tool.validation', $di->lazyNew('Ushahidi_ValidationEngine')); $di->set('tool.jsontranscode', $di->lazyNew('Ushahidi\Core\Tool\JsonTranscode')); - $di->set('tool.acl', $di->lazyNew('Ushahidi_Acl')); - $di->setter['Ushahidi_Acl']['setRoleRepo'] = $di->lazyGet('repository.role'); - - // Register filesystem adpater types - // Currently supported: Local filesysten, AWS S3 v3, Rackspace - // the naming scheme must match the cdn type set in config/cdn - $di->set('adapter.local', $di->lazyNew( - 'Ushahidi_FilesystemAdapter_Local', - [ - 'config' => $di->lazyGet('cdn.config') - ] - ) - ); - $di->set('adapter.aws', $di->lazyNew( - 'Ushahidi_FilesystemAdapter_AWS', - [ - 'config' => $di->lazyGet('cdn.config') - ] - ) - ); - $di->set('adapter.rackspace', $di->lazyNew( - 'Ushahidi_FilesystemAdapter_Rackspace', - [ - 'config' => $di->lazyGet('cdn.config') - ] - ) - ); - - // Media Filesystem - // The Ushahidi filesystem adapter returns a flysystem adapter for a given - // cdn type based on the provided configuration - $di->set('tool.filesystem', $di->lazyNew('Ushahidi_Filesystem')); - $di->params['Ushahidi_Filesystem'] = [ - 'adapter' => $di->lazy(function () use ($di) { - $adapter_type = $di->get('cdn.config'); - $fsa = $di->get('adapter.' . $adapter_type['type']); - - return $fsa->getAdapter(); - }) - ]; // Formatters $di->set('formatter.entity.api', $di->lazyNew('Ushahidi_Formatter_API')); @@ -472,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'), @@ -572,7 +538,8 @@ public static function init() 'user_repo' => $di->lazyGet('repository.user'), 'collection_repo' => $di->lazyGet('repository.set'), 'savedsearch_repo' => $di->lazyGet('repository.savedsearch'), - ];$di->params['Ushahidi_Validator_Webhook_Update'] = [ + ]; + $di->params['Ushahidi_Validator_Webhook_Update'] = [ 'user_repo' => $di->lazyGet('repository.user'), ]; $di->params['Ushahidi_Validator_SavedSearch_Create'] = [ @@ -663,7 +630,7 @@ public static function init() 'point' => $di->lazyGet('validator.post.point'), 'relation' => $di->lazyGet('validator.post.relation'), 'varchar' => $di->lazyGet('validator.post.varchar'), - 'markdown' => $di->lazyGet('validator.post.markdown'), + 'markdown' => $di->lazyGet('validator.post.markdown'), 'title' => $di->lazyGet('validator.post.title'), 'media' => $di->lazyGet('validator.post.media'), 'video' => $di->lazyGet('validator.post.video'), @@ -680,12 +647,6 @@ public static function init() $di->setter['Ushahidi_Transformer_CSVPostTransformer']['setRepo'] = $di->lazyGet('repository.post'); - $di->set('filereader.csv', $di->lazyNew('Ushahidi_FileReader_CSV')); - $di->setter['Ushahidi_FileReader_CSV']['setReaderFactory'] = - $di->lazyGet('csv.reader_factory'); - - $di->set('csv.reader_factory', $di->lazyNew('Ushahidi_CSVReaderFactory')); - $di->set('tool.mailer', $di->lazyNew('Ushahidi_Mailer')); // Event listener for the Set repo @@ -711,70 +672,6 @@ public static function init() $di->setter['Ushahidi_Listener_PostListener']['setWebhookRepo'] = $di->lazyGet('repository.webhook'); - // Defined memcached - $di->set('memcached', $di->lazy(function () use ($di) { - $config = $di->get('ratelimiter.config'); - - $memcached = new Memcached(); - $memcached->addServer($config['memcached']['host'], $config['memcached']['port']); - - return $memcached; - })); - - // Set up login rate limiter - $di->set('ratelimiter.login.flap', $di->lazyNew('BehEh\Flaps\Flap')); - - $di->params['BehEh\Flaps\Flap'] = [ - 'storage' => $di->lazyNew('BehEh\Flaps\Storage\DoctrineCacheAdapter'), - 'name' => 'login' - ]; - - $di->set('ratelimiter.login.strategy', $di->lazyNew('BehEh\Flaps\Throttling\LeakyBucketStrategy')); - - // 3 requests every 1 minute by default - $di->params['BehEh\Flaps\Throttling\LeakyBucketStrategy'] = [ - 'requests' => 3, - 'timeSpan' => '1m' - ]; - - $di->set('ratelimiter.login', $di->lazyNew('Ushahidi_RateLimiter')); - - $di->params['Ushahidi_RateLimiter'] = [ - 'flap' => $di->lazyGet('ratelimiter.login.flap'), - 'throttlingStrategy' => $di->lazyGet('ratelimiter.login.strategy'), - ]; - - $di->params['BehEh\Flaps\Storage\DoctrineCacheAdapter'] = [ - 'cache' => $di->lazyGet('ratelimiter.cache') - ]; - - // Rate limit storage cache - $di->set('ratelimiter.cache', function() use ($di) { - $config = $di->get('ratelimiter.config'); - $cache = $config['cache']; - - if ($cache === 'memcached') { - $di->setter['Doctrine\Common\Cache\MemcachedCache']['setMemcached'] = - $di->lazyGet('memcached'); - - return $di->newInstance('Doctrine\Common\Cache\MemcachedCache'); - } - elseif ($cache === 'filesystem') { - $di->params['Doctrine\Common\Cache\FilesystemCache'] = [ - 'directory' => $config['filesystem']['directory'], - ]; - - return $di->newInstance('Doctrine\Common\Cache\FilesystemCache'); - } - - // Fall back to using in-memory cache if none is configured - return $di->newInstance('Doctrine\Common\Cache\ArrayCache'); - }); - - // Rate limiter violation handler - $di->setter['BehEh\Flaps\Flap']['setViolationHandler'] = - $di->lazyNew('Ushahidi_ThrottlingViolationHandler'); - /** * 1. Load the plugins */ diff --git a/application/classes/Ushahidi/Formatter/Post/CSV.php b/application/classes/Ushahidi/Formatter/Post/CSV.php index 899a5af7ac..fbcf155cc8 100644 --- a/application/classes/Ushahidi/Formatter/Post/CSV.php +++ b/application/classes/Ushahidi/Formatter/Post/CSV.php @@ -88,7 +88,6 @@ protected function generateCSVRecords($records) // Sort the keys so that they match with columns from the CSV heading ksort($record); - Kohana::$log->add(Log::ERROR, print_r($record, true)); fputcsv($fp, $record); } diff --git a/application/classes/Ushahidi/Formatter/Tag.php b/application/classes/Ushahidi/Formatter/Tag.php index c23d171618..d603131ed8 100644 --- a/application/classes/Ushahidi/Formatter/Tag.php +++ b/application/classes/Ushahidi/Formatter/Tag.php @@ -21,4 +21,15 @@ protected function format_color($value) $value = ltrim($value, '#'); return $value ? '#' . $value : null; } + + protected function format_forms($forms) + { + $output = []; + foreach ($forms as $formid) + { + $output[] = $this->get_relation('forms', $formid); + } + + return $output; + } } diff --git a/application/classes/Ushahidi/FormsTagsTrait.php b/application/classes/Ushahidi/FormsTagsTrait.php new file mode 100644 index 0000000000..20fdbc5272 --- /dev/null +++ b/application/classes/Ushahidi/FormsTagsTrait.php @@ -0,0 +1,122 @@ + + * @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) +*/ + +trait Ushahidi_FormsTagsTrait +{ + //returning forms for a specific Tag-id + private function getFormsForTag($id) + { + $result = DB::select('form_id') + ->from('forms_tags') + ->where('tag_id', '=', $id) + ->execute($this->db); + return $result->as_array(NULL, 'form_id'); + } + //returning tags for a specific Form-id + private function getTagsForForm($id) + { + $result = DB::select('tag_id') + ->from('forms_tags') + ->where('form_id', '=', $id) + ->execute($this->db); + return $result->as_array(NULL, 'tag_id'); + } + + // updating/adding tags to a form + private function updateFormsTags($form_id, $tags) + { + if (!$tags) { + DB::delete('forms_tags') + ->where('form_id', '=', $form_id) + ->execute($this->db); + } else if ($tags) { + $existing = $this->getTagsForForm($form_id); + $insert = DB::insert('forms_tags', ['form_id', 'tag_id']); + $tag_ids = []; + $new_tags = FALSE; + foreach ($tags as $tag) { + if (!in_array($tag, $existing)) { + $insert->values([$form_id, $tag]); + $new_tags = TRUE; + } + $tag_ids[] = $tag; + } + if ($new_tags) { + $insert->execute($this->db); + } + if (!empty($tag_ids)) { + DB::delete('forms_tags') + ->where('tag_id', 'NOT IN', $tag_ids) + ->and_where('form_id', '=', $form_id) + ->execute($this->db); + } + } + } + + //updating/adding forms to a tag + private function updateTagForms($tag_id, $forms) + { + if (empty($forms)) { + DB::delete('forms_tags') + ->where('tag_id', '=', $tag_id) + ->execute($this->db); + } else { + $existing = $this->getFormsForTag($tag_id); + $insert = DB::insert('forms_tags', ['form_id', 'tag_id']); + $form_ids = []; + $new_forms = FALSE; + foreach ($forms as $form) { + if (isset($form['id'])) { + $id = $form['id']; + } else { + $id = $form; + } + if (!in_array($form, $existing)) { + $insert->values([$id, $tag_id]); + $new_forms = TRUE; + } + $form_ids[] = $id; + } + + if ($new_forms) { + $insert->execute($this->db); + } + + if (!empty($form_ids)) { + DB::delete('forms_tags') + ->where('form_id', 'NOT IN', $form_ids) + ->and_where('tag_id', '=', $tag_id) + ->execute($this->db); + } + } + } + + private function updateFormAttributes($id) + { + $attr = DB::select('id', 'options') + ->from('form_attributes') + ->where('input', '=', 'tags') + ->execute($this->db) + ->as_array('id', 'options'); + foreach ($attr as $attr_id => $value) { + $value = json_decode($value); + if (in_array($id, $value)) { + $index = array_search($id, $value); + array_splice($value, $index, 1); + $value = json_encode($value); + DB::update('form_attributes') + ->set(array('options' => $value)) + ->where('id', '=', $attr_id) + ->execute($this->db); + } + } + } +} diff --git a/application/classes/Ushahidi/Repository/Form.php b/application/classes/Ushahidi/Repository/Form.php index 61f14a66d4..cc240e9e94 100644 --- a/application/classes/Ushahidi/Repository/Form.php +++ b/application/classes/Ushahidi/Repository/Form.php @@ -17,6 +17,8 @@ class Ushahidi_Repository_Form extends Ushahidi_Repository implements FormRepository { + use Ushahidi_FormsTagsTrait; + // Ushahidi_Repository protected function getTable() { @@ -27,11 +29,12 @@ protected function getTable() // ReadRepository public function getEntity(Array $data = null) { - if (isset($data["id"])) { + if (isset($data["id"])) { $can_create = $this->getRolesThatCanCreatePosts($data['id']); $data = $data + [ - 'can_create' => $can_create['roles'], + 'can_create' => $can_create['roles'], ]; + $data['tags'] = $this->getTagsForForm($data['id']); } return new Form($data); } @@ -46,7 +49,6 @@ public function getSearchFields() protected function setSearchConditions(SearchData $search) { $query = $this->search_query; - if ($search->parent) { $query->where('parent_id', '=', $search->parent); } @@ -60,15 +62,31 @@ protected function setSearchConditions(SearchData $search) // CreateRepository public function create(Entity $entity) { - // todo ensure default group is created - return parent::create($entity->setState(['created' => time()])); + $tags = $entity->tags; + unset($entity->tags); + $id = parent::create($entity->setState(['created' => time()])); + //updating forms_tags-table + if ($tags && $id !== null) { + $this->updateFormsTags($id, $tags); + } + // todo ensure default group is created + return $id; } // UpdateRepository public function update(Entity $entity) { - return parent::update($entity->setState(['updated' => time()])); + $tags = $entity->tags; + unset($entity->tags); + unset($entity->children); + $id = parent::update($entity->setState(['updated' => time()])); + // updating forms_tags-table + if ($tags && $entity->id !== null) { + $this->updateFormsTags($entity->id, $tags); + } + + return $id; } /** @@ -81,6 +99,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 @@ -103,7 +138,7 @@ public function getRolesThatCanCreatePosts($form_id) $roles = []; - foreach($results as $role) { + foreach ($results as $role) { if (!is_null($role['name'])) { $roles[] = $role['name']; } @@ -113,7 +148,5 @@ public function getRolesThatCanCreatePosts($form_id) '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 27328d2745..ba15c0fa1e 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,8 +24,38 @@ 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; + use Ushahidi_FormsTagsTrait; + + /** + * 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() @@ -29,6 +63,27 @@ 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(); + } + + return $query; + } + // CreateRepository public function create(Entity $entity) { @@ -42,6 +97,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 +170,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 +209,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 +222,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..86c0139032 100644 --- a/application/classes/Ushahidi/Repository/Form/Stage.php +++ b/application/classes/Ushahidi/Repository/Form/Stage.php @@ -13,16 +13,68 @@ 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; + +use Ushahidi\Core\Traits\AdminAccess; +use Ushahidi\Core\Traits\PermissionAccess; +use Ushahidi\Core\Traits\Permissions\ManagePosts; class Ushahidi_Repository_Form_Stage extends Ushahidi_Repository implements FormStageRepository { + use UserContext; + + use PostValueRestrictions; + + // Checks if user is Admin + use AdminAccess; + + // Provides `hasPermission` + use PermissionAccess; + + // Provides `getPermission` + use ManagePosts; + + 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); + + $user = $this->getUser(); + if (!$this->canUserEditForm($form_id, $user)) { + $query->where('show_when_published', '=', "1"); + } + + return $query; + } + // CreateRepository // ReadRepository public function getEntity(Array $data = null) @@ -36,6 +88,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 +134,51 @@ 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('form_id', '=', $form_id) + ->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 +191,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..e875ee555f 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,20 @@ protected function getTable() // Ushahidi_Repository public function getEntity(Array $data = null) { + // Ensure we are dealing with a structured Post + + $user = $this->getUser(); + if ($data['form_id']) + { + + if ($this->canUserReadPostsValues(new Post($data), $user, $this->form_repo)) { + $this->restricted = false; + } + // 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 +137,21 @@ 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, $user)) + { + unset($data['author_realname']); + unset($data['author_email']); + unset($data['user_id']); + } + } + return new Post($data); } @@ -142,12 +179,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 +199,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 +263,7 @@ protected function setSearchConditions(SearchData $search) $query = $this->search_query; $table = $this->getTable(); - + // Filter by status $status = $search->getFilter('status', ['published']); // @@ -324,7 +367,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 +571,7 @@ protected function setSearchConditions(SearchData $search) ->where("$table.status", '=', 'published') ->or_where("$table.user_id", '=', $user->id) ->and_where_close(); - } + } } // SearchRepository @@ -1067,4 +1110,4 @@ public function doesPostRequireApproval($formId) return true; } -} +} \ No newline at end of file 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/Repository/Tag.php b/application/classes/Ushahidi/Repository/Tag.php index bebf0ad70e..8aa1de1bc5 100644 --- a/application/classes/Ushahidi/Repository/Tag.php +++ b/application/classes/Ushahidi/Repository/Tag.php @@ -23,7 +23,8 @@ class Ushahidi_Repository_Tag extends Ushahidi_Repository implements { // Use the JSON transcoder to encode properties use Ushahidi_JsonTranscodeRepository; - + // Use trait to for updating forms_tags-table + use Ushahidi_FormsTagsTrait; private $created_id; private $created_ts; @@ -39,6 +40,20 @@ protected function getTable() // ReadRepository public function getEntity(Array $data = null) { + if (!empty($data['id'])) + { + $data['forms'] = $this->getFormsForTag($data['id']); + + if(empty($data['parent_id'])) { + + $data['children'] = + DB::select('id') + ->from('tags') + ->where('parent_id','=',$data['id']) + ->execute($this->db) + ->as_array(); + } + } return new Tag($data); } @@ -51,25 +66,41 @@ protected function getJsonProperties() // SearchRepository public function getSearchFields() { - return ['tag', 'type', 'parent_id', 'q', /* LIKE tag */]; + return ['tag', 'type', 'parent_id', 'q', 'level', 'formId' /* LIKE tag */]; } // Ushahidi_Repository - protected function setSearchConditions(SearchData $search) + protected function setSearchConditions(SearchData $search) + { + $query = $this->search_query; + foreach (['tag', 'type', 'parent_id'] as $key) + { + if ($search->$key) { + $query->where($key, '=', $search->$key); + } + } + if ($search->q) { + // Tag text searching + $query->where('tag', 'LIKE', "%{$search->q}%"); + } + if($search->level) { + //searching for top-level-tags + if($search->level === 'parent') { + $query->where('parent_id', '=', null); + } + } + if($search->formId){ + $query->join('forms_tags') + ->on('tags.id', '=', 'forms_tags.tag_id') + ->where('form_id','=', $search->formId); + } + } + // SearchRepository + public function getSearchResults() { - $query = $this->search_query; - - foreach (['tag', 'type', 'parent_id'] as $key) - { - if ($search->$key) { - $query->where($key, '=', $search->$key); - } - } - - if ($search->q) { - // Tag text searching - $query->where('tag', 'LIKE', "%{$search->q}%"); - } + $query = $this->getSearchQuery(); + $results = $query->distinct(TRUE)->execute($this->db); + return $this->getCollection($results->as_array()); } // CreateRepository @@ -77,9 +108,35 @@ public function create(Entity $entity) { $record = $entity->asArray(); $record['created'] = time(); - return $this->executeInsert($this->removeNullValues($record)); + + unset($record['forms']); + + $id = $this->executeInsert($this->removeNullValues($record)); + + if($entity->forms) { + //updating forms_tags-table + $this->updateTagForms($id, $entity->forms); + } + + return $id; + } + + public function update(Entity $entity) + { + $tag = $entity->getChanged(); + unset($tag['forms']); + + $count = $this->executeUpdate(['id' => $entity->id], $tag); + // updating forms_tags-table + if($entity->hasChanged('forms')) + { + $this->updateTagForms($entity->id, $entity->forms); + } + + return $count; } + // UpdatePostTagRepository public function getByTag($tag) { @@ -103,10 +160,18 @@ public function isSlugAvailable($slug) { return $this->selectCount(compact('slug')) === 0; } + public function delete(Entity $entity) + { + $this->updateFormAttributes($entity->id); + return $this->executeDelete([ + 'id' => $entity->id + ]); + } // DeleteTagRepository public function deleteTag($id) { + $this->updateFormAttributes($entity->id); return $this->delete(compact('id')); } } diff --git a/application/classes/Ushahidi/Signer/Signature.php b/application/classes/Ushahidi/Signer/Signature.php deleted file mode 100644 index 5936c8fc85..0000000000 --- a/application/classes/Ushahidi/Signer/Signature.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @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\Tool\Signer; - -class Ushahidi_Signer_Signature implements Signer -{ - protected $authToken; - - public function __construct($authToken) - { - $this->authToken = $authToken; - } - - public function sign($fullUrl, $data) - { - return computeSignature($fullUrl, $data); - } - - public function computeSignature($url, $data = array()) - { - // sort the array by keys - ksort($data); - // append them to the data string in order - // with no delimiters - foreach ($data as $key => $value) { - $url .= "$key$value"; - } - - // This function calculates the HMAC hash of the data with the key - // passed in - // Note: hash_hmac requires PHP 5 >= 5.1.2 or PECL hash:1.1-1.5 - // Or http://pear.php.net/package/Crypt_HMAC/ - return base64_encode(hash_hmac("sha256", $url, $this->authToken, true)); - } - - public function validate($expectedSignature, $url, $data = array()) - { - return self::compare( - $this->computeSignature($url, $data), - $expectedSignature - ); - } - /** - * Time insensitive compare, function's runtime is governed by the length - * of the first argument, not the difference between the arguments. - * @param $a string First part of the comparison pair - * @param $b string Second part of the comparison pair - * @return bool True if $a == $b, false otherwise. - */ - public static function compare($a, $b) - { - $result = true; - if (strlen($a) != strlen($b)) { - return false; - } - if (!$a && !$b) { - return true; - } - $limit = strlen($a); - for ($i = 0; $i < $limit; ++$i) { - if ($a[$i] != $b[$i]) { - $result = false; - } - } - return $result; - } -} diff --git a/application/classes/Ushahidi/Validator/Form/Attribute/Update.php b/application/classes/Ushahidi/Validator/Form/Attribute/Update.php index 8898612f6b..2cae01966c 100644 --- a/application/classes/Ushahidi/Validator/Form/Attribute/Update.php +++ b/application/classes/Ushahidi/Validator/Form/Attribute/Update.php @@ -53,7 +53,8 @@ protected function getRules() 'relation', 'upload', 'video', - 'markdown' + 'markdown', + 'tags', ]]], ], 'type' => [ @@ -70,7 +71,8 @@ protected function getRules() 'relation', 'media', 'title', - 'description' + 'description', + 'tags', ]]], ], 'required' => [ 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..24cdb97dbe 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 "7" + 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/docs/arch-layers.png b/docs/arch-layers.png new file mode 100644 index 0000000000..c058326c4f Binary files /dev/null and b/docs/arch-layers.png differ diff --git a/docs/architecture.md b/docs/architecture.md index e4f3a293be..13b1dfc693 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -40,24 +40,13 @@ Within the API there are two layers: the delivery and the business logic (core a #### Core Application -Within the core application, we use generally follow the [Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html). The central part of the business logic is defined as use cases and entities. All dependencies flow inwards towards the entities, which have no dependencies. In order to bring user input to the use cases, we pass create simple request data structures to pass from the delivery layer into the use case. The request structure is a simple array and contains all of the inputs for that specific use case. Once the usecase is complete it returns another simple data structure (response) back to the delivery layer for conversion via a Formatter. Data flow within the platform can be visualized as: +Within the core application, we use generally follow the [Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html). The central part of the business logic is defined as use cases and entities. All dependencies flow inwards towards the entities, which have no dependencies. -![api-request-flow](./api-request-flow.png "API Request Flow") -[source](http://www.nomnoml.com/#view/%23title%3A%20General%20API%20request%20flow%0A%0A%5B%3Cstart%3Eapp%5D-%3E%5BKohana%5D%0A%5BKohana%5D-%3E%5BController%5D%0A%5BController%5D-%3E%5B%3Cstate%3Erequest%5D%0A%5B%3Cstate%3Erequest%5D-%3E%5BUsecase%5D%0A%5BUsecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%5BOutputFormatter%5D-%3E%5B%3Cend%3Ejson%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDependencies%7C%0A%20Repository%3B%0A%20Validator%3B%0A%20Authorizer%3B%0A%20etc...%0A%5Do-%3E%5BUsecase%5D%0A%0A%23direction%3A%20right) - -Specific use cases follow 5 high level patterns for Create, Read, Update, Delete and Search (CRUDS) - -![create-usecase](./create-usecase.png "Create Usecase") -[create](http://www.nomnoml.com/#view/%23title%3A%20Create%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BCreate%20Usecase%5D%0A%5BCreate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BCreate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Create%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Create%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BCreate%20Entity%5D%0A%20%20%20%20%20%5BCreate%20Entity%5D-%3E%5BGet%20Created%5D%0A%20%20%20%20%20%5BGet%20Created%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) +![architecture-layers](./arch-layers.png "Software architecture layers") -![read-usecase](./read-usecase.png "Read Usecase") -[read](http://www.nomnoml.com/#view/%23title%3A%20Read%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BRead%20Usecase%5D%0A%5BRead%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BRead%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) +In order to bring user input to the use cases, we pass simple data structures from the delivery layer into the use case. The request structure is a simple array and contains all of the inputs for that specific use case. Once the usecase is complete it returns another simple data structure (response) back to the delivery layer for conversion via a Formatter. Data flow within the platform can be visualized as: -![update-usecase](./update-usecase.png "UpdateUsecase") -[update](http://www.nomnoml.com/#view/%23title%3A%20Update%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BUpdate%20Usecase%5D%0A%5BUpdate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BUpdate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BUpdate%20State%5D%0A%20%20%20%20%20%5BUpdate%20State%5D-%3E%5BVerify%20Update%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Update%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BUpdate%20Entity%5D%0A%20%20%20%20%20%5BUpdate%20Entity%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) - -![delete-usecase](./delete-usecase.png "Delete Usecase") -[delete](http://www.nomnoml.com/#view/%23title%3A%20Delete%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BDelete%20Usecase%5D%0A%5BDelete%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDelete%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Delete%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Delete%20Auth%5D-%3E%5BDelete%20Entity%5D%0A%20%20%20%20%20%5BDelete%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) +![api-request-flow](./api-request-flow.png "API Request Flow") +[source](http://www.nomnoml.com/#view/%23title%3A%20General%20API%20request%20flow%0A%0A%5B%3Cstart%3Eapp%5D-%3E%5BKohana%5D%0A%5BKohana%5D-%3E%5BController%5D%0A%5BController%5D-%3E%5B%3Cstate%3Erequest%5D%0A%5B%3Cstate%3Erequest%5D-%3E%5BUsecase%5D%0A%5BUsecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%5BOutputFormatter%5D-%3E%5B%3Cend%3Ejson%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDependencies%7C%0A%20Repository%3B%0A%20Validator%3B%0A%20Authorizer%3B%0A%20etc...%0A%5Do-%3E%5BUsecase%5D%0A%0A%23direction%3A%20right) -![search-usecase](./search-usecase.png "Search Usecase") -[search](http://www.nomnoml.com/#view/%23title%3A%20Search%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BSearch%20Usecase%5D%0A%5BSearch%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BSearch%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Search%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Search%20Auth%5D-%3E%5BSet%20Search%20Params%5D%0A%20%20%20%20%20%5BSet%20Search%20Params%5D-%3E%5BGet%20Search%20Sesults%5D%0A%20%20%20%20%20%5BGet%20Search%20Sesults%5D-%3E%5BVerify%20Read%20Auth%7C%0A%20%20%20%20%20%20%20%20%5B%3Cstart%3E%20foreach%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5Bcheck%20auth%5D%0A%20%20%20%20%20%20%20%20%5Bcheck%20auth%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5B%3Cend%3E%5D%0A%20%20%20%20%20%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Results%5D%0A%20%20%20%20%20%5BFormat%20Results%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) +See [Use Case Internals](./use-case-internals.md) for more detail diff --git a/docs/create-usecase-collab.png b/docs/create-usecase-collab.png new file mode 100644 index 0000000000..8de17327e4 Binary files /dev/null and b/docs/create-usecase-collab.png differ diff --git a/docs/use-case-internals.md b/docs/use-case-internals.md new file mode 100644 index 0000000000..57b9ff1664 --- /dev/null +++ b/docs/use-case-internals.md @@ -0,0 +1,87 @@ +# Use Case Internals + +## What is a use case? + +Part of Hexagonal Architecture is the concept of the Application Boundary. This boundary separates our application as a whole from everything else (both framework and communication with the outside world). + +> A Use Case (sometimes called a Command) is an explicitly defined way in which an application can be used. + +We define how the outside world can communicate with our application by creating "Use Cases". These essentially are classes which name actions that can be taken. For example, our CreatePostUsecase defines that our application can create a post. + +Defining Use Cases has some useful side-affects. For example, we clearly and explicitly can see how our application "wants" to be interacted with. We can plan use cases ahead of time, or add them as needed, but use cases should capture the operations which can happen within our application. + +> Aside: Platform uses some generic CRUDS usecases. These aren't tied to a specific Domain Model (Entity) ie. a Post but rather have the entity and repo injected into them. This makes Use Cases significantly less well defined. A developer can no longer glance at the Use Case directory and see what actions are available. This might be something we can improve in future + +## Anatomy of a Use Case (in platform) + +Use Cases in platform all follow a high level interface. In short they all have a `interact()` method. + +``` +interface Usecase +{ + /** + * @return Array + */ + public function interact(); +} +``` + +To enable building of some generic use cases they also have `isSearch` and `isWrite` methods. + +``` +interface Usecase +{ + /** + * Will this usecase write any data? + * + * @return Boolean + */ + public function isWrite(); + + /** + * Will this usecase search for data? + * + * @return Boolean + */ + public function isSearch(); + + /** + * @return Array + */ + public function interact(); +} +``` + +The actual parameters for each UseCase are injected through setter methods, commonly: `setPayload()`, `setIdentifiers` and `setFilters`. + +## CRUDS use cases + +Most of our use cases follow 5 high level patterns for Create, Read, Update, Delete and Search (CRUDS) + +### Create + +![create-usecase](./create-usecase.png "Create Usecase") +[create](http://www.nomnoml.com/#view/%23title%3A%20Create%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BCreate%20Usecase%5D%0A%5BCreate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BCreate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Create%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Create%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BCreate%20Entity%5D%0A%20%20%20%20%20%5BCreate%20Entity%5D-%3E%5BGet%20Created%5D%0A%20%20%20%20%20%5BGet%20Created%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) + +![create-usecase-collab](./create-usecase-collab.png "Create Usecase Collaborators") +[collaborators](http://www.nomnoml.com/#view/%23title%3A%20Create%20UseCase%20Collaborators%0A%0A%5BCreateUsecase%7C%7C%0Ainteract()%0AsetPayload()%5D%0A%0A%5BValidator%5D%3C-%2B%5BCreateUsecase%5D%0A%5BAuthorizer%5D%3C-%2B%5BCreateUsecase%5D%0A%5BFormatter%5D%3C-%2B%5BCreateUsecase%5D%0A%5BRepository%5D%3C-%2B%5BCreateUsecase%5D%0A%0A%5BCreateUsecase%5D-%3E%5B%3Cinput%3E%20payload%5D%0A%0A%23direction%3A%20right) + +### Read + +![read-usecase](./read-usecase.png "Read Usecase") +[read](http://www.nomnoml.com/#view/%23title%3A%20Read%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BRead%20Usecase%5D%0A%5BRead%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BRead%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) + +### Update + +![update-usecase](./update-usecase.png "UpdateUsecase") +[update](http://www.nomnoml.com/#view/%23title%3A%20Update%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BUpdate%20Usecase%5D%0A%5BUpdate%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BUpdate%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BUpdate%20State%5D%0A%20%20%20%20%20%5BUpdate%20State%5D-%3E%5BVerify%20Update%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Update%20Auth%5D-%3E%5BVerify%20Valid%5D%0A%20%20%20%20%20%5BVerify%20Valid%5D-%3E%5BUpdate%20Entity%5D%0A%20%20%20%20%20%5BUpdate%20Entity%5D-%3E%5B%3Cchoice%3E%20Can%20Read%3F%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%20%20%20%20%20%5B%3Cchoice%3E%20Can%20Read%3F%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) + +### Delete + +![delete-usecase](./delete-usecase.png "Delete Usecase") +[delete](http://www.nomnoml.com/#view/%23title%3A%20Delete%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BDelete%20Usecase%5D%0A%5BDelete%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BDelete%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Delete%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Delete%20Auth%5D-%3E%5BDelete%20Entity%5D%0A%20%20%20%20%20%5BDelete%20Entity%5D-%3E%5BVerify%20Read%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Entity%5D%0A%20%20%20%20%20%5BFormat%20Entity%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) + +### Search + +![search-usecase](./search-usecase.png "Search Usecase") +[search](http://www.nomnoml.com/#view/%23title%3A%20Search%20UseCase%0A%5B%3Cstate%3Erequest%5D-%3E%5BSearch%20Usecase%5D%0A%5BSearch%20Usecase%5D-%3E%5B%3Cstate%3Eresponse%5D%0A%5B%3Cstate%3Eresponse%5D-%3E%5BOutputFormatter%5D%0A%0A%5B%3Cstate%3Erequest%7C%0Apayload%3B%0Aidentifier%3B%0Afilters%5D%0A%0A%5BSearch%20Usecase%7C%0A%20%20%20%20%20%5B%3Cstart%3E%20interact()%5D-%3E%5BGet%20Entity%5D%0A%20%20%20%20%20%5BGet%20Entity%5D-%3E%5BVerify%20Search%20Auth%5D%0A%20%20%20%20%20%5BVerify%20Search%20Auth%5D-%3E%5BSet%20Search%20Params%5D%0A%20%20%20%20%20%5BSet%20Search%20Params%5D-%3E%5BGet%20Search%20Sesults%5D%0A%20%20%20%20%20%5BGet%20Search%20Sesults%5D-%3E%5BVerify%20Read%20Auth%7C%0A%20%20%20%20%20%20%20%20%5B%3Cstart%3E%20foreach%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5Bcheck%20auth%5D%0A%20%20%20%20%20%20%20%20%5Bcheck%20auth%5D-%3E%5B%3Cchoice%3Ewhile%20results%3F%5D%0A%20%20%20%20%20%20%20%20%5B%3Cchoice%3Ewhile%20results%3F%5D-%3E%5B%3Cend%3E%5D%0A%20%20%20%20%20%5D%0A%20%20%20%20%20%5BVerify%20Read%20Auth%5D-%3E%5BFormat%20Results%5D%0A%20%20%20%20%20%5BFormat%20Results%5D-%3E%5B%3Cend%3E%20return%5D%0A%5D%0A%0A%23direction%3A%20right) diff --git a/migrations/20170315231639_add_post_markdown_table.php b/migrations/20170315231639_add_post_markdown_table.php index 8e28a2b4ff..7710fd0a5f 100644 --- a/migrations/20170315231639_add_post_markdown_table.php +++ b/migrations/20170315231639_add_post_markdown_table.php @@ -12,7 +12,7 @@ public function up() $this->table('post_markdown') ->addColumn('post_id', 'integer') ->addColumn('form_attribute_id', 'integer') - ->addColumn('value', 'string', ['null' => true]) + ->addColumn('value', 'text', ['null' => true]) ->addColumn('created', 'integer', ['default' => 0]) ->addColumn('updated', 'integer', ['null' => true]) ->addForeignKey('form_attribute_id', 'form_attributes', 'id', [ 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/20170325110408_add_language_to_user.php b/migrations/20170325110408_add_language_to_user.php new file mode 100644 index 0000000000..7a05ad3d34 --- /dev/null +++ b/migrations/20170325110408_add_language_to_user.php @@ -0,0 +1,28 @@ +table('users') + ->addColumn('language', 'string', [ + 'null' => true, + ]) + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->table('users') + ->removeColumn('language') + ->update(); + } +} diff --git a/migrations/20170328080656_add_forms_tags_table.php b/migrations/20170328080656_add_forms_tags_table.php new file mode 100644 index 0000000000..97a3545934 --- /dev/null +++ b/migrations/20170328080656_add_forms_tags_table.php @@ -0,0 +1,52 @@ +table('forms_tags', [ + 'id' => false, + 'primary_key' => ['form_id', 'tag_id'], + ]) + ->addColumn('form_id', 'integer') + ->addForeignKey('form_id', 'forms', 'id', [ + 'delete' => 'CASCADE', + 'update' => 'CASCADE' + ]) + ->addColumn('tag_id', 'integer') + ->addForeignKey('tag_id', 'tags', 'id', [ + 'delete' => 'CASCADE', + 'update' => 'CASCADE' + ]) + ->create(); + + } + + /** + * Migrate Up. + */ + public function up() + { + + + } + + /** + * Migrate Down. + */ + public function down() + { + + + + } +} 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/migrations/20170412191954_convert_form_tags_to_form_attribute.php b/migrations/20170412191954_convert_form_tags_to_form_attribute.php new file mode 100644 index 0000000000..7742b73197 --- /dev/null +++ b/migrations/20170412191954_convert_form_tags_to_form_attribute.php @@ -0,0 +1,59 @@ +getAdapter()->getConnection(); + $rows = $this->fetchAll( + "SELECT id + FROM form_stages" + ); + $tag_rows = $this->fetchAll( + "SELECT id + FROM tags" + ); + $insert = $pdo->prepare( + "INSERT into form_attributes + (`label`,`type`, `required`, `priority`, `cardinality`, `input`, `options`, `key`, `form_stage_id`) + VALUES + ('Categories', 'varchar', 0, 3, 0, 'tags', :tags, :key, :form_stage_id)" + ); + + $tags = []; + + foreach ($tag_rows as $tag_row) { + array_push($tags, (int)$tag_row['id']); + } + $tags = json_encode($tags); + + foreach ($rows as $row) { + $uuid = Uuid::uuid4(); + $key = $uuid->toString(); + $insert->execute( + [ + ':tags' => $tags, + ':key' => $key, + ':form_stage_id' => $row['id'] + ] + ); + } + } + + /** + * Migrate Down. + */ + public function down() + { + $this->execute("DELETE from form_attributes where input = 'tags'"); + + } +} diff --git a/migrations/20170417082819_add_tags_to_forms_tags.php b/migrations/20170417082819_add_tags_to_forms_tags.php new file mode 100644 index 0000000000..2fed17221d --- /dev/null +++ b/migrations/20170417082819_add_tags_to_forms_tags.php @@ -0,0 +1,49 @@ +getAdapter()->getConnection(); + + $forms = $this->fetchAll( + "SELECT id + FROM forms" + ); + $tags = $this->fetchAll( + "SELECT id + FROM tags" + ); + + $insert = $pdo->prepare( + "INSERT into + forms_tags + (`tag_id`, `form_id`) + VALUES(:tag_id, :form_id) + " + ); + + foreach ($forms as $form) { + foreach ($tags as $tag) { + $insert->execute( + [':tag_id' => $tag['id'], + ':form_id' => $form['id'] + ] + ); + } + } + } + + /** + * Migrate Down. + */ + public function down() + { + $this->execute("DELETE from forms_tags"); + } +} diff --git a/migrations/20170417090621_move_post_tag_values_to_post_varchar.php b/migrations/20170417090621_move_post_tag_values_to_post_varchar.php new file mode 100644 index 0000000000..fed076d255 --- /dev/null +++ b/migrations/20170417090621_move_post_tag_values_to_post_varchar.php @@ -0,0 +1,89 @@ +getAdapter()->getConnection(); + // fetching posts with form_attribute_id + $posts = $this->fetchAll( + "SELECT posts.id, posts.form_id, form_attributes.id as form_attribute_id + FROM posts + INNER JOIN form_stages + ON form_stages.form_id = posts.form_id + INNER JOIN form_attributes + ON form_attributes.form_stage_id = form_stages.id" + ); + $insert = $pdo->prepare( + "INSERT into + post_varchar + (`post_id`, `form_attribute_id`, `value`, `created`) + VALUES(:post_id, :form_attribute_id, :value, :created)" + ); + foreach ($posts as $post) { + $post_tags = $pdo->prepare( + "SELECT tag_id + FROM posts_tags + WHERE post_id = :post_id" + ); + $post_tags->execute([':post_id' => $post['id']]); + // inserting post_ids and tag_ids(value) in post_varchar + $tags = $post_tags->fetchAll(); + foreach ($tags as $tag) { + $insert->execute( + [ + ':post_id' => $post['id'], + ':form_attribute_id' => $post['form_attribute_id'], + ':value' => $tag['tag_id'], + ':created' => time() + ] + ); + } + } + } + + /** + * Migrate Down. + */ + public function down() + { + $pdo = $this->getAdapter()->getConnection(); + $posts = $this->fetchAll( + "SELECT posts.id, posts.form_id, form_attributes.id as form_attribute_id + FROM posts + INNER JOIN form_stages + ON form_stages.form_id = posts.form_id + INNER JOIN form_attributes + ON form_attributes.form_stage_id = form_stages.id" + ); + $delete = $pdo->prepare( + "DELETE from post_varchar + WHERE post_id = :post_id AND form_attribute_id = :form_attribute_id AND value = :value" + ); + + foreach ($posts as $post) { + $post_tags = $pdo->prepare( + "SELECT tag_id + FROM posts_tags + WHERE post_id = :post_id" + ); + $post_tags->execute([':post_id' => $post['id']]); + // inserting post_ids and tag_ids(value) in post_varchar + $tags = $post_tags->fetchAll(); + foreach ($tags as $tag) { + $delete->execute( + [ + ':post_id' => $post['id'], + ':form_attribute_id' => $post['form_attribute_id'], + ':value' => $tag['tag_id'] + ] + ); + } + } + } +} diff --git a/application/classes/Ushahidi/Acl.php b/src/App/Acl.php similarity index 83% rename from application/classes/Ushahidi/Acl.php rename to src/App/Acl.php index af2e03ab8b..c5b0e609ca 100644 --- a/application/classes/Ushahidi/Acl.php +++ b/src/App/Acl.php @@ -1,4 +1,4 @@ -role) { return false; } - + $role = $this->role_repo->getByName($user->role); // Does the user have the permission? diff --git a/application/classes/Ushahidi/Authenticator/Password.php b/src/App/Authenticator/Password.php similarity index 81% rename from application/classes/Ushahidi/Authenticator/Password.php rename to src/App/Authenticator/Password.php index 37f7f10a19..796e6d2641 100644 --- a/application/classes/Ushahidi/Authenticator/Password.php +++ b/src/App/Authenticator/Password.php @@ -1,4 +1,4 @@ -fetchOne()); - $reader->addFilter(function($row) use ($nbColumns) { + $reader->addFilter(function ($row) use ($nbColumns) { return count($row) == $nbColumns; }); @@ -51,6 +53,6 @@ public function process($file) $reader->setLimit($this->limit); } - return new ArrayIterator($reader->fetchAssoc()); + return new \ArrayIterator($reader->fetchAssoc()); } } diff --git a/application/classes/Ushahidi/CSVReaderFactory.php b/src/App/FileReader/CSVReaderFactory.php similarity index 64% rename from application/classes/Ushahidi/CSVReaderFactory.php rename to src/App/FileReader/CSVReaderFactory.php index b38e329d5a..44ae1cca52 100644 --- a/application/classes/Ushahidi/CSVReaderFactory.php +++ b/src/App/FileReader/CSVReaderFactory.php @@ -1,4 +1,4 @@ -config['media_upload_dir']); + return new LocalAdapter($this->config['media_upload_dir']); } } - diff --git a/application/classes/Ushahidi/FilesystemAdapter/Rackspace.php b/src/App/FilesystemAdapter/Rackspace.php similarity index 80% rename from application/classes/Ushahidi/FilesystemAdapter/Rackspace.php rename to src/App/FilesystemAdapter/Rackspace.php index 446dd91387..6bd8f775a4 100644 --- a/application/classes/Ushahidi/FilesystemAdapter/Rackspace.php +++ b/src/App/FilesystemAdapter/Rackspace.php @@ -1,4 +1,4 @@ - $this->config['apiKey'], )); - $store = $client->objectStoreService(null,$this->config['region']); + $store = $client->objectStoreService(null, $this->config['region']); $container = $store->getContainer($this->config['container']); return new Adapter($container); } } - diff --git a/application/classes/Ushahidi/Hasher/Password.php b/src/App/Hasher/Password.php similarity index 78% rename from application/classes/Ushahidi/Hasher/Password.php rename to src/App/Hasher/Password.php index f485ef80a8..ead74656e9 100644 --- a/application/classes/Ushahidi/Hasher/Password.php +++ b/src/App/Hasher/Password.php @@ -1,4 +1,4 @@ -set('tool.acl', $di->lazyNew('Ushahidi\App\Acl')); +$di->setter['Ushahidi\App\Acl']['setRoleRepo'] = $di->lazyGet('repository.role'); + +$di->set('tool.hasher.password', $di->lazyNew('Ushahidi\App\Hasher\Password')); +$di->set('tool.authenticator.password', $di->lazyNew('Ushahidi\App\Authenticator\Password')); + +$di->set('filereader.csv', $di->lazyNew('Ushahidi\App\FileReader\CSV')); +$di->setter['Ushahidi\App\FileReader\CSV']['setReaderFactory'] = + $di->lazyGet('csv.reader_factory'); + +$di->set('csv.reader_factory', $di->lazyNew('Ushahidi\App\FileReader\CSVReaderFactory')); + +// Register filesystem adapter types +// Currently supported: Local filesysten, AWS S3 v3, Rackspace +// the naming scheme must match the cdn type set in config/cdn +$di->set('adapter.local', $di->lazyNew( + 'Ushahidi\App\FilesystemAdapter\Local', + ['config' => $di->lazyGet('cdn.config')] +)); + +$di->set('adapter.aws', $di->lazyNew( + 'Ushahidi\App\FilesystemAdapter\AWS', + ['config' => $di->lazyGet('cdn.config')] +)); + +$di->set('adapter.rackspace', $di->lazyNew( + 'Ushahidi\App\FilesystemAdapter\Rackspace', + ['config' => $di->lazyGet('cdn.config')] +)); + +// Media Filesystem +// The Ushahidi filesystem adapter returns a flysystem adapter for a given +// cdn type based on the provided configuration +$di->set('tool.filesystem', $di->lazyNew('Ushahidi\App\Filesystem')); +$di->params['Ushahidi\App\Filesystem'] = [ + 'adapter' => $di->lazy(function () use ($di) { + $adapter_type = $di->get('cdn.config'); + $fsa = $di->get('adapter.' . $adapter_type['type']); + + return $fsa->getAdapter(); + }) +]; + +// Defined memcached +$di->set('memcached', $di->lazy(function () use ($di) { + $config = $di->get('ratelimiter.config'); + + $memcached = new Memcached(); + $memcached->addServer($config['memcached']['host'], $config['memcached']['port']); + + return $memcached; +})); + +// Set up login rate limiter +$di->set('ratelimiter.login.flap', $di->lazyNew('BehEh\Flaps\Flap')); + +$di->params['BehEh\Flaps\Flap'] = [ + 'storage' => $di->lazyNew('BehEh\Flaps\Storage\DoctrineCacheAdapter'), + 'name' => 'login' +]; + +$di->set('ratelimiter.login.strategy', $di->lazyNew('BehEh\Flaps\Throttling\LeakyBucketStrategy')); + +// 3 requests every 1 minute by default +$di->params['BehEh\Flaps\Throttling\LeakyBucketStrategy'] = [ + 'requests' => 3, + 'timeSpan' => '1m' +]; + +$di->set('ratelimiter.login', $di->lazyNew('Ushahidi\App\RateLimiter')); + +$di->params['Ushahidi\App\RateLimiter'] = [ + 'flap' => $di->lazyGet('ratelimiter.login.flap'), + 'throttlingStrategy' => $di->lazyGet('ratelimiter.login.strategy'), +]; + +$di->params['BehEh\Flaps\Storage\DoctrineCacheAdapter'] = [ + 'cache' => $di->lazyGet('ratelimiter.cache') +]; + +// Rate limit storage cache +$di->set('ratelimiter.cache', function () use ($di) { + $config = $di->get('ratelimiter.config'); + $cache = $config['cache']; + + if ($cache === 'memcached') { + $di->setter['Doctrine\Common\Cache\MemcachedCache']['setMemcached'] = + $di->lazyGet('memcached'); + + return $di->newInstance('Doctrine\Common\Cache\MemcachedCache'); + } elseif ($cache === 'filesystem') { + $di->params['Doctrine\Common\Cache\FilesystemCache'] = [ + 'directory' => $config['filesystem']['directory'], + ]; + + return $di->newInstance('Doctrine\Common\Cache\FilesystemCache'); + } + + // Fall back to using in-memory cache if none is configured + return $di->newInstance('Doctrine\Common\Cache\ArrayCache'); +}); + +// Rate limiter violation handler +$di->setter['BehEh\Flaps\Flap']['setViolationHandler'] = + $di->lazyNew('Ushahidi\App\ThrottlingViolationHandler'); diff --git a/application/classes/Ushahidi/RateLimiter.php b/src/App/RateLimiter.php similarity index 85% rename from application/classes/Ushahidi/RateLimiter.php rename to src/App/RateLimiter.php index 02dda4b0e6..98da9f689c 100644 --- a/application/classes/Ushahidi/RateLimiter.php +++ b/src/App/RateLimiter.php @@ -1,4 +1,4 @@ - 'bool', 'created' => 'int', 'updated' => 'int', + 'hide_author' => 'bool', 'require_approval' => 'bool', 'everyone_can_create' => 'bool', 'can_create' => 'array', + 'tags' => '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/Entity/Tag.php b/src/Core/Entity/Tag.php index 5c0f1554c9..50fc758853 100644 --- a/src/Core/Entity/Tag.php +++ b/src/Core/Entity/Tag.php @@ -26,6 +26,8 @@ class Tag extends StaticEntity protected $priority; protected $created; protected $role; + protected $forms; + protected $children; // StatefulData protected function getDerived() @@ -55,6 +57,8 @@ protected function getDefinition() 'priority' => 'int', 'created' => 'int', 'role' => '*json', + 'forms' => 'array', + 'children' =>'array', ]; } diff --git a/src/Core/Entity/User.php b/src/Core/Entity/User.php index 74d4716301..ec0e3a745d 100644 --- a/src/Core/Entity/User.php +++ b/src/Core/Entity/User.php @@ -26,6 +26,7 @@ class User extends StaticEntity protected $created; protected $updated; protected $role; + protected $language; // DataTransformer protected function getDefinition() @@ -42,6 +43,7 @@ protected function getDefinition() 'created' => 'int', 'updated' => 'int', 'role' => 'string', + 'language' => 'string', ]; } diff --git a/src/Core/Traits/PostValueRestrictions.php b/src/Core/Traits/PostValueRestrictions.php new file mode 100644 index 0000000000..a4d71bdcb2 --- /dev/null +++ b/src/Core/Traits/PostValueRestrictions.php @@ -0,0 +1,55 @@ + + * @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, $user) + { + + if ($post->form_id) { + + if ($this->canUserEditForm($post->form_id, $user)) { + return true; + } + + return !$form_repo->isAuthorHidden($post->form_id); + } + + return true; + } + + + /** + * Test whether the post instance requires value restriction + * @param Post $post + * @return Boolean + */ + public function canUserReadPostsValues(Post $post, $user) + { + return $this->canUserEditForm($post->form_id, $user); + } + + /* FormRole */ + protected function canUserEditForm($form_id, $user) + { + return $this->isUserAdmin($user) || $this->hasPermission($user, $this->getPermission()); + } +} 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 diff --git a/src/Init.php b/src/Init.php index 5711b52e30..06b8047ca4 100644 --- a/src/Init.php +++ b/src/Init.php @@ -347,6 +347,7 @@ function feature($name) $di->setter['Ushahidi\Core\Traits\DataImportAccess']['setEnabled'] = $di->lazyGet('data-import.enabled'); // Tools +$di->set('tool.signer', $di->lazyNew('Ushahidi\Core\Tool\Signer')); $di->set('tool.uploader', $di->lazyNew('Ushahidi\Core\Tool\Uploader')); $di->params['Ushahidi\Core\Tool\Uploader'] = [ 'fs' => $di->lazyGet('tool.filesystem'), @@ -396,3 +397,5 @@ function feature($name) ]; $di->set('authorizer.console', $di->lazyNew('Ushahidi\Console\Authorizer\ConsoleAuthorizer')); + +require __DIR__ . '/App/Init.php';