diff --git a/database/migrations/phinx/20231016120259_add_user_id_to_tags_table.php b/database/migrations/phinx/20231016120259_add_user_id_to_tags_table.php new file mode 100644 index 0000000000..e5e08d1622 --- /dev/null +++ b/database/migrations/phinx/20231016120259_add_user_id_to_tags_table.php @@ -0,0 +1,41 @@ +table('tags') + ->addColumn('user_id', 'integer', [ + 'after' => 'parent_id', + 'null' => true, + ]) + ->update(); + } +} diff --git a/src/Ushahidi/Core/Concerns/FilterRecords.php b/src/Ushahidi/Core/Concerns/FilterRecords.php index 648bf27b45..a07bb8139c 100644 --- a/src/Ushahidi/Core/Concerns/FilterRecords.php +++ b/src/Ushahidi/Core/Concerns/FilterRecords.php @@ -31,7 +31,7 @@ trait FilterRecords * 'orderby' => 'username', * ]); * - * @param Array $filters + * @param array $filters * @return $this */ public function setFilters(array $filters) @@ -45,8 +45,8 @@ public function setFilters(array $filters) * * $this->setFilter('role', 'admin'); * - * @param String $name - * @param Mixed $value + * @param string $name + * @param mixed $value * @return $this */ public function setFilter($name, $value) @@ -64,9 +64,9 @@ public function setFilter($name, $value) * * NOTE: Defaults cannot be provided when using this method! * - * @param Array $allowed allowed parameters - * @param Array $force force all parameters to be defined - * @return Array + * @param array $allowed allowed parameters + * @param array $force force all parameters to be defined + * @return array */ public function getFilters(array $allowed, $force = false) { diff --git a/src/Ushahidi/Core/Concerns/OwnerAccess.php b/src/Ushahidi/Core/Concerns/OwnerAccess.php index 4922b0b14f..1d803890f4 100644 --- a/src/Ushahidi/Core/Concerns/OwnerAccess.php +++ b/src/Ushahidi/Core/Concerns/OwnerAccess.php @@ -25,7 +25,7 @@ trait OwnerAccess * * @return boolean */ - protected function isUserOwner(Entity $ownable, Entity $user) + public function isUserOwner(Entity $ownable, Entity $user) { // @todo ensure we always check the original user_id not the updated value! return ($user->getId() && $ownable->user_id === $user->getId()); @@ -36,7 +36,7 @@ protected function isUserOwner(Entity $ownable, Entity $user) * * @return boolean */ - protected function isUserAndOwnerAnonymous(Entity $ownable, Entity $user) + public function isUserAndOwnerAnonymous(Entity $ownable, Entity $user) { // @todo ensure we always check the original user_id not the updated value! return (! $user->getId() && ! $ownable->user_id); diff --git a/src/Ushahidi/Core/Concerns/PrivAccess.php b/src/Ushahidi/Core/Concerns/PrivAccess.php index 6221d03c9f..fbeb7c6043 100644 --- a/src/Ushahidi/Core/Concerns/PrivAccess.php +++ b/src/Ushahidi/Core/Concerns/PrivAccess.php @@ -20,7 +20,8 @@ trait PrivAccess /** * Get a list of all possible privilges. * By default, returns standard HTTP REST methods. - * @return Array + * + * @return array */ protected function getAllPrivs() { diff --git a/src/Ushahidi/Core/Tool/Acl.php b/src/Ushahidi/Core/Tool/Acl.php index 800520ac82..23d7038b6a 100644 --- a/src/Ushahidi/Core/Tool/Acl.php +++ b/src/Ushahidi/Core/Tool/Acl.php @@ -57,10 +57,14 @@ public function hasPermission(Entity $user, $permission) protected function customRoleHasPermission(Entity $user, $permission) { $role = $this->role_repo->getByName($user->role); - $permissions = array_map('strtolower', $role->permissions); - - // Does the user have the permission? - return in_array(strtolower($permission), $permissions); + if (isset($role->permissions) && is_array($role->permissions)) { + $permissions = array_map('strtolower', $role->permissions); + + // Does the user have the permission? + return in_array(strtolower($permission), $permissions); + } + + return false; } protected function defaultHasPermission(Entity $user, $permission) diff --git a/src/Ushahidi/Core/Tool/Authorizer/SetAuthorizer.php b/src/Ushahidi/Core/Tool/Authorizer/SetAuthorizer.php index 10e475b489..9d5e30855e 100644 --- a/src/Ushahidi/Core/Tool/Authorizer/SetAuthorizer.php +++ b/src/Ushahidi/Core/Tool/Authorizer/SetAuthorizer.php @@ -44,10 +44,10 @@ class SetAuthorizer implements Authorizer // if roles are available for this deployment. use AccessControlList; - protected function isVisibleToUser(Set $entity, $user) + protected function isVisibleToUser(Set $set, $user) { - if ($entity->role) { - return in_array($user->role, $entity->role); + if ($set->role) { + return in_array($user->role, $set->role); } // If no roles are selected, the Set is considered completely public. @@ -57,6 +57,11 @@ protected function isVisibleToUser(Set $entity, $user) /* Authorizer */ public function isAllowed(Entity $entity, $privilege) { + // Firstly, all users can search sets + if ($privilege === 'search') { + return true; + } + // These checks are run within the user context. $user = $this->getUser(); @@ -65,27 +70,34 @@ public function isAllowed(Entity $entity, $privilege) return false; } - // First check whether there is a role with the right permissions - if ($this->acl->hasPermission($user, Permission::MANAGE_SETS)) { + // We check if a user has the 'admin' role. If they do they're + // allowed access to everything (all entities and all privileges) + $is_admin = $this->isUserAdmin($user); + if ($is_admin) { return true; } - // Then we check if a user has the 'admin' role. If they do they're - // allowed access to everything (all entities and all privileges) - if ($this->isUserAdmin($user)) { + // We check whether there is a role with the right permissions + if ($this->acl->hasPermission($user, Permission::MANAGE_SETS)) { return true; } // Non-admin users are not allowed to make sets featured - if (in_array($privilege, ['create', 'update']) and $entity->hasChanged('featured')) { + if (!$is_admin && $entity->hasChanged('featured') && in_array($privilege, ['create', 'update'])) { return false; } + $isUserOwner = $this->isUserOwner($entity, $user); // If the user is the owner of this set, they can do anything - if ($this->isUserOwner($entity, $user)) { + if ($isUserOwner) { return true; } + // TODO: We want to check if the set entity is available only to owner + // if (!$isUserOwner && $entity->view_options['only_me'] == true) { + // return false; + // } + // Check if the Set is only visible to specific roles. if ($this->isVisibleToUser($entity, $user) and $privilege === 'read') { return true; @@ -96,11 +108,6 @@ public function isAllowed(Entity $entity, $privilege) return true; } - // Finally, all users can search sets - if ($privilege === 'search') { - return true; - } - // If no other access checks succeed, we default to denying access return false; } diff --git a/src/Ushahidi/DataSource/Email/Email.php b/src/Ushahidi/DataSource/Email/Email.php index cc23cace7d..a3e7a6fd6f 100644 --- a/src/Ushahidi/DataSource/Email/Email.php +++ b/src/Ushahidi/DataSource/Email/Email.php @@ -159,9 +159,13 @@ public function fetch($limit = false) // Encryption type $encryption = (strcasecmp($encryption, 'none') != 0) ? '/'.$encryption : ''; + // To connect to an SSL IMAP or POP3 server with a self-signed certificate, + // add /novalidate-cert after the encryption protocol specification: + $no_cert_validation = !empty($encryption) ? '/novalidate-cert' : ''; + try { // Try to connect - $inbox = '{'.$server.':'.$port.'/'.$type.$encryption.'}INBOX'; + $inbox = '{'.$server.':'.$port.'/'.$type.$encryption.$no_cert_validation.'}INBOX'; $connection = @imap_open($inbox, $username, $password, 0, 1); $errors = imap_errors(); diff --git a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchAllCategoriesQueryHandler.php b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchAllCategoriesQueryHandler.php index 2c265e600a..d1a19fb5f3 100644 --- a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchAllCategoriesQueryHandler.php +++ b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchAllCategoriesQueryHandler.php @@ -5,8 +5,10 @@ use App\Bus\Action; use App\Bus\Query\AbstractQueryHandler; use App\Bus\Query\Query; +use Ushahidi\Core\Tool\SearchData; use Ushahidi\Modules\V5\Actions\Category\Queries\FetchAllCategoriesQuery; use Ushahidi\Modules\V5\Repository\Category\CategoryRepository; +use Illuminate\Support\Facades\Auth; class FetchAllCategoriesQueryHandler extends AbstractQueryHandler { @@ -29,6 +31,24 @@ public function __invoke(Action $action) * @var FetchAllCategoriesQuery $action */ $this->isSupported($action); - return $this->categoryRepository->fetchAll($action->getPaging(), $action->getCategorySearchFields()); + + $data = new SearchData; + + $searchFields = $action->getCategorySearchFields(); + + $user = Auth::guard()->user(); + + $data->setFilter('keyword', $searchFields->q()); + + $data->setFilter('tag', $searchFields->tag()); + $data->setFilter('type', $searchFields->type()); + $data->setFilter('role', $searchFields->role()); + $data->setFilter('user_id', $user->id ?? null); + $data->setFilter('parent_id', $searchFields->parentId()); + $data->setFilter('is_parent', $searchFields->level() === 'parent'); + $data->setFilter('is_admin', $searchFields->role() && $searchFields->role() == "admin"); + + $this->categoryRepository->setSearchParams($data); + return $this->categoryRepository->fetchAll($action->getPaging()); } } diff --git a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchCategoryByIdQueryHandler.php b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchCategoryByIdQueryHandler.php index fed7710d5d..a9b0ab3c20 100644 --- a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchCategoryByIdQueryHandler.php +++ b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/FetchCategoryByIdQueryHandler.php @@ -6,9 +6,11 @@ use App\Bus\Action; use App\Bus\Query\AbstractQueryHandler; use App\Bus\Query\Query; +use Ushahidi\Core\Tool\SearchData; use Ushahidi\Modules\V5\Actions\Category\Queries\FetchCategoryByIdQuery; use Ushahidi\Modules\V5\Models\Category; use Ushahidi\Modules\V5\Repository\Category\CategoryRepository; +use Illuminate\Support\Facades\Auth; class FetchCategoryByIdQueryHandler extends AbstractQueryHandler { diff --git a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/StoreCategoryCommandHandler.php b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/StoreCategoryCommandHandler.php index bc8f276d9f..4f73518296 100644 --- a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/StoreCategoryCommandHandler.php +++ b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/StoreCategoryCommandHandler.php @@ -8,6 +8,7 @@ use Ushahidi\Modules\V5\Actions\Category\Commands\StoreCategoryCommand; use Ushahidi\Modules\V5\Models\Category; use Ushahidi\Modules\V5\Repository\Category\CategoryRepository; +use Illuminate\Support\Facades\Auth; class StoreCategoryCommandHandler extends AbstractCommandHandler { @@ -43,8 +44,11 @@ public function __invoke(Action $action): int $parentId = null; } + $user_id = Auth::guard()->user()->id ?? null; + return $this->categoryRepository->store( $parentId, + $user_id, ucfirst($action->getTag()), $slug, $action->getType(), diff --git a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/UpdateCategoryCommandHandler.php b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/UpdateCategoryCommandHandler.php index 0afb54c55a..a6add7c1b3 100644 --- a/src/Ushahidi/Modules/V5/Actions/Category/Handlers/UpdateCategoryCommandHandler.php +++ b/src/Ushahidi/Modules/V5/Actions/Category/Handlers/UpdateCategoryCommandHandler.php @@ -9,6 +9,7 @@ use Ushahidi\Modules\V5\Models\Category; use Ushahidi\Modules\V5\Repository\Category\CategoryRepository; use Ushahidi\Modules\V5\Actions\Category\Commands\UpdateCategoryCommand; +use Illuminate\Support\Facades\Auth; class UpdateCategoryCommandHandler extends AbstractCommandHandler { @@ -31,9 +32,12 @@ public function __invoke(Action $action): Category */ $this->isSupported($action); + $user_id = Auth::guard()->user()->id ?? null; + $this->categoryRepository->update( $action->getCategoryId(), $action->getParentId(), + $user_id, $action->getTag(), $action->getSlug(), $action->getType(), diff --git a/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/CreateCollectionPostCommandHandler.php b/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/CreateCollectionPostCommandHandler.php index 6e21ce0956..a32a7f9d65 100644 --- a/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/CreateCollectionPostCommandHandler.php +++ b/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/CreateCollectionPostCommandHandler.php @@ -34,6 +34,6 @@ protected function isSupported(Command $command) public function __invoke($action) //: int { $this->isSupported($action); - $this->collection_post_repository->Create($action->getCollectionId(), $action->getPostId()); + $this->collection_post_repository->create($action->getCollectionId(), $action->getPostId()); } } diff --git a/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/FetchCollectionQueryHandler.php b/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/FetchCollectionQueryHandler.php index e9e162841b..df7a84db54 100644 --- a/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/FetchCollectionQueryHandler.php +++ b/src/Ushahidi/Modules/V5/Actions/Collection/Handlers/FetchCollectionQueryHandler.php @@ -4,16 +4,29 @@ use App\Bus\Query\AbstractQueryHandler; use App\Bus\Query\Query; +use Ushahidi\Core\Tool\Authorizer\SetAuthorizer; use Ushahidi\Modules\V5\Actions\Collection\Queries\FetchCollectionQuery; use Ushahidi\Modules\V5\Repository\Set\SetRepository as CollectionRepository; +use Ushahidi\Core\Tool\SearchData; +use Illuminate\Support\Facades\Auth; + +use function JmesPath\search; class FetchCollectionQueryHandler extends AbstractQueryHandler { private $collection_repository; - public function __construct(CollectionRepository $collection_repository) - { + /** + * @var SetAuthorizer + */ + private $setAuthorizer; + + public function __construct( + CollectionRepository $collection_repository, + SetAuthorizer $setAuthorizer + ) { $this->collection_repository = $collection_repository; + $this->setAuthorizer = $setAuthorizer; } protected function isSupported(Query $query) @@ -25,19 +38,48 @@ protected function isSupported(Query $query) } /** - * @param FetchCollectionQuery $query - * @return LengthAwarePaginator + * @param FetchCollectionQuery $action + * + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + * + * phpcs:disable */ public function __invoke($query) //: LengthAwarePaginator { + // TODO: This is redundant, we should be able to remove this $this->isSupported($query); - $skip = $query->getLimit() * ($query->getPage() - 1); - return $this->collection_repository->fetch( - $query->getLimit(), - $skip, - $query->getSortBy(), - $query->getOrder(), - $query->getSearchData() - ); + + $search_fields = $query->getSearchData(); + + $search = new SearchData(); + + $user = Auth::guard()->user(); + + // Querying Values + $search->setFilter('keyword', $search_fields->q()); + $search->setFilter('role', $search_fields->role()); + $search->setFilter('is_admin', $search_fields->role() == "admin"); + // $search->setFilter('is_guest', !Auth::user() || !Auth::user()->id); + // $search->setFilter('is_me_only', $search_fields->public()); + $search->setFilter('user_id', $user->id ?? null); + + $search->setFilter('is_saved_search', false); + + // Paging Values + $limit = $query->getLimit() ?? config('paging.default_laravel_pageing_limit'); + $search->setFilter('limit', $limit); + $search->setFilter('skip', $limit * ($query->getPage() - 1)); + + // Sorting Values + $search->setFilter('sort', $query->getSortBy()); + $search->setFilter('order', $query->getOrder()); + + $this->collection_repository->setSearchParams($search); + + // TODO: We shouldn't let the repository return a Laravel paginator instance, + // this should be created in the controller + $result = $this->collection_repository->fetch(); + + return $result; } } diff --git a/src/Ushahidi/Modules/V5/Actions/SavedSearch/Handlers/FetchSavedSearchQueryHandler.php b/src/Ushahidi/Modules/V5/Actions/SavedSearch/Handlers/FetchSavedSearchQueryHandler.php index fe3749e04c..b1356b9da2 100644 --- a/src/Ushahidi/Modules/V5/Actions/SavedSearch/Handlers/FetchSavedSearchQueryHandler.php +++ b/src/Ushahidi/Modules/V5/Actions/SavedSearch/Handlers/FetchSavedSearchQueryHandler.php @@ -6,6 +6,8 @@ use App\Bus\Query\Query; use Ushahidi\Modules\V5\Actions\SavedSearch\Queries\FetchSavedSearchQuery; use Ushahidi\Modules\V5\Repository\Set\SetRepository as SavedSearchRepository; +use Ushahidi\Core\Tool\SearchData; +use Illuminate\Support\Facades\Auth; class FetchSavedSearchQueryHandler extends AbstractQueryHandler { @@ -31,13 +33,32 @@ protected function isSupported(Query $query) public function __invoke($query) //: LengthAwarePaginator { $this->isSupported($query); - $skip = $query->getLimit() * ($query->getPage() - 1); - return $this->saved_search_repository->fetch( - $query->getLimit(), - $skip, - $query->getSortBy(), - $query->getOrder(), - $query->getSearchData() - ); + + $search_fields = $query->getSearchData(); + + $search = new SearchData(); + $user = Auth::guard()->user(); + + $search->setFilter('keyword', $search_fields->q()); + $search->setFilter('role', $search_fields->role()); + $search->setFilter('is_admin', $search_fields->role() == "admin"); + // $search->setFilter('is_guest', !Auth::user() || !Auth::user()->id); + // $search->setFilter('is_me_only', $search_fields->public()); + $search->setFilter('user_id', $user->id ?? null); + + $search->setFilter('is_saved_search', true); + + // Paging Values + $limit = $query->getLimit() ?? config('paging.default_laravel_pageing_limit'); + $search->setFilter('limit', $limit); + $search->setFilter('skip', $limit * ($query->getPage() - 1)); + + // Sorting Values + $search->setFilter('sort', $query->getSortBy()); + $search->setFilter('order', $query->getOrder()); + + $this->saved_search_repository->setSearchParams($search); + + return $this->saved_search_repository->fetch(); } } diff --git a/src/Ushahidi/Modules/V5/Http/Controllers/CategoryController.php b/src/Ushahidi/Modules/V5/Http/Controllers/CategoryController.php index d870e4afb4..3fc370f14e 100644 --- a/src/Ushahidi/Modules/V5/Http/Controllers/CategoryController.php +++ b/src/Ushahidi/Modules/V5/Http/Controllers/CategoryController.php @@ -71,11 +71,12 @@ public function store(CategoryRequest $request) { $this->authorize('create', Category::class); + $id = $this->commandBus->handle(StoreCategoryCommand::createFromRequest($request)); + + $category = $this->queryBus->handle(new FetchCategoryByIdQuery($id)); + DB::beginTransaction(); try { - $id = $this->commandBus->handle(StoreCategoryCommand::createFromRequest($request)); - - $category = $this->queryBus->handle(new FetchCategoryByIdQuery($id)); $errors = $this->saveTranslations( $category, $category->toArray(), @@ -88,11 +89,12 @@ public function store(CategoryRequest $request) return self::make422($errors, 'translation'); } DB::commit(); - return new CategoryResource($category); } catch (\Exception $e) { DB::rollback(); return self::make500($e->getMessage()); } + + return new CategoryResource($category); } /** diff --git a/src/Ushahidi/Modules/V5/Http/Controllers/CollectionController.php b/src/Ushahidi/Modules/V5/Http/Controllers/CollectionController.php index 6083d89336..56765b36e6 100644 --- a/src/Ushahidi/Modules/V5/Http/Controllers/CollectionController.php +++ b/src/Ushahidi/Modules/V5/Http/Controllers/CollectionController.php @@ -30,7 +30,7 @@ public function show(int $id) { $collection = $this->queryBus->handle(new FetchCollectionByIdQuery($id)); - $this->authorizeAnyone('show', $collection); + $this->authorizeAnyone('view', $collection); return new CollectionResource($collection); } //end show() @@ -44,18 +44,18 @@ public function show(int $id) */ public function index(Request $request) { + $this->authorizeAnyone('viewAny', CollectionModel::class); - $this->authorizeAnyone('index', new CollectionModel()); - - $collections = $this->queryBus->handle( - new FetchCollectionQuery( - $request->query('limit', FetchCollectionQuery::DEFAULT_LIMIT), - $request->query('page', 1), - $request->query('sortBy', FetchCollectionQuery::DEFAULT_SORT_BY), - $request->query('order', FetchCollectionQuery::DEFAULT_ORDER), - new CollectionSearchFields($request) - ) + $action = new FetchCollectionQuery( + $request->query('limit', FetchCollectionQuery::DEFAULT_LIMIT), + $request->query('page', 1), + $request->query('sortBy', FetchCollectionQuery::DEFAULT_SORT_BY), + $request->query('order', FetchCollectionQuery::DEFAULT_ORDER), + new CollectionSearchFields($request) ); + + $collections = $this->queryBus->handle($action); + return new CollectionCollection($collections); } //end index() diff --git a/src/Ushahidi/Modules/V5/Http/Controllers/TosController.php b/src/Ushahidi/Modules/V5/Http/Controllers/TosController.php index 1da1584f51..fc1b69397b 100644 --- a/src/Ushahidi/Modules/V5/Http/Controllers/TosController.php +++ b/src/Ushahidi/Modules/V5/Http/Controllers/TosController.php @@ -26,7 +26,7 @@ protected function ignoreInput() { return ['user_id', 'agreement_date']; } - + /** * Display the specified resource. * @@ -41,7 +41,7 @@ public function show(int $id, QueryBus $queryBus) return new TosResource($tos); }//end show() - + /** * Display the specified resource. * @@ -64,7 +64,7 @@ public function index(Request $request, QueryBus $queryBus) return $resourceCollection; }//end index() - + /** * Create new Tos. * @@ -78,7 +78,7 @@ public function store(StoreTosRequest $request, CommandBus $commandBus, QueryBus $command = new CreateTosCommand( $this->setInputDefaults( $this->getFields($request->input()), - $this->getGenericUser() + $this->getAuthUser() ) ); $commandBus->handle($command); diff --git a/src/Ushahidi/Modules/V5/Http/Controllers/V5Controller.php b/src/Ushahidi/Modules/V5/Http/Controllers/V5Controller.php index f1c551f9b7..e49febcdc5 100644 --- a/src/Ushahidi/Modules/V5/Http/Controllers/V5Controller.php +++ b/src/Ushahidi/Modules/V5/Http/Controllers/V5Controller.php @@ -172,8 +172,7 @@ public function authorizeAnyone($ability, $arguments = []) list($ability, $arguments) = $this->parseAbilityAndArguments($ability, $arguments); return $this->authorizeForUser( - $this->getGenericUser() ?? - new GenericUser(['role' => 'guest']), + $this->getAuthUser() ?? new GenericUser(['role' => 'guest']), $ability, $arguments ); @@ -190,13 +189,13 @@ public function authorizeAnyone($ability, $arguments = []) */ public function authorizeForCurrentUser($ability, $arguments = []) { - $gUser = $this->getGenericUser(); + $user = $this->getAuthUser(); list($ability, $arguments) = $this->parseAbilityAndArguments($ability, $arguments); - return app(Gate::class)->forUser($gUser)->authorize($ability, $arguments); + return app(Gate::class)->forUser($user)->authorize($ability, $arguments); } - public function getGenericUser() + public function getAuthUser() { return Auth::guard()->user(); } diff --git a/src/Ushahidi/Modules/V5/Models/Category.php b/src/Ushahidi/Modules/V5/Models/Category.php index 05e2387678..876643279f 100644 --- a/src/Ushahidi/Modules/V5/Models/Category.php +++ b/src/Ushahidi/Modules/V5/Models/Category.php @@ -43,6 +43,7 @@ class Category extends BaseModel */ protected $fillable = [ 'parent_id', + 'user_id', 'tag', 'slug', 'type', diff --git a/src/Ushahidi/Modules/V5/Policies/CategoryPolicy.php b/src/Ushahidi/Modules/V5/Policies/CategoryPolicy.php index 50aca6b7c8..b01264687b 100644 --- a/src/Ushahidi/Modules/V5/Policies/CategoryPolicy.php +++ b/src/Ushahidi/Modules/V5/Policies/CategoryPolicy.php @@ -45,7 +45,10 @@ public function delete(User $user, EloquentCategory $category) public function update(User $user, EloquentCategory $category) { - $accessedCategory = new StaticCategory($category->toArray()); + $accessedCategory = new StaticCategory($category->getRawOriginal()); + + $accessedCategory->setState($category->getDirty()); + return $this->authorizer->setUser($user)->isAllowed($accessedCategory, 'update'); } } diff --git a/src/Ushahidi/Modules/V5/Policies/CollectionPolicy.php b/src/Ushahidi/Modules/V5/Policies/CollectionPolicy.php deleted file mode 100644 index 0f4ba250b5..0000000000 --- a/src/Ushahidi/Modules/V5/Policies/CollectionPolicy.php +++ /dev/null @@ -1,192 +0,0 @@ -queryBus = $queryBus; - } - - /** - * - * @return bool - */ - public function index() - { - $set_entity = new Entity\Set(); - return $this->isAllowed($set_entity, 'search'); - } - - /** - * - * @param User $user - * @param Set $set - * @return bool - */ - public function show(User $user, Set $set) - { - $set_entity = new Entity\Set(); - $set_entity->setState($set->toArray()); - return $this->isAllowed($set_entity, 'read'); - } - - /** - * - * @param GenericUser $user - * @param Set $set - * @return bool - */ - public function delete(User $user, Set $set) - { - $set_entity = new Entity\Set(); - $set_entity->setState($set->toArray()); - return $this->isAllowed($set_entity, 'delete'); - } - /** - * @param Set $set - * @return bool - */ - public function update(User $user, Set $set) - { - // we convert to a form entity to be able to continue using the old authorizers and classes. - $set_entity = new Entity\Set($set->toArray()); - return $this->isAllowed($set_entity, 'update'); - } - - - /** - * @param Survey $set - * @return bool - */ - public function store() - { - // we convert to a form entity to be able to continue using the old authorizers and classes. - $set_entity = new Entity\Set(); - return $this->isAllowed($set_entity, 'create'); - } - /** - * @param $entity - * @param string $privilege - * @return bool - */ - public function isAllowed($entity, $privilege) - { - $authorizer = service('authorizer.set'); - - // These checks are run within the user context. - $user = $authorizer->getUser(); - //$user = Auth::user(); - // Only logged in users have access if the deployment is private - if (!$this->canAccessDeployment($user)) { - return false; - } - - // Then we check if a user has the 'admin' role. If they do they're - // allowed access to everything (all entities and all privileges) - if ($this->isUserAdmin($user)) { - return true; - } - // Non-admin users are not allowed to change collection featured - $old_values = []; - if ($entity->id) { - $old_set = Set::where('id', '=', $entity->id)->first(); - $old_values = $old_set->toArray(); - } - if (in_array($privilege, ['create', 'update']) - && $this->valueIsChanged( - 'featured', - $entity->asArray(), - $old_values - ) - ) { - return false; - } - - // If the user is the owner of this set, they can do anything - if ($this->isUserOwner($entity, $user)) { - return true; - } - - - // First check whether there is a role with the right permissions - if ($authorizer->acl->hasPermission($user, Permission::MANAGE_SETS)) { - return true; - } - - - // Check if the Set is only visible to specific roles. - if ($this->isVisibleToUser($entity, $user) and $privilege === 'read') { - return true; - } - - // All *logged in* users can create sets - if ($user->getId() and $privilege === 'create') { - return true; - } - - // Finally, all users can search sets - if ($privilege === 'search') { - return true; - } - - // If no other access checks succeed, we default to denying access - return false; - } - - protected function isVisibleToUser(Entity\Set $entity, $user) - { - if ($entity->role) { - return in_array($user->role, $entity->role); - } - - // If no roles are selected, the Set is considered completely public. - return true; - } - - private function valueIsChanged($key, $new_values, $old_values) - { - if (isset($old_values[$key]) && $new_values[$key] != $old_values[$key]) { - return true; - } - return false; - } -} diff --git a/src/Ushahidi/Modules/V5/Policies/SetPolicy.php b/src/Ushahidi/Modules/V5/Policies/SetPolicy.php new file mode 100644 index 0000000000..c58a376134 --- /dev/null +++ b/src/Ushahidi/Modules/V5/Policies/SetPolicy.php @@ -0,0 +1,64 @@ +authorizer = $authorizer; + $this->authorizer->setAcl($acl); + } + + public function viewAny(User $user) + { + $this->authorizer->setUser($user); + return $this->authorizer->isAllowed(new OhanzeeSet, 'search'); + } + + public function view(User $user, Set $set) + { + $this->authorizer->setUser($user); + + $entity = new OhanzeeSet; + $entity->setState($set->toArray()); + return $this->authorizer->isAllowed($entity, 'read'); + } + + public function store(User $user) + { + $this->authorizer->setUser($user); + + // we convert to a form entity to be able to continue using the old authorizers and classes. + return $this->authorizer->isAllowed(new OhanzeeSet, 'create'); + } + + public function delete(User $user, Set $set) + { + $this->authorizer->setUser($user); + + $set_entity = new OhanzeeSet(); + $set_entity->setState($set->toArray()); + return $this->authorizer->isAllowed($set_entity, 'delete'); + } + + public function update(User $user, Set $set) + { + $this->authorizer->setUser($user); + + // we convert to a form entity to be able to continue using the old authorizers and classes. + $set_entity = new OhanzeeSet($set->getRawOriginal()); + + $set_entity->setState($set->getDirty()); + + return $this->authorizer->isAllowed($set_entity, 'update'); + } +} diff --git a/src/Ushahidi/Modules/V5/Providers/AuthServiceProvider.php b/src/Ushahidi/Modules/V5/Providers/AuthServiceProvider.php index 05206b4a3f..682835929d 100644 --- a/src/Ushahidi/Modules/V5/Providers/AuthServiceProvider.php +++ b/src/Ushahidi/Modules/V5/Providers/AuthServiceProvider.php @@ -24,7 +24,7 @@ class AuthServiceProvider extends ServiceProvider Models\Role::class => Policies\RolePolicy::class, Models\Post\Post::class => Policies\PostPolicy::class, Models\Tos::class => Policies\TosPolicy::class, - Models\Set::class => Policies\CollectionPolicy::class, + Models\Set::class => Policies\SetPolicy::class, Models\SetPost::class => Policies\CollectionPostPolicy::class, Models\Config::class => Policies\ConfigPolicy::class, Models\Contact::class => Policies\ContactPolicy::class, diff --git a/src/Ushahidi/Modules/V5/Repository/Category/CategoryRepository.php b/src/Ushahidi/Modules/V5/Repository/Category/CategoryRepository.php index f609f31c49..79bfedf932 100644 --- a/src/Ushahidi/Modules/V5/Repository/Category/CategoryRepository.php +++ b/src/Ushahidi/Modules/V5/Repository/Category/CategoryRepository.php @@ -6,6 +6,7 @@ use Ushahidi\Modules\V5\Models\Category; use Ushahidi\Modules\V5\DTO\Paging; use Ushahidi\Modules\V5\DTO\CategorySearchFields; +use Ushahidi\Core\Tool\SearchData; interface CategoryRepository { @@ -19,10 +20,11 @@ interface CategoryRepository */ public function findById(int $id): Category; - public function fetchAll(Paging $paging, CategorySearchFields $category_search_fields); + public function fetchAll(Paging $paging); public function store( ?string $parentId, + ?int $userId, string $tag, string $slug, string $type, @@ -38,6 +40,7 @@ public function slugExists(string $slug): bool; public function update( int $id, ?string $parentId, + ?int $userId, ?string $tag, ?string $slug, ?string $type, @@ -49,4 +52,6 @@ public function update( ?string $defaultBaseLanguage, ?array $availableLanguages ): int; + + public function setSearchParams(SearchData $searchData); } diff --git a/src/Ushahidi/Modules/V5/Repository/Category/EloquentCategoryRepository.php b/src/Ushahidi/Modules/V5/Repository/Category/EloquentCategoryRepository.php index 83b0bd185d..1be5c60b19 100644 --- a/src/Ushahidi/Modules/V5/Repository/Category/EloquentCategoryRepository.php +++ b/src/Ushahidi/Modules/V5/Repository/Category/EloquentCategoryRepository.php @@ -2,46 +2,98 @@ namespace Ushahidi\Modules\V5\Repository\Category; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use Ushahidi\Modules\V5\Models\Category; use Ushahidi\Modules\V5\DTO\Paging; use Ushahidi\Modules\V5\DTO\CategorySearchFields; use Ushahidi\Core\Exception\NotFoundException; use Illuminate\Support\Facades\Auth; +use Ushahidi\Core\Tool\SearchData; class EloquentCategoryRepository implements CategoryRepository { - private function setSearchCondition(CategorySearchFields $search_fields, $builder) + /** + * @var SearchData + */ + protected $searchData = null; + + /** + * Set search constraints + * + * @param SearchData $searchData + * @return void + */ + public function setSearchParams(SearchData $searchData) { + $this->searchData = $searchData; + } - if ($search_fields->q()) { - $builder->where('tag', 'LIKE', "%" . $search_fields->q() . "%"); - } - if ($search_fields->tag()) { - $builder->where('tag', '=', $search_fields->tag()); - } - if ($search_fields->type()) { - $builder->where('type', '=', $search_fields->type()); + private function setSearchCondition(Builder $builder, ?SearchData $search_fields) + { + if ($search_fields === null) { + return $builder; } - if ($search_fields->level() === 'parent') { + $parent_id = $search_fields->getFilter('parent_id'); + $is_parent = $search_fields->getFilter('is_parent'); + + if (isset($parent_id)) { + $builder->where('parent_id', $parent_id); + } elseif ($is_parent === false) { $builder->whereNull('parent_id'); } - if ($search_fields->parentId()) { - $builder->where('parent_id', '=', $search_fields->parentId()); - } - if (!Auth::user() || !Auth::user()->id) { - $builder->whereNull('role'); - } elseif ($search_fields->role() && $search_fields->role() != "admin") { - $builder->where(function ($query) use ($search_fields) { - $query->whereNull('role') - ->orWhere('role', 'LIKE', "%" . $search_fields->role() . "%"); + $builder->where(function (Builder $builder) use ($search_fields) { + $keyword = $search_fields->getFilter('keyword'); + $tag = $search_fields->getFilter('tag'); + $type = $search_fields->getFilter('type'); + + if (isset($keyword)) { + $builder->where('tag', 'LIKE', "%" . $keyword . "%"); + } + + if (isset($tag)) { + $builder->orWhere('tag', 'LIKE', "%" . $keyword . "%"); + } + + if (isset($type)) { + $builder->orWhere('type', '=', $type); + } + }); + + $is_admin = $search_fields->getFilter('is_admin'); + if ($is_admin === false) { + $builder->where(function (Builder $builder) use ($search_fields) { + // Default always get categories with null roles or has everyone + $builder->whereNull('role'); + + // This query isn't working as expected + $builder->orWhere('role', 'like', '%everyone%'); + + $role = $search_fields->getFilter('role'); + if (isset($role) && !is_null($role)) { + $builder->orWhere('role', 'like', "%" . $role . "%"); + } + + // If it's a logged in user + $user_id = $search_fields->getFilter('user_id'); + if (isset($user_id) && !is_null($user_id)) { + // Where the user is the owner of the category + $builder->orWhere(function (Builder $query) use ($user_id) { + //TODO: Fix this query in future release + $query->where('role', 'like', '%me%') + ->where('user_id', $user_id); + }); + } }); } + + // var_dump($builder->toSql()); + // exit; return $builder; } - + /** * This method will fetch a single Category from the database utilising * Laravel Eloquent ORM. Will throw an exception if provided identifier does @@ -58,18 +110,20 @@ public function findById(int $id): Category } return $category; } - public function fetchAll(Paging $paging, CategorySearchFields $category_search_fields) + + public function fetchAll(Paging $paging) { return $this->setSearchCondition( - $category_search_fields, Category::take($paging->getLimit()) ->skip($paging->getSkip()) - ->orderBy($paging->getOrderBy(), $paging->getOrder()) + ->orderBy($paging->getOrderBy(), $paging->getOrder()), + $this->searchData )->paginate($paging->getLimit()); } public function store( ?string $parentId, + ?int $userId, string $tag, string $slug, string $type, @@ -83,6 +137,7 @@ public function store( ): int { $input = array_filter([ 'parent_id' => $parentId, + 'user_id' => $userId, 'tag' => $tag, 'slug' => $slug, 'type' => $type, @@ -97,8 +152,7 @@ public function store( }); $category = new Category($input); - $category->saveOrFail(); - $category->refresh(); + $isSaved = $category->saveOrFail(); return $category->id; } @@ -111,6 +165,7 @@ public function slugExists(string $slug): bool public function update( int $id, ?string $parentId, + ?int $userId, ?string $tag, ?string $slug, ?string $type, @@ -124,6 +179,7 @@ public function update( ): int { $input = [ 'parent_id' => $parentId, + 'user_id' => $userId, 'tag' => $tag, 'slug' => $slug, 'type' => $type, diff --git a/src/Ushahidi/Modules/V5/Repository/Set/EloquentSetRepository.php b/src/Ushahidi/Modules/V5/Repository/Set/EloquentSetRepository.php index b514076fb1..4f672836ed 100644 --- a/src/Ushahidi/Modules/V5/Repository/Set/EloquentSetRepository.php +++ b/src/Ushahidi/Modules/V5/Repository/Set/EloquentSetRepository.php @@ -11,9 +11,15 @@ use Ushahidi\Modules\V5\DTO\CollectionSearchFields; use Ushahidi\Core\Entity\Set as CollectionEntity; use Illuminate\Support\Facades\Auth; +use Ushahidi\Core\Tool\SearchData; class EloquentSetRepository implements SetRepository { + /** + * @var SearchData + */ + protected $searchData; + /** * This method will fetch all the Set for the logged user from the database utilising * Laravel Eloquent ORM and return them as an array @@ -22,44 +28,76 @@ class EloquentSetRepository implements SetRepository * @param string $sortBy * @param string $order * - * @return Set[] + * @return Set[]|LengthAwarePaginator */ - public function fetch( - int $limit, - int $skip, - string $sortBy, - string $order, - CollectionSearchFields $search_fields - ): LengthAwarePaginator { - - return $this->setSearchCondition( - $search_fields, - Set::query()->withCount('posts')->take($limit) - ->skip($skip) - ->orderBy($sortBy, $order) - )->paginate($limit ? $limit : config('paging.default_laravel_pageing_limit')); + public function fetch() + { + $data = $this->searchData; + + $query = $this->setSearchCondition(Set::query(), $data); + + $sort = $data->getFilter('sort'); + $order = $data->getFilter('order'); + if (isset($sort)) { + $query->orderBy($sort, $order); + } + + if ($data->getFilter('with_post_count')) { + $query->withCount('posts'); + } + + $limit = $data->getFilter('limit'); + if (isset($limit)) { + return $query->paginate($limit); + } + return $query->get(); } - private function setSearchCondition(CollectionSearchFields $search_fields, Builder $builder) + private function setSearchCondition(Builder $builder, SearchData $search_fields) { - $builder->where('search', '=', $search_fields->search()); + $is_saved_search = (int) $search_fields->getFilter('is_saved_search'); + $builder->where('search', '=', $is_saved_search); - if ($search_fields->q()) { - $builder->where('name', 'LIKE', "%" . $search_fields->q() . "%"); + $keyword = $search_fields->getFilter('keyword'); + if (isset($keyword) && !empty($keyword)) { + $builder->where('name', 'LIKE', "%" . $keyword . "%"); } - // guest - if (!Auth::user() || !Auth::user()->id) { - $builder->whereNull('role'); - } elseif ($search_fields->role() && $search_fields->role() != "admin") { + + $is_admin = $search_fields->getFilter('is_admin'); + if ($is_admin == false) { $builder->where(function ($query) use ($search_fields) { - $query->whereNull('role') - ->orWhere('role', 'LIKE', "%" . $search_fields->role() . "%"); + // Default search for everyone and guest user + $query->where('role', 'LIKE', "%everyone%"); + + $query->orWhereNull('role'); + + // is owner + $user_id = $search_fields->getFilter('user_id'); + if (isset($user_id) && !is_null($user_id)) { + $query->orWhere(function ($query) use ($user_id) { + $query->where('role', 'LIKE', "%me%") + ->where('user_id', '=', $user_id); + }); + } + + // is not admin + $role = $search_fields->getFilter('role'); + if (isset($role) && !is_null($role)) { + $query->orWhere(function ($builder) use ($search_fields) { + $builder->where('role', 'LIKE', "%" . $search_fields->getFilter('role') . "%"); + }); + } }); } return $builder; } + public function setSearchParams(SearchData $searchData) + { + $this->searchData = $searchData; + } + /** * This method will fetch a single Set from the database utilising * Laravel Eloquent ORM. Will throw an exception if provided identifier does diff --git a/src/Ushahidi/Modules/V5/Repository/Set/SetRepository.php b/src/Ushahidi/Modules/V5/Repository/Set/SetRepository.php index ec5e525738..a4e927e0c0 100644 --- a/src/Ushahidi/Modules/V5/Repository/Set/SetRepository.php +++ b/src/Ushahidi/Modules/V5/Repository/Set/SetRepository.php @@ -3,9 +3,10 @@ namespace Ushahidi\Modules\V5\Repository\Set; use Ushahidi\Modules\V5\Models\Set; -use Illuminate\Pagination\LengthAwarePaginator; -use Ushahidi\Modules\V5\DTO\CollectionSearchFields; use Ushahidi\Core\Entity\Set as CollectionEntity; +use Ushahidi\Core\Exception\NotFoundException; +use Ushahidi\Core\Tool\SearchData; +use Ushahidi\Modules\V5\DTO\CollectionSearchFields; interface SetRepository { @@ -17,15 +18,9 @@ interface SetRepository * @param string $sortBy * @param string $order * @param bool $search - * @return Set[] + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ - public function fetch( - int $limit, - int $skip, - string $sortBy, - string $order, - CollectionSearchFields $search_fields - ): LengthAwarePaginator; + public function fetch(); /** * This method will fetch a single Set from the database utilising @@ -57,4 +52,6 @@ public function update(int $id, CollectionEntity $set_entity, bool $search = fal * @param int $id */ public function delete(int $id, bool $search = false): void; + + public function setSearchParams(SearchData $searchData); } diff --git a/tests/Integration/v5/tags.v5.feature b/tests/Integration/v5/tags.v5.feature index b99236ad6d..a025b1ba62 100644 --- a/tests/Integration/v5/tags.v5.feature +++ b/tests/Integration/v5/tags.v5.feature @@ -120,7 +120,7 @@ Feature: Testing the Categories API Then the response is JSON And the response has a "errors.failed_validations" property And the "errors.failed_validations.0.field" property equals "role" - And the "errors.failed_validations.0.error_messages.0" property equals "The child category role must be the same as the parent role." + And the "errors.failed_validations.0.error_messages.0" property equals "The child category role must be the same as the parent role." Then the guzzle status code should be 422 Scenario: Creating a tag with a duplicate slug is not possible Given that I want to make a new "Category" @@ -368,7 +368,7 @@ Feature: Testing the Categories API And that the api_url is "api/v5" When I request "/categories" Then the response is JSON - And the "results" property count is "19" + And the "results" property count is "14" Then the guzzle status code should be 200 Scenario: Listing All Tags available to regular users Given that I want to get all "Categories" @@ -376,15 +376,15 @@ Feature: Testing the Categories API And that the api_url is "api/v5" When I request "/categories" Then the response is JSON - And the "results" property count is "9" + And the "results" property count is "7" Then the guzzle status code should be 200 - + Scenario: Listing All Tags available to non-users Given that I want to get all "Categories" And that the api_url is "api/v5" When I request "/categories" Then the response is JSON - And the "results" property count is "7" + And the "results" property count is "5" Then the guzzle status code should be 200 # # @resetFixture @@ -557,7 +557,7 @@ Feature: Testing the Categories API Then the response is JSON And the response has a "errors.failed_validations" property And the "errors.failed_validations.0.field" property equals "role" - And the "errors.failed_validations.0.error_messages.0" property equals "The child category role must be the same as the parent role." + And the "errors.failed_validations.0.error_messages.0" property equals "The child category role must be the same as the parent role." Then the guzzle status code should be 422 Scenario: Creating a new child with no role for a tag with role=["admin"] @@ -581,7 +581,7 @@ Feature: Testing the Categories API Then the response is JSON And the response has a "errors.failed_validations" property And the "errors.failed_validations.0.field" property equals "role" - And the "errors.failed_validations.0.error_messages.0" property equals "The child category role must be the same as the parent role." + And the "errors.failed_validations.0.error_messages.0" property equals "The child category role must be the same as the parent role." Then the guzzle status code should be 422 @resetFixture diff --git a/tests/Unit/DataSource/EmailDataSourceTest.php b/tests/Unit/DataSource/EmailDataSourceTest.php index ceaec5d66f..d8f11dc688 100644 --- a/tests/Unit/DataSource/EmailDataSourceTest.php +++ b/tests/Unit/DataSource/EmailDataSourceTest.php @@ -108,7 +108,7 @@ public function testFetch() $mockImapOpen = PHPMockery::mock("Ushahidi\DataSource\Email", 'imap_open'); $mockImapOpen ->with( - '{imap.somewhere.com:993/imap/ssl}INBOX', + '{imap.somewhere.com:993/imap/ssl/novalidate-cert}INBOX', 'someuser', 'mypassword', 0,