first commit
This commit is contained in:
@@ -0,0 +1,685 @@
|
||||
<?php
|
||||
/**
|
||||
* @file classes/user/Collector.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Collector
|
||||
*
|
||||
* @brief A helper class to configure a Query Builder to get a collection of users
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use APP\core\Application;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\MySqlConnection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use InvalidArgumentException;
|
||||
use PKP\core\interfaces\CollectorInterface;
|
||||
use PKP\core\PKPString;
|
||||
use PKP\facades\Locale;
|
||||
use PKP\identity\Identity;
|
||||
use PKP\plugins\Hook;
|
||||
|
||||
/**
|
||||
* @template T of User
|
||||
*/
|
||||
class Collector implements CollectorInterface
|
||||
{
|
||||
public const ORDERBY_ID = 'id';
|
||||
public const ORDERBY_GIVENNAME = 'givenName';
|
||||
public const ORDERBY_FAMILYNAME = 'familyName';
|
||||
|
||||
public const ORDER_DIR_ASC = 'ASC';
|
||||
public const ORDER_DIR_DESC = 'DESC';
|
||||
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_DISABLED = 'disabled';
|
||||
public const STATUS_ALL = null;
|
||||
|
||||
public DAO $dao;
|
||||
|
||||
public string $orderBy = self::ORDERBY_ID;
|
||||
public string $orderDirection = 'ASC';
|
||||
public ?array $orderLocales = null;
|
||||
public ?array $userGroupIds = null;
|
||||
public ?array $roleIds = null;
|
||||
public ?array $userIds = null;
|
||||
public ?array $excludeUserIds = null;
|
||||
public ?array $workflowStageIds = null;
|
||||
public ?array $contextIds = null;
|
||||
public ?string $registeredBefore = null;
|
||||
public ?string $registeredAfter = null;
|
||||
public ?string $status = self::STATUS_ACTIVE;
|
||||
public bool $includeReviewerData = false;
|
||||
public ?array $assignedSectionIds = null;
|
||||
public ?array $assignedCategoryIds = null;
|
||||
public ?array $settings = null;
|
||||
public ?string $searchPhrase = null;
|
||||
public ?array $excludeSubmissionStage = null;
|
||||
public ?array $excludeRoles = null;
|
||||
public ?array $assignedTo = null;
|
||||
public ?int $reviewerRating = null;
|
||||
public ?int $reviewsCompleted = null;
|
||||
public ?array $daysSinceLastAssignment = null;
|
||||
public ?int $averageCompletion = null;
|
||||
public ?array $reviewsActive = null;
|
||||
public ?int $count = null;
|
||||
public ?int $offset = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(DAO $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
public function getCount(): int
|
||||
{
|
||||
return $this->dao->getCount($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc DAO::getMany()
|
||||
*
|
||||
* @return LazyCollection<int,T>
|
||||
*/
|
||||
public function getMany(): LazyCollection
|
||||
{
|
||||
return $this->dao->getMany($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int,int>
|
||||
*/
|
||||
public function getIds(): Collection
|
||||
{
|
||||
return $this->dao->getIds($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to users in these user groups
|
||||
*/
|
||||
public function filterByUserGroupIds(?array $userGroupIds): self
|
||||
{
|
||||
$this->userGroupIds = $userGroupIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by user IDs
|
||||
*/
|
||||
public function filterByUserIds(?array $userIds): self
|
||||
{
|
||||
$this->userIds = $userIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to those who have a minimum reviewer rating
|
||||
*/
|
||||
public function filterByReviewerRating(?int $reviewerRating): self
|
||||
{
|
||||
$this->includeReviewerData(true);
|
||||
$this->reviewerRating = $reviewerRating;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to those who have completed at least this many reviews
|
||||
*/
|
||||
public function filterByReviewsCompleted(?int $reviewsCompleted): self
|
||||
{
|
||||
$this->includeReviewerData(true);
|
||||
$this->reviewsCompleted = $reviewsCompleted;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to those who's last review assignment was at least this many
|
||||
* days ago.
|
||||
*/
|
||||
public function filterByDaysSinceLastAssignment(?int $minimumDaysSinceLastAssignment = null, ?int $maximumDaysSinceLastAssignment = null): self
|
||||
{
|
||||
$this->includeReviewerData(true);
|
||||
$this->daysSinceLastAssignment = [$minimumDaysSinceLastAssignment, $maximumDaysSinceLastAssignment];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to those who complete a review on average less than this many
|
||||
* days after their assignment.
|
||||
*/
|
||||
public function filterByAverageCompletion(?int $averageCompletion): self
|
||||
{
|
||||
$this->includeReviewerData(true);
|
||||
$this->averageCompletion = $averageCompletion;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to those who have at least this many active review assignments
|
||||
*/
|
||||
public function filterByReviewsActive(?int $minimumReviewsActive = null, ?int $maximumReviewsActive = null): self
|
||||
{
|
||||
$this->includeReviewerData(true);
|
||||
$this->reviewsActive = [$minimumReviewsActive, $maximumReviewsActive];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude results with the specified user IDs.
|
||||
*/
|
||||
public function excludeUserIds(?array $excludeUserIds): self
|
||||
{
|
||||
$this->excludeUserIds = $excludeUserIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to users enrolled in these roles
|
||||
*/
|
||||
public function filterByRoleIds(?array $roleIds): self
|
||||
{
|
||||
$this->roleIds = $roleIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to users enrolled in these roles
|
||||
*/
|
||||
public function filterByWorkflowStageIds(?array $workflowStageIds): self
|
||||
{
|
||||
$this->workflowStageIds = $workflowStageIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Limit results to users with user groups in these context IDs
|
||||
*/
|
||||
public function filterByContextIds(?array $contextIds): self
|
||||
{
|
||||
$this->contextIds = $contextIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to users registered before a specified date.
|
||||
*/
|
||||
public function filterRegisteredBefore(?string $registeredBefore): self
|
||||
{
|
||||
$this->registeredBefore = $registeredBefore;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to users registered after a specified date.
|
||||
*/
|
||||
public function filterRegisteredAfter(?string $registeredAfter): self
|
||||
{
|
||||
$this->registeredAfter = $registeredAfter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines whether reviewer data should be included
|
||||
*/
|
||||
public function includeReviewerData(bool $includeReviewerData = true): self
|
||||
{
|
||||
$this->includeReviewerData = $includeReviewerData;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of users not assigned to a given submission stage as a user group.
|
||||
* (Replaces UserStageAssignmentDAO::getUsersNotAssignedToStageInUserGroup)
|
||||
*/
|
||||
public function filterExcludeSubmissionStage(int $submissionId, int $stageId, int $userGroupId): self
|
||||
{
|
||||
$this->excludeSubmissionStage = [
|
||||
'submission_id' => $submissionId,
|
||||
'stage_id' => $stageId,
|
||||
'user_group_id' => $userGroupId,
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of users not assigned to the given roles
|
||||
* (Replaces UserGroupDAO::getUsersNotInRole)
|
||||
*/
|
||||
public function filterExcludeRoles(?array $excludedRoles): self
|
||||
{
|
||||
$this->excludeRoles = $excludedRoles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve assigned users by submission and stage IDs.
|
||||
* (Replaces UserStageAssignmentDAO::getUsersBySubmissionAndStageId)
|
||||
*/
|
||||
public function assignedTo(?int $submissionId = null, ?int $stageId = null, ?int $userGroupId = null): self
|
||||
{
|
||||
if ($submissionId === null) {
|
||||
// Clear the condition.
|
||||
$this->assignedTo = null;
|
||||
if ($stageId !== null || $userGroupId !== null) {
|
||||
throw new InvalidArgumentException('If a stage or user group ID is specified, a submission ID must be specified as well.');
|
||||
}
|
||||
} else {
|
||||
$this->assignedTo = [
|
||||
'submissionId' => $submissionId,
|
||||
'stageId' => $stageId,
|
||||
'userGroupId' => $userGroupId,
|
||||
];
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by active / disabled status.
|
||||
*
|
||||
* @param string|null $status self::STATUS_ACTIVE, self::STATUS_DISABLED or self::STATUS_ALL.
|
||||
*/
|
||||
public function filterByStatus(?string $status): self
|
||||
{
|
||||
if (!in_array($this->status, [self::STATUS_ACTIVE, self::STATUS_DISABLED, self::STATUS_ALL], true)) {
|
||||
throw new InvalidArgumentException("Invalid status: \"{$this->status}\"");
|
||||
}
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by assigned subeditor section IDs
|
||||
*/
|
||||
public function assignedToSectionIds(?array $sectionIds): self
|
||||
{
|
||||
$this->assignedSectionIds = $sectionIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by assigned subeditor section IDs
|
||||
*/
|
||||
public function assignedToCategoryIds(?array $categoryIds): self
|
||||
{
|
||||
$this->assignedCategoryIds = $categoryIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by exact match of user settings (the locale is ignored)
|
||||
*
|
||||
* @param array|null $settings The key must be a valid setting_name while the value will match the setting_value
|
||||
*/
|
||||
public function filterBySettings(?array $settings): self
|
||||
{
|
||||
$this->settings = $settings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit results to users matching this search query
|
||||
*/
|
||||
public function searchPhrase(?string $phrase): self
|
||||
{
|
||||
$this->searchPhrase = $phrase;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order the results
|
||||
*
|
||||
* @param string $sorter One of the self::ORDERBY_ constants
|
||||
* @param string $direction One of the self::ORDER_DIR_ constants
|
||||
* @param array|null locales Optional list of locale precedences when ordering by localized columns
|
||||
*/
|
||||
public function orderBy(string $sorter, string $direction = self::ORDER_DIR_DESC, ?array $locales = null): self
|
||||
{
|
||||
if (!in_array($sorter, [static::ORDERBY_FAMILYNAME, static::ORDERBY_GIVENNAME, static::ORDERBY_ID])) {
|
||||
throw new InvalidArgumentException("Invalid order by: {$sorter}");
|
||||
}
|
||||
if (!in_array($direction, [static::ORDER_DIR_ASC, static::ORDER_DIR_DESC])) {
|
||||
throw new InvalidArgumentException("Invalid order direction: {$direction}");
|
||||
}
|
||||
$this->orderBy = $sorter;
|
||||
$this->orderDirection = $direction;
|
||||
$this->orderLocales = $locales;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of objects retrieved
|
||||
*/
|
||||
public function limit(?int $count): self
|
||||
{
|
||||
$this->count = $count;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the number of objects retrieved, for example to
|
||||
* retrieve the second page of contents
|
||||
*/
|
||||
public function offset(?int $offset): self
|
||||
{
|
||||
$this->offset = $offset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc CollectorInterface::getQueryBuilder()
|
||||
*/
|
||||
public function getQueryBuilder(): Builder
|
||||
{
|
||||
if ($this->offset !== null && $this->count === null) {
|
||||
throw new InvalidArgumentException('The offset requires the count to be defined');
|
||||
}
|
||||
|
||||
$query = DB::table('users', 'u')
|
||||
->select('u.*')
|
||||
// Filters by registration date
|
||||
->when($this->registeredBefore !== null, fn (Builder $query) => $query->where('u.date_registered', '<', Carbon::rawParse($this->registeredBefore)->addDay()->toDateString()))
|
||||
->when($this->registeredAfter !== null, fn (Builder $query) => $query->where('u.date_registered', '>=', $this->registeredAfter))
|
||||
// Filters by user ID
|
||||
->when($this->userIds !== null, fn (Builder $query) => $query->whereIn('u.user_id', $this->userIds))
|
||||
->when($this->excludeUserIds !== null, fn (Builder $query) => $query->whereNotIn('u.user_id', $this->excludeUserIds))
|
||||
// User enabled/disabled state
|
||||
->when($this->status !== self::STATUS_ALL, fn (Builder $query) => $query->where('u.disabled', '=', $this->status === self::STATUS_DISABLED))
|
||||
// Adds limit and offset for pagination
|
||||
->when($this->count !== null, fn (Builder $query) => $query->limit($this->count))
|
||||
->when($this->offset !== null, fn (Builder $query) => $query->offset($this->offset));
|
||||
|
||||
$this
|
||||
->buildReviewerStatistics($query)
|
||||
->buildUserGroupFilter($query)
|
||||
->buildSearchFilter($query)
|
||||
->buildSubEditorFilter($query)
|
||||
->buildSettingsFilter($query)
|
||||
->buildExcludedSubmissionStagesFilter($query)
|
||||
->buildSubmissionAssignmentsFilter($query)
|
||||
->buildOrderBy($query);
|
||||
|
||||
// Add app-specific query statements
|
||||
Hook::call('User::Collector', [$query, $this]);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the filters related to submission assignments (submission, stage, user group)
|
||||
*/
|
||||
protected function buildSubmissionAssignmentsFilter(Builder $query): self
|
||||
{
|
||||
if ($this->assignedTo === null) {
|
||||
return $this;
|
||||
}
|
||||
$query->whereExists(
|
||||
fn (Builder $query) => $query->from('stage_assignments', 'sa')
|
||||
->join('user_group_stage AS ugs', 'sa.user_group_id', '=', 'ugs.user_group_id')
|
||||
->whereColumn('sa.user_id', '=', 'u.user_id')
|
||||
->when(isset($this->assignedTo['submissionId']), fn ($query) => $query->where('sa.submission_id', '=', $this->assignedTo['submissionId']))
|
||||
->when(isset($this->assignedTo['stageId']), fn ($query) => $query->where('ugs.stage_id', '=', $this->assignedTo['stageId']))
|
||||
->when(isset($this->assignedTo['userGroupId']), fn ($query) => $query->where('sa.user_group_id', '=', $this->assignedTo['userGroupId']))
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the filters related to the user group
|
||||
*/
|
||||
protected function buildUserGroupFilter(Builder $query): self
|
||||
{
|
||||
if ($this->userGroupIds === null && $this->roleIds === null && $this->contextIds === null && $this->workflowStageIds === null) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$subQuery = DB::table('user_user_groups as uug')
|
||||
->join('user_groups AS ug', 'uug.user_group_id', '=', 'ug.user_group_id')
|
||||
->whereColumn('uug.user_id', '=', 'u.user_id')
|
||||
->when($this->userGroupIds !== null, fn (Builder $subQuery) => $subQuery->whereIn('uug.user_group_id', $this->userGroupIds))
|
||||
->when(
|
||||
$this->workflowStageIds !== null,
|
||||
fn (Builder $subQuery) => $subQuery
|
||||
->join('user_group_stage AS ugs', 'ug.user_group_id', '=', 'ugs.user_group_id')
|
||||
->whereIn('ugs.stage_id', $this->workflowStageIds)
|
||||
)
|
||||
->when($this->roleIds !== null, fn ($subQuery) => $subQuery->whereIn('ug.role_id', $this->roleIds))
|
||||
->when($this->contextIds !== null, fn ($subQuery) => $subQuery->whereIn('ug.context_id', $this->contextIds));
|
||||
|
||||
$joinSub = clone $subQuery;
|
||||
$query
|
||||
->addWhereExistsQuery(
|
||||
$subQuery->when(
|
||||
$this->excludeRoles != null,
|
||||
// This aggregates a column role_count, which holds an information whether user holds specified role
|
||||
fn (Builder $subQuery) => $subQuery->leftJoinSub(
|
||||
$joinSub
|
||||
->select('uug.user_id')
|
||||
->whereIn('ug.role_id', $this->excludeRoles),
|
||||
'agr',
|
||||
'uug.user_id',
|
||||
'agr.user_id'
|
||||
)->whereNull('agr.user_id')
|
||||
)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the settings filter
|
||||
*/
|
||||
protected function buildSettingsFilter(Builder $query): self
|
||||
{
|
||||
foreach ($this->settings ?? [] as $name => $value) {
|
||||
$query->whereExists(
|
||||
fn (Builder $query) => $query->from('user_settings', 'us')
|
||||
->whereColumn('us.user_id', '=', 'u.user_id')
|
||||
->where('us.setting_name', '=', $name)
|
||||
->where('us.setting_value', '=', $value)
|
||||
);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the excluded submission stages filter
|
||||
*/
|
||||
protected function buildExcludedSubmissionStagesFilter(Builder $query): self
|
||||
{
|
||||
if ($this->excludeSubmissionStage === null) {
|
||||
return $this;
|
||||
}
|
||||
$query->whereExists(
|
||||
fn (Builder $query) => $query->from('user_user_groups', 'uug')
|
||||
->join('user_group_stage AS ugs', 'ugs.user_group_id', '=', 'uug.user_group_id')
|
||||
->leftJoin(
|
||||
'stage_assignments AS sa',
|
||||
fn (JoinClause $join) => $join->on('sa.user_id', '=', 'uug.user_id')
|
||||
->on('sa.user_group_id', '=', 'uug.user_group_id')
|
||||
->where('sa.submission_id', '=', $this->excludeSubmissionStage['submission_id'])
|
||||
)
|
||||
->whereColumn('uug.user_id', '=', 'u.user_id')
|
||||
->where('uug.user_group_id', '=', $this->excludeSubmissionStage['user_group_id'])
|
||||
->where('ugs.stage_id', '=', $this->excludeSubmissionStage['stage_id'])
|
||||
->whereNull('sa.user_group_id')
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the sub-editor filter
|
||||
*/
|
||||
protected function buildSubEditorFilter(Builder $query): self
|
||||
{
|
||||
$subEditorFilters = [
|
||||
Application::ASSOC_TYPE_SECTION => $this->assignedSectionIds,
|
||||
Application::ASSOC_TYPE_CATEGORY => $this->assignedCategoryIds,
|
||||
];
|
||||
foreach (array_filter($subEditorFilters, fn (?array $assocIds) => !empty($assocIds)) as $assocType => $assocIds) {
|
||||
$query->whereExists(
|
||||
fn (Builder $query) => $query->from('subeditor_submission_group', 'ssg')
|
||||
->whereColumn('ssg.user_id', '=', 'u.user_id')
|
||||
->where('ssg.assoc_type', '=', $assocType)
|
||||
->whereIn('ssg.assoc_id', $assocIds)
|
||||
);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the reviewer statistics and related filters
|
||||
*/
|
||||
protected function buildReviewerStatistics(Builder $query): self
|
||||
{
|
||||
if (!$this->includeReviewerData) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$disableSharedReviewerStatistics = (bool) Application::get()->getRequest()->getSite()->getData('disableSharedReviewerStatistics');
|
||||
$dateDiff = fn (string $dateA, string $dateB): string => DB::connection() instanceof MySqlConnection
|
||||
? "DATEDIFF({$dateA}, {$dateB})"
|
||||
: "DATE_PART('day', {$dateA} - {$dateB})";
|
||||
|
||||
$query->leftJoinSub(
|
||||
fn (Builder $query) => $query->from('review_assignments', 'ra')
|
||||
->groupBy('ra.reviewer_id')
|
||||
->select('ra.reviewer_id')
|
||||
->selectRaw('MAX(ra.date_assigned) AS last_assigned')
|
||||
->selectRaw('COUNT(CASE WHEN ra.date_completed IS NULL AND ra.declined = 0 AND ra.cancelled = 0 THEN 1 END) AS incomplete_count')
|
||||
->selectRaw('COUNT(CASE WHEN ra.date_completed IS NOT NULL AND ra.declined = 0 THEN 1 END) AS complete_count')
|
||||
->selectRaw('SUM(ra.declined) AS declined_count')
|
||||
->selectRaw('SUM(ra.cancelled) AS cancelled_count')
|
||||
->selectRaw('AVG(' . $dateDiff('ra.date_completed', 'ra.date_notified') . ') AS average_time')
|
||||
->selectRaw('AVG(ra.quality) AS reviewer_rating')
|
||||
->when($disableSharedReviewerStatistics, fn (Builder $query) => $query->join('submissions AS s', 'ra.submission_id', '=', 's.submission_id')
|
||||
->when($this->contextIds !== null, fn (Builder $query) => $query->whereIn('s.context_id', $this->contextIds))),
|
||||
'ra_stats',
|
||||
'u.user_id',
|
||||
'=',
|
||||
'ra_stats.reviewer_id'
|
||||
)
|
||||
// Select all statistics columns
|
||||
->addSelect('ra_stats.*')
|
||||
// Reviewer rating
|
||||
->when($this->reviewerRating !== null, fn (Builder $query) => $query->where('ra_stats.reviewer_rating', '>=', $this->reviewerRating))
|
||||
// Completed reviews
|
||||
->when($this->reviewsCompleted !== null, fn (Builder $query) => $query->where('ra_stats.complete_count', '>=', $this->reviewsCompleted))
|
||||
// Minimum active reviews
|
||||
->when(($minReviews = $this->reviewsActive[0] ?? null) !== null, fn (Builder $query) => $query->where('ra_stats.incomplete_count', '>=', $minReviews))
|
||||
// Maximum active reviews
|
||||
->when(($maxReviews = $this->reviewsActive[1] ?? null) !== null, fn (Builder $query) => $query->where('ra_stats.incomplete_count', '<=', $maxReviews))
|
||||
// Minimum days since last review assignment
|
||||
->when(
|
||||
($minDays = $this->daysSinceLastAssignment[0] ?? null) !== null,
|
||||
fn (Builder $query) => $query
|
||||
->where('ra_stats.last_assigned', '<=', Carbon::now()->subDays($minDays)->toDateString())
|
||||
)
|
||||
// Maximum days since last review assignment
|
||||
->when(
|
||||
($maxDays = $this->daysSinceLastAssignment[1] ?? null) !== null,
|
||||
fn (Builder $query) => $query
|
||||
->where('ra_stats.last_assigned', '>=', Carbon::now()->subDays($maxDays + 1)->toDateString()) // Add one to include upper bound
|
||||
)
|
||||
// Average days to complete review
|
||||
->when($this->averageCompletion !== null, fn (Builder $query) => $query->where('ra_stats.average_time', '<=', $this->averageCompletion));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the user search
|
||||
*/
|
||||
protected function buildSearchFilter(Builder $query): self
|
||||
{
|
||||
if ($this->searchPhrase === null || !strlen($searchPhrase = trim($this->searchPhrase))) {
|
||||
return $this;
|
||||
}
|
||||
// Settings where the search will be performed
|
||||
$settings = [Identity::IDENTITY_SETTING_GIVENNAME, Identity::IDENTITY_SETTING_FAMILYNAME, 'preferredPublicName', 'affiliation', 'biography', 'orcid'];
|
||||
// Break words by whitespace, trims and escapes "%" and "_"
|
||||
$words = array_map(fn (string $word) => '%' . addcslashes($word, '%_') . '%', PKPString::regexp_split('/\s+/', $searchPhrase));
|
||||
foreach ($words as $word) {
|
||||
$query->where(
|
||||
fn ($query) => $query->whereRaw('LOWER(u.username) LIKE LOWER(?)', [$word])
|
||||
->orWhereRaw('LOWER(u.email) LIKE LOWER(?)', [$word])
|
||||
->orWhereExists(
|
||||
fn (Builder $query) => $query->from('user_settings', 'us')
|
||||
->whereColumn('us.user_id', '=', 'u.user_id')
|
||||
->whereIn('us.setting_name', $settings)
|
||||
->whereRaw('LOWER(us.setting_value) LIKE LOWER(?)', [$word])
|
||||
)
|
||||
->orWhereExists(
|
||||
fn (Builder $query) => $query->from('user_interests', 'ui')
|
||||
->join('controlled_vocab_entry_settings AS cves', 'ui.controlled_vocab_entry_id', '=', 'cves.controlled_vocab_entry_id')
|
||||
->whereColumn('ui.user_id', '=', 'u.user_id')
|
||||
->whereRaw('LOWER(cves.setting_value) LIKE LOWER(?)', [$word])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the order by clause
|
||||
*/
|
||||
protected function buildOrderBy(Builder $query): self
|
||||
{
|
||||
$orderByFields = [self::ORDERBY_ID => 'u.user_id'];
|
||||
if ($orderByField = $orderByFields[$this->orderBy] ?? null) {
|
||||
$query->orderBy($orderByField, $this->orderDirection);
|
||||
return $this;
|
||||
}
|
||||
|
||||
$nameSettings = [self::ORDERBY_GIVENNAME => Identity::IDENTITY_SETTING_GIVENNAME, self::ORDERBY_FAMILYNAME => Identity::IDENTITY_SETTING_FAMILYNAME];
|
||||
if ($nameSettings[$this->orderBy] ?? null) {
|
||||
$locales = array_unique(
|
||||
empty($this->orderLocales)
|
||||
? [Locale::getLocale(), Application::get()->getRequest()->getSite()->getPrimaryLocale()]
|
||||
: array_values($this->orderLocales)
|
||||
);
|
||||
$sortedSettings = array_values($this->orderBy === self::ORDERBY_GIVENNAME ? $nameSettings : array_reverse($nameSettings));
|
||||
$query->orderBy(
|
||||
function (Builder $query) use ($sortedSettings, $locales): void {
|
||||
$query->fromSub(fn (Builder $query) => $query->from(null)->selectRaw(0), 'placeholder');
|
||||
$aliasesBySetting = [];
|
||||
foreach ($sortedSettings as $i => $setting) {
|
||||
$aliases = [];
|
||||
foreach ($locales as $j => $locale) {
|
||||
$aliases[] = $alias = "us_{$i}_{$j}";
|
||||
$query->leftJoin(
|
||||
"user_settings AS {$alias}",
|
||||
fn (JoinClause $join) => $join
|
||||
->on("{$alias}.user_id", '=', 'u.user_id')
|
||||
->where("{$alias}.setting_name", '=', $setting)
|
||||
->where("{$alias}.locale", '=', $locale)
|
||||
);
|
||||
}
|
||||
$aliasesBySetting[] = $aliases;
|
||||
}
|
||||
// Build a possibly long CONCAT(COALESCE(given_localeA, given_localeB, [...]), COALESCE(family_localeA, family_localeB, [...])
|
||||
$coalescedSettings = array_map(
|
||||
fn (array $aliases) => 'COALESCE(' . implode(', ', array_map(fn (string $alias) => "{$alias}.setting_value", $aliases)) . ", '')",
|
||||
$aliasesBySetting
|
||||
);
|
||||
$query->selectRaw('CONCAT(' . implode(', ', $coalescedSettings) . ')');
|
||||
},
|
||||
$this->orderDirection
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/DAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class DAO
|
||||
*
|
||||
* @ingroup user
|
||||
*
|
||||
* @see User
|
||||
*
|
||||
* @brief Operations for retrieving and modifying User objects.
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use PKP\core\DataObject;
|
||||
use PKP\core\EntityDAO;
|
||||
use PKP\identity\Identity;
|
||||
|
||||
/**
|
||||
* @template T of User
|
||||
* @extends EntityDAO<T>
|
||||
*/
|
||||
class DAO extends EntityDAO
|
||||
{
|
||||
/** @copydoc EntityDAO::$schema */
|
||||
public $schema = \PKP\services\PKPSchemaService::SCHEMA_USER;
|
||||
|
||||
/** @copydoc EntityDAO::$table */
|
||||
public $table = 'users';
|
||||
|
||||
/** @copydoc EntityDAO::$settingsTable */
|
||||
public $settingsTable = 'user_settings';
|
||||
|
||||
/** @copydoc EntityDAO::$primarykeyColumn */
|
||||
public $primaryKeyColumn = 'user_id';
|
||||
|
||||
/** @copydoc EntityDAO::$primaryTableColumns */
|
||||
public $primaryTableColumns = [
|
||||
'id' => 'user_id',
|
||||
'userName' => 'username',
|
||||
'password' => 'password',
|
||||
'email' => 'email',
|
||||
'url' => 'url',
|
||||
'phone' => 'phone',
|
||||
'mailingAddress' => 'mailing_address',
|
||||
'billingAddress' => 'billing_address',
|
||||
'country' => 'country',
|
||||
'locales' => 'locales',
|
||||
'gossip' => 'gossip',
|
||||
'dateLastEmail' => 'date_last_email',
|
||||
'dateRegistered' => 'date_registered',
|
||||
'dateValidated' => 'date_validated',
|
||||
'dateLastLogin' => 'date_last_login',
|
||||
'mustChangePassword' => 'must_change_password',
|
||||
'authStr' => 'auth_str',
|
||||
'disabled' => 'disabled',
|
||||
'disabledReason' => 'disabled_reason',
|
||||
'inlineHelp' => 'inline_help',
|
||||
];
|
||||
|
||||
/* These constants are used user-selectable search fields. */
|
||||
public const USER_FIELD_USERID = 'user_id';
|
||||
public const USER_FIELD_USERNAME = 'username';
|
||||
public const USER_FIELD_EMAIL = 'email';
|
||||
public const USER_FIELD_URL = 'url';
|
||||
public const USER_FIELD_INTERESTS = 'interests';
|
||||
public const USER_FIELD_AFFILIATION = 'affiliation';
|
||||
public const USER_FIELD_NONE = null;
|
||||
|
||||
/**
|
||||
* Construct a new User object.
|
||||
*/
|
||||
public function newDataObject()
|
||||
{
|
||||
return new User();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user
|
||||
*
|
||||
* @param bool $allowDisabled If true, allow fetching a disabled user.
|
||||
*/
|
||||
public function get(int $id, $allowDisabled = false): ?User
|
||||
{
|
||||
$row = DB::table($this->table)
|
||||
->where($this->primaryKeyColumn, $id)
|
||||
->first();
|
||||
/** @var User */
|
||||
$user = $row ? $this->fromRow($row) : null;
|
||||
if (!$allowDisabled && $user?->getDisabled()) {
|
||||
return null;
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user exists
|
||||
*/
|
||||
public function exists(int $id): bool
|
||||
{
|
||||
return DB::table($this->table)
|
||||
->where($this->primaryKeyColumn, '=', $id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of users matching the configured query
|
||||
* @return LazyCollection<int,T>
|
||||
*/
|
||||
public function getMany(Collector $query): LazyCollection
|
||||
{
|
||||
$rows = $query
|
||||
->getQueryBuilder()
|
||||
->get();
|
||||
|
||||
return LazyCollection::make(function () use ($rows, $query) {
|
||||
foreach ($rows as $row) {
|
||||
yield $row->user_id => $this->fromRow($row, $query->includeReviewerData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total count of users matching the configured query
|
||||
*/
|
||||
public function getCount(Collector $query): int
|
||||
{
|
||||
return $query
|
||||
->getQueryBuilder()
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of ids matching the configured query
|
||||
*
|
||||
* @return Collection<int,int>
|
||||
*/
|
||||
public function getIds(Collector $query): Collection
|
||||
{
|
||||
return $query
|
||||
->getQueryBuilder()
|
||||
->select('u.' . $this->primaryKeyColumn)
|
||||
->pluck('u.' . $this->primaryKeyColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by username.
|
||||
*
|
||||
* @return ?User
|
||||
*/
|
||||
public function getByUsername(string $username, bool $allowDisabled = false): ?User
|
||||
{
|
||||
$row = DB::table('users')
|
||||
->whereRaw('LOWER(username) = LOWER(?)', [$username])
|
||||
->when(!$allowDisabled, function ($query) {
|
||||
return $query->where('disabled', '=', false);
|
||||
})
|
||||
->get('user_id')
|
||||
->first();
|
||||
|
||||
return $row ? $this->get($row->user_id, $allowDisabled) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by email address.
|
||||
*
|
||||
* @return ?User
|
||||
*/
|
||||
public function getByEmail(string $email, bool $allowDisabled = false): ?User
|
||||
{
|
||||
$row = DB::table('users')
|
||||
->whereRaw('LOWER(email) = LOWER(?)', [$email])
|
||||
->when(!$allowDisabled, function ($query) {
|
||||
return $query->where('disabled', '=', false);
|
||||
})
|
||||
->get('user_id')
|
||||
->first();
|
||||
return $row ? $this->get($row->user_id, $allowDisabled) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user by the TDL ID (implicit authentication).
|
||||
*
|
||||
* @param string $authstr
|
||||
* @param bool $allowDisabled
|
||||
*
|
||||
* @return ?User
|
||||
*/
|
||||
public function getUserByAuthStr($authstr, $allowDisabled = true): ?User
|
||||
{
|
||||
$row = DB::table('users')
|
||||
->where('auth_str', $authstr)
|
||||
->when(!$allowDisabled, function ($query) {
|
||||
return $query->where('disabled', 0);
|
||||
})
|
||||
->get('user_id')
|
||||
->first();
|
||||
return $row ? $this->get($row->user_id) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by username and (encrypted) password.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password encrypted password
|
||||
* @param bool $allowDisabled
|
||||
*
|
||||
* @return ?User
|
||||
*/
|
||||
public function getUserByCredentials($username, $password, $allowDisabled = true): ?User
|
||||
{
|
||||
$row = DB::table('users')
|
||||
->where('username', '=', $username)
|
||||
->where('password', '=', $password)
|
||||
->when(!$allowDisabled, function ($query) {
|
||||
return $query->where('disabled', '=', false);
|
||||
})
|
||||
->get('user_id')
|
||||
->first();
|
||||
return $row ? $this->get($row->user_id) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc EntityDAO::fromRow
|
||||
*
|
||||
*/
|
||||
public function fromRow(object $row, bool $includeReviewerData = false): DataObject
|
||||
{
|
||||
$user = parent::fromRow($row);
|
||||
if ($includeReviewerData) {
|
||||
$user->setData('lastAssigned', $row->last_assigned);
|
||||
$user->setData('incompleteCount', (int) $row->incomplete_count);
|
||||
$user->setData('completeCount', (int) $row->complete_count);
|
||||
$user->setData('declinedCount', (int) $row->declined_count);
|
||||
$user->setData('cancelledCount', (int) $row->cancelled_count);
|
||||
$user->setData('averageTime', (int) $row->average_time);
|
||||
|
||||
// 0 values should return null. They represent a reviewer with no ratings
|
||||
if ($reviewerRating = $row->reviewer_rating) {
|
||||
$user->setData('reviewerRating', max(1, round($reviewerRating)));
|
||||
}
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc EntityDAO::_insert()
|
||||
*/
|
||||
public function insert(User $user): int
|
||||
{
|
||||
return parent::_insert($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc EntityDAO::update()
|
||||
*/
|
||||
public function update(User $user)
|
||||
{
|
||||
parent::_update($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc EntityDAO::_delete()
|
||||
*/
|
||||
public function delete(User $user)
|
||||
{
|
||||
parent::_delete($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user names when the site primary locale changes.
|
||||
*
|
||||
* @param string $oldLocale
|
||||
* @param string $newLocale
|
||||
*/
|
||||
public function changeSitePrimaryLocale($oldLocale, $newLocale)
|
||||
{
|
||||
// remove all empty user names in the new locale
|
||||
// so that we do not have to take care if we should insert or update them -- we can then only insert them if needed
|
||||
$settingNames = [Identity::IDENTITY_SETTING_GIVENNAME, Identity::IDENTITY_SETTING_FAMILYNAME, 'preferredPublicName'];
|
||||
foreach ($settingNames as $settingName) {
|
||||
DB::delete("DELETE from user_settings WHERE locale = ? AND setting_name = ? AND setting_value = ''", [$newLocale, $settingName]);
|
||||
}
|
||||
// get all names of all users in the new locale
|
||||
$result = DB::select(
|
||||
'SELECT DISTINCT us.user_id, usg.setting_value AS given_name, usf.setting_value AS family_name, usp.setting_value AS preferred_public_name
|
||||
FROM user_settings us
|
||||
LEFT JOIN user_settings usg ON (usg.user_id = us.user_id AND usg.locale = ? AND usg.setting_name = ?)
|
||||
LEFT JOIN user_settings usf ON (usf.user_id = us.user_id AND usf.locale = ? AND usf.setting_name = ?)
|
||||
LEFT JOIN user_settings usp ON (usp.user_id = us.user_id AND usp.locale = ? AND usp.setting_name = ?)',
|
||||
[$newLocale, Identity::IDENTITY_SETTING_GIVENNAME, $newLocale, Identity::IDENTITY_SETTING_FAMILYNAME, $newLocale, 'preferredPublicName']
|
||||
);
|
||||
foreach ($result as $row) {
|
||||
$userId = $row->user_id;
|
||||
if (empty($row->given_name) && empty($row->family_name) && empty($row->preferred_public_name)) {
|
||||
// if no user name exists in the new locale, insert them all
|
||||
foreach ($settingNames as $settingName) {
|
||||
DB::insert(
|
||||
'INSERT INTO user_settings (user_id, locale, setting_name, setting_value)
|
||||
SELECT DISTINCT us.user_id, ?, ?, us.setting_value
|
||||
FROM user_settings us
|
||||
WHERE us.setting_name = ? AND us.locale = ? AND us.user_id = ?',
|
||||
[$newLocale, $settingName, $settingName, $oldLocale, $userId]
|
||||
);
|
||||
}
|
||||
} elseif (empty($row->given_name)) {
|
||||
// if the given name does not exist in the new locale (but one of the other names do exist), insert it
|
||||
DB::insert(
|
||||
'INSERT INTO user_settings (user_id, locale, setting_name, setting_value)
|
||||
SELECT DISTINCT us.user_id, ?, ?, us.setting_value
|
||||
FROM user_settings us
|
||||
WHERE us.setting_name = ? AND us.locale = ? AND us.user_id = ?',
|
||||
[$newLocale, Identity::IDENTITY_SETTING_GIVENNAME, Identity::IDENTITY_SETTING_GIVENNAME, $oldLocale, $userId]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete unvalidated expired users
|
||||
*
|
||||
* @param Carbon $dateTillValid The dateTime till before which user will consider expired
|
||||
* @param array $excludableUsersId The users id to exclude form delete operation
|
||||
*
|
||||
* @return int The number rows affected by DB operation
|
||||
*/
|
||||
public function deleteUnvalidatedExpiredUsers(Carbon $dateTillValid, array $excludableUsersId = [])
|
||||
{
|
||||
$users = DB::table('users')
|
||||
->whereNull('date_validated')
|
||||
->whereNull('date_last_login')
|
||||
->where('date_registered', '<', $dateTillValid)
|
||||
->when(!empty($excludableUsersId), fn ($query) => $query->whereNotIn('id', $excludableUsersId))
|
||||
->get();
|
||||
|
||||
$userRepository = Repo::user();
|
||||
|
||||
$users->each(fn ($user) => $userRepository->delete($userRepository->get($user->user_id, true)));
|
||||
|
||||
return $users->count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/InterestDAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class InterestDAO
|
||||
*
|
||||
* @ingroup user
|
||||
*
|
||||
* @see User
|
||||
*
|
||||
* @brief Operations for retrieving and modifying a user's review interests.
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use PKP\controlledVocab\ControlledVocab;
|
||||
use PKP\controlledVocab\ControlledVocabDAO;
|
||||
use PKP\core\ArrayItemIterator;
|
||||
use PKP\db\DAORegistry;
|
||||
|
||||
class InterestDAO extends ControlledVocabDAO
|
||||
{
|
||||
public const CONTROLLED_VOCAB_INTEREST = 'interest';
|
||||
/**
|
||||
* Create or return the Controlled Vocabulary for interests
|
||||
*
|
||||
* @return ControlledVocab
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return parent::_build(self::CONTROLLED_VOCAB_INTEREST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of controlled vocabulary entry IDs (corresponding to interest keywords) attributed to a user
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUserInterestIds($userId)
|
||||
{
|
||||
$controlledVocab = $this->build();
|
||||
$result = $this->retrieveRange(
|
||||
'SELECT cve.controlled_vocab_entry_id FROM controlled_vocab_entries cve, user_interests ui WHERE cve.controlled_vocab_id = ? AND ui.controlled_vocab_entry_id = cve.controlled_vocab_entry_id AND ui.user_id = ?',
|
||||
[(int) $controlledVocab->getId(), (int) $userId]
|
||||
);
|
||||
|
||||
$ids = [];
|
||||
foreach ($result as $row) {
|
||||
$ids[] = $row->controlled_vocab_entry_id;
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of user IDs attributed to an interest
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUserIdsByInterest($interest)
|
||||
{
|
||||
$result = $this->retrieve(
|
||||
'
|
||||
SELECT ui.user_id
|
||||
FROM user_interests ui
|
||||
INNER JOIN controlled_vocab_entry_settings cves ON (ui.controlled_vocab_entry_id = cves.controlled_vocab_entry_id)
|
||||
WHERE cves.setting_name = ? AND cves.setting_value = ?',
|
||||
['interest', $interest]
|
||||
);
|
||||
|
||||
$returner = [];
|
||||
foreach ($result as $row) {
|
||||
$returner[] = $row->user_id;
|
||||
}
|
||||
return $returner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all user's interests
|
||||
*
|
||||
* @param string $filter (optional)
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getAllInterests($filter = null)
|
||||
{
|
||||
$controlledVocab = $this->build();
|
||||
$interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */
|
||||
$iterator = $interestEntryDao->getByControlledVocabId($controlledVocab->getId(), null, $filter);
|
||||
|
||||
// Sort by name.
|
||||
$interests = $iterator->toArray();
|
||||
usort($interests, function ($s1, $s2) {
|
||||
return strcmp($s1->getInterest(), $s2->getInterest());
|
||||
});
|
||||
|
||||
// Turn back into an iterator.
|
||||
return new ArrayItemIterator($interests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's set of interests
|
||||
*
|
||||
* @param array $interests
|
||||
* @param int $userId
|
||||
*/
|
||||
public function setUserInterests($interests, $userId)
|
||||
{
|
||||
// Remove duplicates
|
||||
$interests ??= [];
|
||||
$interests = array_unique($interests);
|
||||
|
||||
// Trim whitespace
|
||||
$interests = array_map('trim', $interests);
|
||||
|
||||
// Delete the existing interests association.
|
||||
$this->update(
|
||||
'DELETE FROM user_interests WHERE user_id = ?',
|
||||
[(int) $userId]
|
||||
);
|
||||
|
||||
$interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */
|
||||
$controlledVocab = $this->build();
|
||||
|
||||
// Store the new interests.
|
||||
foreach ((array) $interests as $interest) {
|
||||
$interestEntry = $interestEntryDao->getBySetting(
|
||||
$interest,
|
||||
$controlledVocab->getSymbolic(),
|
||||
$controlledVocab->getAssocId(),
|
||||
$controlledVocab->getAssocType(),
|
||||
$controlledVocab->getSymbolic()
|
||||
);
|
||||
|
||||
if (!$interestEntry) {
|
||||
$interestEntry = $interestEntryDao->newDataObject(); /** @var InterestEntry $interestEntry */
|
||||
$interestEntry->setInterest($interest);
|
||||
$interestEntry->setControlledVocabId($controlledVocab->getId());
|
||||
$interestEntry->setId($interestEntryDao->insertObject($interestEntry));
|
||||
}
|
||||
|
||||
$this->update(
|
||||
'INSERT INTO user_interests (user_id, controlled_vocab_entry_id) VALUES (?, ?)',
|
||||
[(int) $userId, (int) $interestEntry->getId()]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\InterestDAO', '\InterestDAO');
|
||||
define('CONTROLLED_VOCAB_INTEREST', InterestDAO::CONTROLLED_VOCAB_INTEREST);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/InterestEntry.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Interest
|
||||
*
|
||||
* @ingroup user
|
||||
*
|
||||
* @see InterestDAO
|
||||
*
|
||||
* @brief Basic class describing a reviewer interest
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
class InterestEntry extends \PKP\controlledVocab\ControlledVocabEntry
|
||||
{
|
||||
//
|
||||
// Get/set methods
|
||||
//
|
||||
|
||||
/**
|
||||
* Get the interest
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInterest()
|
||||
{
|
||||
return $this->getData('interest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the interest text
|
||||
*
|
||||
* @param string $interest
|
||||
*/
|
||||
public function setInterest($interest)
|
||||
{
|
||||
$this->setData('interest', $interest);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\InterestEntry', '\InterestEntry');
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/InterestEntryDAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class InterestEntryDAO
|
||||
*
|
||||
* @ingroup user
|
||||
*
|
||||
* @see User
|
||||
*
|
||||
* @brief Operations for retrieving and modifying a user's review interests.
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use PKP\controlledVocab\ControlledVocabEntryDAO;
|
||||
use PKP\db\DAOResultFactory;
|
||||
use PKP\db\DBResultRange;
|
||||
|
||||
class InterestEntryDAO extends ControlledVocabEntryDAO
|
||||
{
|
||||
/**
|
||||
* Construct a new data object corresponding to this DAO.
|
||||
*
|
||||
* @return InterestEntry
|
||||
*/
|
||||
public function newDataObject()
|
||||
{
|
||||
return new InterestEntry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of non-localized additional fields to store.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalFieldNames()
|
||||
{
|
||||
return ['interest'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an iterator of controlled vocabulary entries matching a
|
||||
* particular controlled vocabulary ID.
|
||||
*
|
||||
* @param int $controlledVocabId
|
||||
* @param DBResultRange $rangeInfo optional range information for result
|
||||
* @param string $filter Optional filter to match to beginnings of results
|
||||
*
|
||||
* @return DAOResultFactory<InterestEntry> Object containing matching CVE objects
|
||||
*/
|
||||
public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null)
|
||||
{
|
||||
$params = [(int) $controlledVocabId];
|
||||
if ($filter) {
|
||||
$params[] = 'interest';
|
||||
$params[] = $filter . '%';
|
||||
}
|
||||
|
||||
$result = $this->retrieveRange(
|
||||
'SELECT cve.*
|
||||
FROM controlled_vocab_entries cve
|
||||
JOIN user_interests ui ON (cve.controlled_vocab_entry_id = ui.controlled_vocab_entry_id)
|
||||
' . ($filter ? 'JOIN controlled_vocab_entry_settings cves ON (cves.controlled_vocab_entry_id = cve.controlled_vocab_entry_id)' : '') . '
|
||||
WHERE cve.controlled_vocab_id = ?
|
||||
' . ($filter ? 'AND cves.setting_name=? AND LOWER(cves.setting_value) LIKE LOWER(?)' : '') . '
|
||||
GROUP BY cve.controlled_vocab_entry_id
|
||||
ORDER BY seq',
|
||||
$params,
|
||||
$rangeInfo
|
||||
);
|
||||
|
||||
return new DAOResultFactory($result, $this, '_fromRow');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve controlled vocab entries matching a list of vocab entry IDs
|
||||
*
|
||||
* @param array $entryIds
|
||||
*
|
||||
* @return DAOResultFactory<InterestEntry>
|
||||
*/
|
||||
public function getByIds($entryIds)
|
||||
{
|
||||
$entryString = join(',', array_map('intval', $entryIds));
|
||||
|
||||
$result = $this->retrieve(
|
||||
'SELECT * FROM controlled_vocab_entries WHERE controlled_vocab_entry_id IN (' . $entryString . ')'
|
||||
);
|
||||
|
||||
return new DAOResultFactory($result, $this, '_fromRow');
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\InterestEntryDAO', '\InterestEntryDAO');
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/InterestManager.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class InterestManager
|
||||
*
|
||||
* @ingroup user
|
||||
*
|
||||
* @see InterestDAO
|
||||
*
|
||||
* @brief Handle user interest functions.
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use PKP\db\DAORegistry;
|
||||
|
||||
class InterestManager
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all interests for all users in the system
|
||||
*
|
||||
* @param string $filter
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllInterests($filter = null)
|
||||
{
|
||||
$interestDao = DAORegistry::getDAO('InterestDAO'); /** @var InterestDAO $interestDao */
|
||||
$interests = $interestDao->getAllInterests($filter);
|
||||
|
||||
$interestReturner = [];
|
||||
while ($interest = $interests->next()) {
|
||||
$interestReturner[] = $interest->getInterest();
|
||||
}
|
||||
|
||||
return $interestReturner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user reviewing interests. (Cached in memory for batch fetches.)
|
||||
*/
|
||||
public function getInterestsForUser(User $user): array
|
||||
{
|
||||
static $interestsCache = [];
|
||||
$interests = [];
|
||||
$interestDao = DAORegistry::getDAO('InterestDAO'); /** @var InterestDAO $interestDao */
|
||||
$interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */
|
||||
$controlledVocab = $interestDao->build();
|
||||
foreach ($interestDao->getUserInterestIds($user->getId()) as $interestEntryId) {
|
||||
/** @var InterestEntry */
|
||||
$interestEntry = $interestsCache[$interestEntryId] ??= $interestEntryDao->getById(
|
||||
$interestEntryId,
|
||||
$controlledVocab->getId()
|
||||
);
|
||||
if ($interestEntry) {
|
||||
$interests[] = $interestEntry->getInterest();
|
||||
}
|
||||
}
|
||||
|
||||
return $interests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a comma separated string of a user's interests
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInterestsString($user)
|
||||
{
|
||||
$interests = $this->getInterestsForUser($user);
|
||||
|
||||
return implode(', ', $interests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's interests
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function setInterestsForUser($user, $interests)
|
||||
{
|
||||
$interestDao = DAORegistry::getDAO('InterestDAO'); /** @var InterestDAO $interestDao */
|
||||
$interests = is_array($interests) ? $interests : (empty($interests) ? null : explode(',', $interests));
|
||||
$interestDao->setUserInterests($interests, $user->getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\InterestManager', '\InterestManager');
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* @defgroup lib_pkp_classes_user
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file lib/pkp/classes/user/Report.php
|
||||
*
|
||||
* Copyright (c) 2003-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file LICENSE.
|
||||
*
|
||||
* @class Report
|
||||
*
|
||||
* @ingroup lib_pkp_classes_user
|
||||
*
|
||||
* @brief Generates a CSV report with basic user information given a list of users and an output stream.
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\core\Request;
|
||||
use APP\facades\Repo;
|
||||
use PKP\facades\Locale;
|
||||
use PKP\userGroup\UserGroup;
|
||||
|
||||
class Report
|
||||
{
|
||||
/** @var iterable The report data source, should yield /User objects */
|
||||
private iterable $_dataSource;
|
||||
|
||||
private Request $_request;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param iterable $dataSource The data source, should yield /User objects
|
||||
*/
|
||||
public function __construct(iterable $dataSource)
|
||||
{
|
||||
$this->_dataSource = $dataSource;
|
||||
$this->_request = Application::get()->getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the report to the given output
|
||||
*
|
||||
* @param resource $output A ready to write stream
|
||||
*/
|
||||
public function serialize($output): void
|
||||
{
|
||||
// Adds BOM (byte order mark) to enforce the UTF-8 format
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
|
||||
// Outputs column headings
|
||||
fputcsv($output, $this->_getHeadings());
|
||||
|
||||
// Outputs each user
|
||||
foreach ($this->_dataSource as $user) {
|
||||
fputcsv($output, $this->_getDataRow($user));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the report headings
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function _getHeadings(): array
|
||||
{
|
||||
return [
|
||||
__('common.id'),
|
||||
__('user.givenName'),
|
||||
__('user.familyName'),
|
||||
__('user.email'),
|
||||
__('user.phone'),
|
||||
__('common.country'),
|
||||
__('common.mailingAddress'),
|
||||
__('user.dateRegistered'),
|
||||
__('common.updated'),
|
||||
...array_map(fn (UserGroup $userGroup) => $userGroup->getLocalizedName(), $this->_getUserGroups())
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the report row
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function _getDataRow(User $user): array
|
||||
{
|
||||
$userGroups = Repo::userGroup()->userUserGroups($user->getId());
|
||||
$groups = [];
|
||||
foreach ($userGroups as $userGroup) {
|
||||
$groups[$userGroup->getId()] = 0;
|
||||
}
|
||||
|
||||
return [
|
||||
$user->getId(),
|
||||
$user->getLocalizedGivenName(),
|
||||
$user->getFamilyName(Locale::getLocale()),
|
||||
$user->getEmail(),
|
||||
$user->getPhone(),
|
||||
$user->getCountryLocalized(),
|
||||
$user->getMailingAddress(),
|
||||
$user->getDateRegistered(),
|
||||
$user->getLocalizedData('dateProfileUpdated'),
|
||||
...array_map(fn (UserGroup $userGroup) => __(isset($groups[$userGroup->getId()]) ? 'common.yes' : 'common.no'), $this->_getUserGroups())
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the user groups
|
||||
*
|
||||
* @return UserGroup[]
|
||||
*/
|
||||
private function _getUserGroups(): array
|
||||
{
|
||||
static $cache;
|
||||
return $cache ??= Repo::userGroup()->getCollector()
|
||||
->filterByContextIds([$this->_request->getContext()->getId()])
|
||||
->getMany()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
/**
|
||||
* @file classes/user/Repository.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Repository
|
||||
*
|
||||
* @brief A repository to find and manage users.
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use APP\submission\Submission;
|
||||
use Carbon\Carbon;
|
||||
use PKP\context\Context;
|
||||
use PKP\context\SubEditorsDAO;
|
||||
use PKP\core\PKPApplication;
|
||||
use PKP\db\DAORegistry;
|
||||
use PKP\file\TemporaryFileDAO;
|
||||
use PKP\log\SubmissionEmailLogDAO;
|
||||
use PKP\log\SubmissionEventLogDAO;
|
||||
use PKP\note\NoteDAO;
|
||||
use PKP\notification\NotificationDAO;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\security\AccessKeyDAO;
|
||||
use PKP\security\Role;
|
||||
use PKP\security\RoleDAO;
|
||||
use PKP\session\SessionDAO;
|
||||
use PKP\stageAssignment\StageAssignmentDAO;
|
||||
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
|
||||
use PKP\submission\SubmissionCommentDAO;
|
||||
|
||||
class Repository
|
||||
{
|
||||
/** @var DAO $dao */
|
||||
public $dao;
|
||||
|
||||
/** @var string $schemaMap The name of the class to map this entity to its schema */
|
||||
public $schemaMap = maps\Schema::class;
|
||||
|
||||
public function __construct(DAO $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/** @copydoc DAO::newDataObject() */
|
||||
public function newDataObject(array $params = []): User
|
||||
{
|
||||
$object = $this->dao->newDataObject();
|
||||
if (!empty($params)) {
|
||||
$object->setAllData($params);
|
||||
}
|
||||
return $object;
|
||||
}
|
||||
|
||||
/** @copydoc DAO::get() */
|
||||
public function get(int $id, $allowDisabled = false): ?User
|
||||
{
|
||||
return $this->dao->get($id, $allowDisabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by API key.
|
||||
*/
|
||||
public function getByApiKey(string $apiKey): ?User
|
||||
{
|
||||
return $this->getCollector()
|
||||
->filterBySettings(['apiKey' => $apiKey])
|
||||
->getMany()
|
||||
->first();
|
||||
}
|
||||
|
||||
/** @copydoc DAO::get() */
|
||||
public function getByUsername(string $username, bool $allowDisabled = false): ?User
|
||||
{
|
||||
return $this->dao->getByUsername($username, $allowDisabled);
|
||||
}
|
||||
|
||||
/** @copydoc DAO::get() */
|
||||
public function getByEmail(string $email, bool $allowDisabled = false): ?User
|
||||
{
|
||||
return $this->dao->getByEmail($email, $allowDisabled);
|
||||
}
|
||||
|
||||
/** @copydoc DAO::getCollector() */
|
||||
public function getCollector(): Collector
|
||||
{
|
||||
return app(Collector::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the map class for mapping users to their schema
|
||||
*/
|
||||
public function getSchemaMap(): maps\Schema
|
||||
{
|
||||
return app('maps')->withExtensions($this->schemaMap);
|
||||
}
|
||||
|
||||
/** @copydoc DAO::insert() */
|
||||
public function add(User $user): int
|
||||
{
|
||||
$id = $this->dao->insert($user);
|
||||
Hook::call('User::add', [$user]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** @copydoc DAO::update() */
|
||||
public function edit(User $user, array $params = [])
|
||||
{
|
||||
$newUser = clone $user;
|
||||
$newUser->setAllData(array_merge($newUser->_data, $params));
|
||||
|
||||
Hook::call('User::edit', [$newUser, $user, $params]);
|
||||
|
||||
$this->dao->update($newUser);
|
||||
}
|
||||
|
||||
/** @copydoc DAO::delete */
|
||||
public function delete(User $user)
|
||||
{
|
||||
Hook::call('User::delete::before', [&$user]);
|
||||
|
||||
$this->dao->delete($user);
|
||||
|
||||
Hook::call('User::delete', [&$user]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the current user view and edit the gossip field for a user
|
||||
*
|
||||
* @param int $userId The user who's gossip field should be accessed
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canCurrentUserGossip($userId)
|
||||
{
|
||||
$request = Application::get()->getRequest();
|
||||
$context = $request->getContext();
|
||||
$contextId = $context ? $context->getId() : \PKP\core\PKPApplication::CONTEXT_ID_NONE;
|
||||
$currentUser = $request->getUser();
|
||||
|
||||
// Logged out users can never view gossip fields
|
||||
if (!$currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Users can never view their own gossip fields
|
||||
if ($currentUser->getId() === $userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$roleDao = DAORegistry::getDAO('RoleDAO'); /** @var RoleDAO $roleDao */
|
||||
// Only reviewers have gossip fields
|
||||
if (!$roleDao->userHasRole($contextId, $userId, Role::ROLE_ID_REVIEWER)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only admins, editors and subeditors can view gossip fields
|
||||
if (!$roleDao->userHasRole($contextId, $currentUser->getId(), [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this user access the requested workflow stage
|
||||
*
|
||||
* The user must have an assigned role in the specified stage or
|
||||
* be a manager or site admin that has no assigned role in the
|
||||
* submission.
|
||||
*
|
||||
* @param string $stageId One of the WORKFLOW_STAGE_ID_* constants.
|
||||
* @param string $workflowType Accessing the editorial or author workflow? PKPApplication::WORKFLOW_TYPE_*
|
||||
* @param array $userAccessibleStages User's assignments to the workflow stages. Application::ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES
|
||||
* @param array $userRoles User's roles in the context
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canUserAccessStage($stageId, $workflowType, $userAccessibleStages, $userRoles)
|
||||
{
|
||||
$workflowRoles = Application::get()->getWorkflowTypeRoles()[$workflowType];
|
||||
|
||||
if (array_key_exists($stageId, $userAccessibleStages)
|
||||
&& !empty(array_intersect($workflowRoles, $userAccessibleStages[$stageId]))) {
|
||||
return true;
|
||||
}
|
||||
if (empty($userAccessibleStages) && count(array_intersect([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN], $userRoles))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user roles which give access to (certain) submission workflow stages
|
||||
* returns [
|
||||
* stage ID => [role IDs]
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
public function getAccessibleWorkflowStages(int $userId, int $contextId, Submission $submission, ?array $userRoleIds = null): array
|
||||
{
|
||||
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
|
||||
$stageAssignmentsResult = $stageAssignmentDao->getBySubmissionAndUserIdAndStageId($submission->getId(), $userId);
|
||||
|
||||
if (is_null($userRoleIds)) {
|
||||
$roleDao = DAORegistry::getDAO('RoleDAO'); /** @var RoleDAO $roleDao */
|
||||
$userRoles = $roleDao->getByUserIdGroupedByContext($userId);
|
||||
|
||||
$userRoleIds = [];
|
||||
if (array_key_exists($contextId, $userRoles)) {
|
||||
$contextRoles = $userRoles[$contextId];
|
||||
|
||||
foreach ($contextRoles as $contextRole) { /** @var Role $userRole */
|
||||
$userRoleIds[] = $contextRole->getRoleId();
|
||||
}
|
||||
}
|
||||
|
||||
// Has admin role?
|
||||
if ($contextId != PKPApplication::CONTEXT_ID_NONE &&
|
||||
array_key_exists(PKPApplication::CONTEXT_ID_NONE, $userRoles) &&
|
||||
in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles[PKPApplication::CONTEXT_ID_NONE])
|
||||
) {
|
||||
$userRoleIds[] = Role::ROLE_ID_SITE_ADMIN;
|
||||
}
|
||||
}
|
||||
|
||||
$accessibleWorkflowStages = [];
|
||||
|
||||
// Assigned users have access based on their assignment
|
||||
while ($stageAssignment = $stageAssignmentsResult->next()) {
|
||||
$userGroup = Repo::userGroup()->get($stageAssignment->getUserGroupId());
|
||||
$roleId = $userGroup->getRoleId();
|
||||
|
||||
// Check global user roles within the context, e.g., user can be assigned in the role, which was revoked
|
||||
if (!in_array($roleId, $userRoleIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$accessibleWorkflowStages[$stageAssignment->getStageId()][] = $roleId;
|
||||
}
|
||||
|
||||
// Managers and admin have access if not assigned to the submission or are assigned in a revoked role
|
||||
$managerRoles = array_intersect($userRoleIds, [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER]);
|
||||
if (empty($accessibleWorkflowStages) && !empty($managerRoles)) {
|
||||
$workflowStages = Application::getApplicationStages();
|
||||
foreach ($workflowStages as $stageId) {
|
||||
$accessibleWorkflowStages[$stageId] = $managerRoles;
|
||||
}
|
||||
}
|
||||
|
||||
return $accessibleWorkflowStages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a filtered user report instance
|
||||
*
|
||||
* @param array $args
|
||||
* - @option int[] contextIds Context IDs (required)
|
||||
* - @option int[] userGroupIds List of user groups (all groups by default)
|
||||
*/
|
||||
public function getReport(array $args): Report
|
||||
{
|
||||
$dataSource = $this->getCollector()
|
||||
->filterByUserGroupIds($args['userGroupIds'] ?? null)
|
||||
->filterByContextIds($args['contextIds'] ?? [])
|
||||
->getMany();
|
||||
|
||||
$report = new Report($dataSource);
|
||||
|
||||
Hook::call('User::getReport', [$report]);
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
public function getRolesOverview(Collector $collector)
|
||||
{
|
||||
$result = [
|
||||
[
|
||||
'id' => 'total',
|
||||
'name' => 'stats.allUsers',
|
||||
'value' => $this->dao->getCount($collector),
|
||||
],
|
||||
];
|
||||
|
||||
$roleNames = Application::get()->getRoleNames();
|
||||
|
||||
foreach ($roleNames as $roleId => $roleName) {
|
||||
$result[] = [
|
||||
'id' => $roleId,
|
||||
'name' => $roleName,
|
||||
'value' => $this->dao->getCount($collector->filterByRoleIds([$roleId])),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge user accounts and delete the old user account.
|
||||
*
|
||||
* @param int $oldUserId The user ID to remove
|
||||
* @param int $newUserId The user ID to receive all "assets" (i.e. submissions) from old user
|
||||
*/
|
||||
public function mergeUsers(int $oldUserId, int $newUserId)
|
||||
{
|
||||
// Need both user ids for merge
|
||||
if (empty($oldUserId) || empty($newUserId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Hook::call('UserAction::mergeUsers', [&$oldUserId, &$newUserId]);
|
||||
|
||||
$submissionFiles = Repo::submissionFile()
|
||||
->getCollector()
|
||||
->filterByUploaderUserIds([$oldUserId])
|
||||
->includeDependentFiles()
|
||||
->getMany();
|
||||
|
||||
foreach ($submissionFiles as $submissionFile) {
|
||||
Repo::submissionFile()->edit($submissionFile, ['uploaderUserId' => $newUserId]);
|
||||
}
|
||||
|
||||
$noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
|
||||
$notes = $noteDao->getByUserId($oldUserId);
|
||||
while ($note = $notes->next()) {
|
||||
$note->setUserId($newUserId);
|
||||
$noteDao->updateObject($note);
|
||||
}
|
||||
|
||||
Repo::decision()->dao->reassignDecisions($oldUserId, $newUserId);
|
||||
|
||||
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
|
||||
foreach ($reviewAssignmentDao->getByUserId($oldUserId) as $reviewAssignment) {
|
||||
$reviewAssignment->setReviewerId($newUserId);
|
||||
$reviewAssignmentDao->updateObject($reviewAssignment);
|
||||
}
|
||||
|
||||
$submissionEmailLogDao = DAORegistry::getDAO('SubmissionEmailLogDAO'); /** @var SubmissionEmailLogDAO $submissionEmailLogDao */
|
||||
$submissionEmailLogDao->changeUser($oldUserId, $newUserId);
|
||||
Repo::eventLog()->dao->changeUser($oldUserId, $newUserId);
|
||||
|
||||
$submissionCommentDao = DAORegistry::getDAO('SubmissionCommentDAO'); /** @var SubmissionCommentDAO $submissionCommentDao */
|
||||
$submissionComments = $submissionCommentDao->getByUserId($oldUserId);
|
||||
|
||||
while ($submissionComment = $submissionComments->next()) {
|
||||
$submissionComment->setAuthorId($newUserId);
|
||||
$submissionCommentDao->updateObject($submissionComment);
|
||||
}
|
||||
|
||||
$accessKeyDao = DAORegistry::getDAO('AccessKeyDAO'); /** @var AccessKeyDAO $accessKeyDao */
|
||||
$accessKeyDao->transferAccessKeys($oldUserId, $newUserId);
|
||||
|
||||
$notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
|
||||
$notificationDao->transferNotifications($oldUserId, $newUserId);
|
||||
|
||||
// Delete the old user and associated info.
|
||||
$sessionDao = DAORegistry::getDAO('SessionDAO'); /** @var SessionDAO $sessionDao */
|
||||
$sessionDao->deleteByUserId($oldUserId);
|
||||
$temporaryFileDao = DAORegistry::getDAO('TemporaryFileDAO'); /** @var TemporaryFileDAO $temporaryFileDao */
|
||||
$temporaryFileDao->deleteByUserId($oldUserId);
|
||||
$subEditorsDao = DAORegistry::getDAO('SubEditorsDAO'); /** @var SubEditorsDAO $subEditorsDao */
|
||||
$subEditorsDao->deleteByUserId($oldUserId);
|
||||
|
||||
// Transfer old user's roles
|
||||
$userGroups = Repo::userGroup()->userUserGroups($oldUserId);
|
||||
foreach ($userGroups as $userGroup) {
|
||||
if (!Repo::userGroup()->userInGroup($newUserId, $userGroup->getId())) {
|
||||
Repo::userGroup()->assignUserToGroup($newUserId, $userGroup->getId());
|
||||
}
|
||||
}
|
||||
|
||||
Repo::userGroup()->deleteAssignmentsByUserId($oldUserId);
|
||||
|
||||
// Transfer stage assignments.
|
||||
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
|
||||
$stageAssignments = $stageAssignmentDao->getByUserId($oldUserId);
|
||||
while ($stageAssignment = $stageAssignments->next()) {
|
||||
$duplicateAssignments = $stageAssignmentDao->getBySubmissionAndStageId($stageAssignment->getSubmissionId(), null, $stageAssignment->getUserGroupId(), $newUserId);
|
||||
if (!$duplicateAssignments->next()) {
|
||||
// If no similar assignments already exist, transfer this one.
|
||||
$stageAssignment->setUserId($newUserId);
|
||||
$stageAssignmentDao->updateObject($stageAssignment);
|
||||
} else {
|
||||
// There's already a stage assignment for the new user; delete.
|
||||
$stageAssignmentDao->deleteObject($stageAssignment);
|
||||
}
|
||||
}
|
||||
|
||||
$this->delete($this->get($oldUserId, true));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user object from the Context contact details
|
||||
*/
|
||||
public function getUserFromContextContact(Context $context): User
|
||||
{
|
||||
$contextUser = $this->newDataObject();
|
||||
$supportedLocales = $context->getSupportedFormLocales();
|
||||
$contextUser->setData('email', $context->getData('contactEmail'));
|
||||
$contextUser->setData('givenName', array_fill_keys($supportedLocales, $context->getData('contactName')));
|
||||
return $contextUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete unvalidated expired users
|
||||
*
|
||||
* @param Carbon $dateTillValid The dateTime till before which user will consider expired
|
||||
* @param array $excludableUsersId The users id to exclude form delete operation
|
||||
*
|
||||
* @return int The number rows affected by DB operation
|
||||
*/
|
||||
public function deleteUnvalidatedExpiredUsers(Carbon $dateTillValid, array $excludableUsersId = [])
|
||||
{
|
||||
return $this->dao->deleteUnvalidatedExpiredUsers($dateTillValid, $excludableUsersId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
/**
|
||||
* @defgroup user User
|
||||
* Implements data objects and DAOs concerned with managing user accounts.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/user/User.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class User
|
||||
*
|
||||
* @ingroup user
|
||||
*
|
||||
* @brief Basic class describing users existing in the system.
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use PKP\db\DAORegistry;
|
||||
use PKP\identity\Identity;
|
||||
use PKP\security\RoleDAO;
|
||||
|
||||
class User extends Identity
|
||||
{
|
||||
/** @var array Roles assigned to this user grouped by context */
|
||||
protected $_roles = [];
|
||||
|
||||
//
|
||||
// Get/set methods
|
||||
//
|
||||
|
||||
/**
|
||||
* Get username.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->getData('userName');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set username.
|
||||
*
|
||||
* @param string $username
|
||||
*/
|
||||
public function setUsername($username)
|
||||
{
|
||||
$this->setData('userName', $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get implicit auth ID string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthStr()
|
||||
{
|
||||
return $this->getData('authStr');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Shib ID string for this user.
|
||||
*
|
||||
* @param string $authStr
|
||||
*/
|
||||
public function setAuthStr($authStr)
|
||||
{
|
||||
$this->setData('authStr', $authStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized user signature.
|
||||
*/
|
||||
public function getLocalizedSignature()
|
||||
{
|
||||
return $this->getLocalizedData('signature');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email signature.
|
||||
*
|
||||
* @param string $locale
|
||||
*
|
||||
* @return string|array<string,string>
|
||||
*/
|
||||
public function getSignature($locale)
|
||||
{
|
||||
return $this->getData('signature', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set signature.
|
||||
*
|
||||
* @param string $signature
|
||||
* @param string $locale
|
||||
*/
|
||||
public function setSignature($signature, $locale)
|
||||
{
|
||||
$this->setData('signature', $signature, $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get password (encrypted).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->getData('password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password (assumed to be already encrypted).
|
||||
*
|
||||
* @param string $password
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->setData('password', $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get phone number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhone()
|
||||
{
|
||||
return $this->getData('phone');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set phone number.
|
||||
*
|
||||
* @param string $phone
|
||||
*/
|
||||
public function setPhone($phone)
|
||||
{
|
||||
$this->setData('phone', $phone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mailing address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMailingAddress()
|
||||
{
|
||||
return $this->getData('mailingAddress');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mailing address.
|
||||
*
|
||||
* @param string $mailingAddress
|
||||
*/
|
||||
public function setMailingAddress($mailingAddress)
|
||||
{
|
||||
$this->setData('mailingAddress', $mailingAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get billing address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBillingAddress()
|
||||
{
|
||||
return $this->getData('billingAddress');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set billing address.
|
||||
*
|
||||
* @param string $billingAddress
|
||||
*/
|
||||
public function setBillingAddress($billingAddress)
|
||||
{
|
||||
$this->setData('billingAddress', $billingAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's interests displayed as a comma-separated string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInterestString()
|
||||
{
|
||||
$interestManager = new InterestManager();
|
||||
return $interestManager->getInterestsString($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user gossip.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGossip()
|
||||
{
|
||||
return $this->getData('gossip');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user gossip.
|
||||
*
|
||||
* @param string $gossip
|
||||
*/
|
||||
public function setGossip($gossip)
|
||||
{
|
||||
$this->setData('gossip', $gossip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's working languages.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLocales()
|
||||
{
|
||||
$locales = $this->getData('locales');
|
||||
return $locales ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user's working languages.
|
||||
*
|
||||
* @param array $locales
|
||||
*/
|
||||
public function setLocales($locales)
|
||||
{
|
||||
$this->setData('locales', $locales);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date user last sent an email.
|
||||
*
|
||||
* @return string (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function getDateLastEmail()
|
||||
{
|
||||
return $this->getData('dateLastEmail');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date user last sent an email.
|
||||
*
|
||||
* @param string $dateLastEmail (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function setDateLastEmail($dateLastEmail)
|
||||
{
|
||||
$this->setData('dateLastEmail', $dateLastEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date user registered with the site.
|
||||
*
|
||||
* @return string (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function getDateRegistered()
|
||||
{
|
||||
return $this->getData('dateRegistered');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date user registered with the site.
|
||||
*
|
||||
* @param string $dateRegistered (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function setDateRegistered($dateRegistered)
|
||||
{
|
||||
$this->setData('dateRegistered', $dateRegistered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date user email was validated with the site.
|
||||
*
|
||||
* @return string (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function getDateValidated()
|
||||
{
|
||||
return $this->getData('dateValidated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date user email was validated with the site.
|
||||
*
|
||||
* @param string $dateValidated (YYYY-MM-DD HH:MM:SS)
|
||||
*/
|
||||
public function setDateValidated($dateValidated)
|
||||
{
|
||||
$this->setData('dateValidated', $dateValidated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date user last logged in to the site.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateLastLogin()
|
||||
{
|
||||
return $this->getData('dateLastLogin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date user last logged in to the site.
|
||||
*
|
||||
* @param string $dateLastLogin
|
||||
*/
|
||||
public function setDateLastLogin($dateLastLogin)
|
||||
{
|
||||
$this->setData('dateLastLogin', $dateLastLogin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user must change their password on their next login.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getMustChangePassword()
|
||||
{
|
||||
return $this->getData('mustChangePassword');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not user must change their password on their next login.
|
||||
*
|
||||
* @param bool $mustChangePassword
|
||||
*/
|
||||
public function setMustChangePassword($mustChangePassword)
|
||||
{
|
||||
$this->setData('mustChangePassword', $mustChangePassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is disabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getDisabled()
|
||||
{
|
||||
return $this->getData('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not user is disabled.
|
||||
*
|
||||
* @param bool $disabled
|
||||
*/
|
||||
public function setDisabled($disabled)
|
||||
{
|
||||
$this->setData('disabled', $disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reason the user was disabled.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisabledReason()
|
||||
{
|
||||
return $this->getData('disabledReason');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reason the user is disabled.
|
||||
*
|
||||
* @param string $reasonDisabled
|
||||
*/
|
||||
public function setDisabledReason($reasonDisabled)
|
||||
{
|
||||
$this->setData('disabledReason', $reasonDisabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inline help display status for this user.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getInlineHelp()
|
||||
{
|
||||
return $this->getData('inlineHelp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inline help display status for this user.
|
||||
*
|
||||
* @param int $inlineHelp
|
||||
*/
|
||||
public function setInlineHelp($inlineHelp)
|
||||
{
|
||||
$this->setData('inlineHelp', $inlineHelp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this user has a role in a context
|
||||
*
|
||||
* @param int|array $roles Role(s) to check for
|
||||
* @param int $contextId The context to check for roles in.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRole($roles, $contextId)
|
||||
{
|
||||
$contextRoles = $this->getRoles($contextId);
|
||||
|
||||
if (empty($contextRoles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_array($roles)) {
|
||||
$roles = [$roles];
|
||||
}
|
||||
|
||||
foreach ($contextRoles as $contextRole) {
|
||||
if (in_array((int) $contextRole->getId(), $roles)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this user's roles in a context
|
||||
*
|
||||
* @param int $contextId The context to retrieve roles in.
|
||||
* @param bool $noCache Force the roles to be retrieved from the database
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRoles($contextId, $noCache = false)
|
||||
{
|
||||
if ($noCache || empty($this->_roles[$contextId])) {
|
||||
$userRolesDao = DAORegistry::getDAO('RoleDAO'); /** @var RoleDAO $userRolesDao */
|
||||
$this->setRoles($userRolesDao->getByUserId($this->getId(), $contextId), $contextId);
|
||||
}
|
||||
|
||||
return $this->_roles[$contextId] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this user's roles in a context
|
||||
*
|
||||
* @param array $roles The roles to assign this user
|
||||
* @param int $contextId The context to assign these roles
|
||||
*/
|
||||
public function setRoles($roles, $contextId)
|
||||
{
|
||||
$this->_roles[$contextId] = $roles;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\User', '\User');
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/UserStageAssignmentDAO.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class UserStageAssignmentDAO
|
||||
*
|
||||
* @ingroup user
|
||||
*
|
||||
* @brief Operations for users as related to their stage assignments
|
||||
*/
|
||||
|
||||
namespace PKP\user;
|
||||
|
||||
use APP\core\Application;
|
||||
use PKP\db\DAO;
|
||||
use PKP\db\DAOResultFactory;
|
||||
use PKP\db\DBResultRange;
|
||||
use PKP\facades\Locale;
|
||||
use PKP\identity\Identity;
|
||||
|
||||
/**
|
||||
* @deprecated 3.4.0
|
||||
* @see \PKP\user\Collector
|
||||
*/
|
||||
class UserStageAssignmentDAO extends DAO
|
||||
{
|
||||
/**
|
||||
* Delete a stage assignment by Id.
|
||||
*
|
||||
* @param int $assignmentId
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteAssignment($assignmentId)
|
||||
{
|
||||
return $this->update('DELETE FROM stage_assignments WHERE stage_assignment_id = ?', [(int) $assignmentId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of users of a user group not assigned to a given submission stage and matching the specified settings.
|
||||
* @todo Not working and not being used, probably can be removed
|
||||
*
|
||||
* @param int $submissionId
|
||||
* @param int $stageId
|
||||
* @param int $userGroupId
|
||||
* @param string|null $name Partial string match with user name
|
||||
* @param ?DBResultRange $rangeInfo
|
||||
*
|
||||
* @return DAOResultFactory Object
|
||||
*/
|
||||
public function filterUsersNotAssignedToStageInUserGroup($submissionId, $stageId, $userGroupId, $name = null, $rangeInfo = null)
|
||||
{
|
||||
$site = Application::get()->getRequest()->getSite();
|
||||
$primaryLocale = $site->getPrimaryLocale();
|
||||
$locale = Locale::getLocale();
|
||||
$params = [
|
||||
(int) $submissionId,
|
||||
(int) $stageId,
|
||||
Identity::IDENTITY_SETTING_GIVENNAME, $primaryLocale,
|
||||
Identity::IDENTITY_SETTING_FAMILYNAME, $primaryLocale,
|
||||
Identity::IDENTITY_SETTING_GIVENNAME, $locale,
|
||||
Identity::IDENTITY_SETTING_FAMILYNAME, $locale,
|
||||
(int) $userGroupId,
|
||||
];
|
||||
if ($name !== null) {
|
||||
$params = array_merge($params, array_fill(0, 6, '%' . (string) $name . '%'));
|
||||
}
|
||||
$result = $this->retrieveRange(
|
||||
$sql = 'SELECT u.*
|
||||
FROM users u
|
||||
LEFT JOIN user_user_groups uug ON (u.user_id = uug.user_id)
|
||||
LEFT JOIN stage_assignments s ON (s.user_id = uug.user_id AND s.user_group_id = uug.user_group_id AND s.submission_id = ?)
|
||||
JOIN user_group_stage ugs ON (uug.user_group_id = ugs.user_group_id AND ugs.stage_id = ?)
|
||||
LEFT JOIN user_settings usgs_pl ON (usgs_pl.user_id = u.user_id AND usgs_pl.setting_name = ? AND usgs_pl.locale = ?)
|
||||
LEFT JOIN user_settings usfs_pl ON (usfs_pl.user_id = u.user_id AND usfs_pl.setting_name = ? AND usfs_pl.locale = ?)
|
||||
LEFT JOIN user_settings usgs_l ON (usgs_l.user_id = u.user_id AND usgs_l.setting_name = ? AND usgs_l.locale = ?)
|
||||
LEFT JOIN user_settings usfs_l ON (usfs_l.user_id = u.user_id AND usfs_l.setting_name = ? AND usfs_l.locale = ?)
|
||||
|
||||
WHERE uug.user_group_id = ? AND
|
||||
s.user_group_id IS NULL'
|
||||
. ($name !== null ? ' AND (usgs_pl.setting_value LIKE ? OR usgs_l.setting_value LIKE ? OR usfs_pl.setting_value LIKE ? OR usfs_l.setting_value LIKE ? OR u.username LIKE ? OR u.email LIKE ?)' : '')
|
||||
. ' ORDER BY COALESCE(usfs_l.setting_value, usfs_pl.setting_value)',
|
||||
$params,
|
||||
$rangeInfo
|
||||
);
|
||||
return new DAOResultFactory($result, $this, '_returnUserFromRowWithData', [], $sql, $params, $rangeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\UserStageAssignmentDAO', '\UserStageAssignmentDAO');
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/APIProfileForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class APIProfileForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to edit user's API key settings.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\notification\NotificationManager;
|
||||
use APP\template\TemplateManager;
|
||||
use Firebase\JWT\JWT;
|
||||
use PKP\config\Config;
|
||||
use PKP\notification\PKPNotification;
|
||||
use PKP\user\User;
|
||||
|
||||
class APIProfileForm extends BaseProfileForm
|
||||
{
|
||||
public const API_KEY_NEW = 1;
|
||||
public const API_KEY_DELETE = 0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
parent::__construct('user/apiProfileForm.tpl', $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::initData()
|
||||
*/
|
||||
public function initData()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$this->setData('apiKeyEnabled', (bool) $user->getData('apiKeyEnabled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
parent::readInputData();
|
||||
|
||||
$this->readUserVars([
|
||||
'apiKeyEnabled',
|
||||
'generateApiKey',
|
||||
'apiKeyAction',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the form to edit user's API key settings.
|
||||
*
|
||||
* @see BaseProfileForm::fetch
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*
|
||||
* @return string JSON-encoded form contents.
|
||||
*/
|
||||
public function fetch($request, $template = null, $display = false)
|
||||
{
|
||||
$user = $request->getUser();
|
||||
$secret = Config::getVar('security', 'api_key_secret', '');
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
|
||||
if ($secret === '') {
|
||||
$this->handleOnMissingAPISecret($templateMgr, $user);
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
$templateMgr->assign(
|
||||
$user->getData('apiKey') ? [
|
||||
'apiKey' => JWT::encode($user->getData('apiKey'), $secret, 'HS256'),
|
||||
'apiKeyAction' => self::API_KEY_DELETE,
|
||||
'apiKeyActionTextKey' => 'user.apiKey.remove',
|
||||
] : [
|
||||
'apiKeyAction' => self::API_KEY_NEW,
|
||||
'apiKeyActionTextKey' => 'user.apiKey.generate',
|
||||
]
|
||||
);
|
||||
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$request = Application::get()->getRequest();
|
||||
$user = $request->getUser();
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
|
||||
if (Config::getVar('security', 'api_key_secret', '') === '') {
|
||||
$this->handleOnMissingAPISecret($templateMgr, $user);
|
||||
parent::execute(...$functionArgs);
|
||||
}
|
||||
|
||||
$apiKeyAction = (int)$this->getData('apiKeyAction');
|
||||
|
||||
$user->setData('apiKeyEnabled', $apiKeyAction === self::API_KEY_NEW ? 1 : null);
|
||||
$user->setData('apiKey', $apiKeyAction === self::API_KEY_NEW ? sha1(time()) : null);
|
||||
|
||||
$this->setData('apiKeyAction', (int)!$apiKeyAction);
|
||||
|
||||
parent::execute(...$functionArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle on missing API secret
|
||||
*
|
||||
*
|
||||
*/
|
||||
protected function handleOnMissingAPISecret(TemplateManager $templateMgr, User $user): void
|
||||
{
|
||||
$notificationManager = new NotificationManager();
|
||||
$notificationManager->createTrivialNotification(
|
||||
$user->getId(),
|
||||
PKPNotification::NOTIFICATION_TYPE_WARNING,
|
||||
[
|
||||
'contents' => __('user.apiKey.secretRequired'),
|
||||
]
|
||||
);
|
||||
$templateMgr->assign([
|
||||
'apiSecretMissing' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\APIProfileForm', '\APIProfileForm');
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/BaseProfileForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class BaseProfileForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Base form to edit an aspect of user profile.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use PKP\form\Form;
|
||||
use PKP\session\SessionManager;
|
||||
use PKP\user\User;
|
||||
|
||||
abstract class BaseProfileForm extends Form
|
||||
{
|
||||
/** @var User */
|
||||
public $_user;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $template
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct($template, $user)
|
||||
{
|
||||
parent::__construct($template);
|
||||
|
||||
$this->_user = $user;
|
||||
assert(isset($user));
|
||||
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorPost($this));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCSRF($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user associated with this profile
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
parent::execute(...$functionArgs);
|
||||
|
||||
$request = Application::get()->getRequest();
|
||||
$user = $request->getUser();
|
||||
Repo::user()->edit($user);
|
||||
|
||||
if ($functionArgs['emailUpdated'] ?? false) {
|
||||
$sessionManager = SessionManager::getManager();
|
||||
$session = $sessionManager->getUserSession();
|
||||
|
||||
if ($session->getSessionVar('email')) {
|
||||
$session->setSessionVar('email', $user->getEmail());
|
||||
}
|
||||
|
||||
$sessionManager->invalidateSessions($user->getId(), $sessionManager->getUserSession()->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\BaseProfileForm', '\BaseProfileForm');
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/ChangePasswordForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class ChangePasswordForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to change a user's password.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\form\Form;
|
||||
use PKP\security\Validation;
|
||||
use PKP\site\Site;
|
||||
use PKP\user\User;
|
||||
|
||||
class ChangePasswordForm extends Form
|
||||
{
|
||||
/** @var User */
|
||||
public $_user;
|
||||
|
||||
/** @var Site */
|
||||
public $_site;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Site $site
|
||||
*/
|
||||
public function __construct($user, $site)
|
||||
{
|
||||
parent::__construct('user/changePassword.tpl');
|
||||
|
||||
$this->_user = $user;
|
||||
$this->_site = $site;
|
||||
|
||||
// Validation checks for this form
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'oldPassword', 'required', 'user.profile.form.oldPasswordInvalid', function ($password) use ($user) {
|
||||
return Validation::checkCredentials($user->getUsername(), $password);
|
||||
}));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorLength($this, 'password', 'required', 'user.register.form.passwordLengthRestriction', '>=', $site->getMinPasswordLength()));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'password', 'required', 'user.profile.form.newPasswordRequired'));
|
||||
$form = $this;
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'password', 'required', 'user.register.form.passwordsDoNotMatch', function ($password) use ($form) {
|
||||
return $password == $form->getData('password2');
|
||||
}));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'password', 'required', 'user.profile.form.passwordSameAsOld', function ($password) use ($form) {
|
||||
return $password != $form->getData('oldPassword');
|
||||
}));
|
||||
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorPost($this));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCSRF($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user associated with this password
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site
|
||||
*/
|
||||
public function getSite()
|
||||
{
|
||||
return $this->_site;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::fetch
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function fetch($request, $template = null, $display = false)
|
||||
{
|
||||
$templateMgr = TemplateManager::getManager();
|
||||
$templateMgr->assign([
|
||||
'minPasswordLength' => $this->getSite()->getMinPasswordLength(),
|
||||
'username' => $this->getUser()->getUsername(),
|
||||
]);
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
$this->readUserVars(['oldPassword', 'password', 'password2']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$user->setPassword(Validation::encryptCredentials($user->getUsername(), $this->getData('password')));
|
||||
parent::execute(...$functionArgs);
|
||||
Repo::user()->edit($user);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\ChangePasswordForm', '\ChangePasswordForm');
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/ContactForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class ContactForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to edit user's contact information.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\facades\Locale;
|
||||
use PKP\user\User;
|
||||
|
||||
class ContactForm extends BaseProfileForm
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
parent::__construct('user/contactForm.tpl', $user);
|
||||
|
||||
// Validation checks for this form
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorEmail($this, 'email', 'required', 'user.profile.form.emailRequired'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'country', 'required', 'user.profile.form.countryRequired'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom(
|
||||
$this,
|
||||
'email',
|
||||
'required',
|
||||
'user.register.form.emailExists',
|
||||
function (string $email, int $userId) {
|
||||
if ($user = Repo::user()->getByEmail($email, true)) {
|
||||
return (int)$user->getId() === $userId;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[(int)$user->getId()]
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::fetch
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function fetch($request, $template = null, $display = false)
|
||||
{
|
||||
$site = $request->getSite();
|
||||
$countries = [];
|
||||
foreach (Locale::getCountries() as $country) {
|
||||
$countries[$country->getAlpha2()] = $country->getLocalName();
|
||||
}
|
||||
asort($countries);
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
$templateMgr->assign([
|
||||
'countries' => $countries,
|
||||
'availableLocales' => $site->getSupportedLocaleNames(),
|
||||
]);
|
||||
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::initData()
|
||||
*/
|
||||
public function initData()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->_data = [
|
||||
'country' => $user->getCountry(),
|
||||
'email' => $user->getEmail(),
|
||||
'phone' => $user->getPhone(),
|
||||
'signature' => $user->getSignature(null), // Localized
|
||||
'mailingAddress' => $user->getMailingAddress(),
|
||||
'affiliation' => $user->getAffiliation(null), // Localized
|
||||
'locales' => $user->getLocales(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
parent::readInputData();
|
||||
|
||||
$this->readUserVars([
|
||||
'country', 'email', 'signature', 'phone', 'mailingAddress', 'affiliation', 'locales',
|
||||
]);
|
||||
|
||||
if ($this->getData('locales') == null || !is_array($this->getData('locales'))) {
|
||||
$this->setData('locales', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$functionArgs['emailUpdated'] = $user->getEmail() !== $this->getData('email');
|
||||
|
||||
$user->setCountry($this->getData('country'));
|
||||
$user->setEmail($this->getData('email'));
|
||||
$user->setSignature($this->getData('signature'), null); // Localized
|
||||
$user->setPhone($this->getData('phone'));
|
||||
$user->setMailingAddress($this->getData('mailingAddress'));
|
||||
$user->setAffiliation($this->getData('affiliation'), null); // Localized
|
||||
|
||||
$request = Application::get()->getRequest();
|
||||
$site = $request->getSite();
|
||||
$availableLocales = $site->getSupportedLocales();
|
||||
$locales = [];
|
||||
foreach ($this->getData('locales') as $locale) {
|
||||
if (Locale::isLocaleValid($locale) && in_array($locale, $availableLocales)) {
|
||||
array_push($locales, $locale);
|
||||
}
|
||||
}
|
||||
$user->setLocales($locales);
|
||||
|
||||
parent::execute(...$functionArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\ContactForm', '\ContactForm');
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/IdentityForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class IdentityForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to edit user's identity information.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\user\User;
|
||||
|
||||
class IdentityForm extends BaseProfileForm
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
parent::__construct('user/identityForm.tpl', $user);
|
||||
|
||||
// the users register for the site, thus
|
||||
// the site primary locale is the required default locale
|
||||
$site = Application::get()->getRequest()->getSite();
|
||||
$this->addSupportedFormLocale($site->getPrimaryLocale());
|
||||
|
||||
// Validation checks for this form
|
||||
$form = $this;
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorLocale($this, 'givenName', 'required', 'user.profile.form.givenNameRequired', $site->getPrimaryLocale()));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'familyName', 'optional', 'user.profile.form.givenNameRequired.locale', function ($familyName) use ($form) {
|
||||
$givenNames = $form->getData('givenName');
|
||||
foreach ($familyName as $locale => $value) {
|
||||
if (!empty($value) && empty($givenNames[$locale])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::fetch
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function fetch($request, $template = null, $display = false)
|
||||
{
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
|
||||
$user = $this->getUser();
|
||||
$templateMgr->assign([
|
||||
'username' => $user->getUsername(),
|
||||
]);
|
||||
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::initData()
|
||||
*/
|
||||
public function initData()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->_data = [
|
||||
'givenName' => $user->getGivenName(null),
|
||||
'familyName' => $user->getFamilyName(null),
|
||||
'preferredPublicName' => $user->getPreferredPublicName(null),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
parent::readInputData();
|
||||
|
||||
$this->readUserVars([
|
||||
'givenName', 'familyName', 'preferredPublicName',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$request = Application::get()->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$user->setGivenName($this->getData('givenName'), null);
|
||||
$user->setFamilyName($this->getData('familyName'), null);
|
||||
$user->setPreferredPublicName($this->getData('preferredPublicName'), null);
|
||||
|
||||
parent::execute(...$functionArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\IdentityForm', '\IdentityForm');
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/LoginChangePasswordForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class LoginChangePasswordForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to change a user's password in order to login.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\form\Form;
|
||||
use PKP\security\Validation;
|
||||
use PKP\site\Site;
|
||||
|
||||
class LoginChangePasswordForm extends Form
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Site $site
|
||||
*/
|
||||
public function __construct($site)
|
||||
{
|
||||
parent::__construct('user/loginChangePassword.tpl');
|
||||
|
||||
// Validation checks for this form
|
||||
$form = $this;
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'oldPassword', 'required', 'user.profile.form.oldPasswordInvalid', function ($password) use ($form) {
|
||||
return Validation::checkCredentials($form->getData('username'), $password);
|
||||
}));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorLength($this, 'password', 'required', 'user.register.form.passwordLengthRestriction', '>=', $site->getMinPasswordLength()));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'password', 'required', 'user.profile.form.newPasswordRequired'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'password', 'required', 'user.register.form.passwordsDoNotMatch', function ($password) use ($form) {
|
||||
return $password == $form->getData('password2');
|
||||
}));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorPost($this));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCSRF($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::display
|
||||
*
|
||||
* @param null|mixed $request
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function display($request = null, $template = null)
|
||||
{
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
$site = $request->getSite();
|
||||
$templateMgr->assign('minPasswordLength', $site->getMinPasswordLength());
|
||||
parent::display($request, $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
$this->readUserVars(['username', 'oldPassword', 'password', 'password2']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*
|
||||
* @return bool success
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$user = Repo::user()->getByUsername($this->getData('username'), false);
|
||||
parent::execute(...$functionArgs);
|
||||
if ($user != null) {
|
||||
$user->setPassword(Validation::encryptCredentials($user->getUsername(), $this->getData('password')));
|
||||
$user->setMustChangePassword(0);
|
||||
Repo::user()->edit($user);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\LoginChangePasswordForm', '\LoginChangePasswordForm');
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/PublicProfileForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class PublicProfileForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to edit user's public profile.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use APP\file\PublicFileManager;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\core\Core;
|
||||
use PKP\user\User;
|
||||
|
||||
class PublicProfileForm extends BaseProfileForm
|
||||
{
|
||||
public const PROFILE_IMAGE_MAX_WIDTH = 150;
|
||||
public const PROFILE_IMAGE_MAX_HEIGHT = 150;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
parent::__construct('user/publicProfileForm.tpl', $user);
|
||||
|
||||
// Validation checks for this form
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorORCID($this, 'orcid', 'optional', 'user.orcid.orcidInvalid'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorUrl($this, 'userUrl', 'optional', 'user.profile.form.urlInvalid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::initData()
|
||||
*/
|
||||
public function initData()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->_data = [
|
||||
'orcid' => $user->getOrcid(),
|
||||
'userUrl' => $user->getUrl(),
|
||||
'biography' => $user->getBiography(null), // Localized
|
||||
];
|
||||
|
||||
parent::initData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
parent::readInputData();
|
||||
|
||||
$this->readUserVars([
|
||||
'orcid', 'userUrl', 'biography',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a profile image.
|
||||
*
|
||||
* @return bool True iff success.
|
||||
*/
|
||||
public function uploadProfileImage()
|
||||
{
|
||||
if (!Application::get()->getRequest()->checkCSRF()) {
|
||||
throw new \Exception('CSRF mismatch!');
|
||||
}
|
||||
|
||||
$publicFileManager = new PublicFileManager();
|
||||
|
||||
$user = $this->getUser();
|
||||
$type = $publicFileManager->getUploadedFileType('uploadedFile');
|
||||
$extension = $publicFileManager->getImageExtension($type);
|
||||
if (!$extension) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uploadName = 'profileImage-' . (int) $user->getId() . $extension;
|
||||
if (!$publicFileManager->uploadSiteFile('uploadedFile', $uploadName)) {
|
||||
return false;
|
||||
}
|
||||
$filePath = $publicFileManager->getSiteFilesPath();
|
||||
[$width, $height] = getimagesize($filePath . '/' . $uploadName);
|
||||
|
||||
if ($width > self::PROFILE_IMAGE_MAX_WIDTH || $height > self::PROFILE_IMAGE_MAX_HEIGHT || $width <= 0 || $height <= 0) {
|
||||
$userSetting = null;
|
||||
$user->setData('profileImage', $userSetting);
|
||||
Repo::user()->edit($user, ['profileImage']);
|
||||
$publicFileManager->removeSiteFile($filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
$user->setData('profileImage', [
|
||||
'name' => $publicFileManager->getUploadedFileName('uploadedFile'),
|
||||
'uploadName' => $uploadName,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'dateUploaded' => Core::getCurrentDate(),
|
||||
]);
|
||||
Repo::user()->edit($user, ['profileImage']);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a profile image.
|
||||
*
|
||||
* @return bool True iff success.
|
||||
*/
|
||||
public function deleteProfileImage()
|
||||
{
|
||||
if (!Application::get()->getRequest()->checkCSRF()) {
|
||||
throw new \Exception('CSRF mismatch!');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$profileImage = $user->getData('profileImage');
|
||||
if (!$profileImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$publicFileManager = new PublicFileManager();
|
||||
if ($publicFileManager->removeSiteFile($profileImage['uploadName'])) {
|
||||
$user->setData('profileImage', null);
|
||||
Repo::user()->edit($user, ['profileImage']);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::fetch
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function fetch($request, $template = null, $display = false)
|
||||
{
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
|
||||
$publicFileManager = new PublicFileManager();
|
||||
$templateMgr->assign([
|
||||
'profileImage' => $request->getUser()->getData('profileImage'),
|
||||
'profileImageMaxWidth' => self::PROFILE_IMAGE_MAX_WIDTH,
|
||||
'profileImageMaxHeight' => self::PROFILE_IMAGE_MAX_HEIGHT,
|
||||
'publicSiteFilesPath' => $publicFileManager->getSiteFilesPath(),
|
||||
]);
|
||||
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$request = Application::get()->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$user->setOrcid($this->getData('orcid'));
|
||||
$user->setUrl($this->getData('userUrl'));
|
||||
$user->setBiography($this->getData('biography'), null); // Localized
|
||||
|
||||
parent::execute(...$functionArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\PublicProfileForm', '\PublicProfileForm');
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
/**
|
||||
* @defgroup user_form User Forms
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/user/form/RegistrationForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class RegistrationForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form for user registration.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use APP\notification\form\NotificationSettingsForm;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\config\Config;
|
||||
use PKP\core\Core;
|
||||
use PKP\db\DAORegistry;
|
||||
use PKP\facades\Locale;
|
||||
use PKP\form\Form;
|
||||
use PKP\security\Role;
|
||||
use PKP\security\Validation;
|
||||
use PKP\session\SessionManager;
|
||||
use PKP\site\Site;
|
||||
use PKP\user\InterestManager;
|
||||
use PKP\user\User;
|
||||
|
||||
class RegistrationForm extends Form
|
||||
{
|
||||
/** @var User The user object being created (available to hooks during registrationform::execute hook) */
|
||||
public $user;
|
||||
|
||||
/** @var bool user is already registered with another context */
|
||||
public $existingUser;
|
||||
|
||||
/** @var bool whether or not captcha is enabled for this form */
|
||||
public $captchaEnabled;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Site $site
|
||||
*/
|
||||
public function __construct($site)
|
||||
{
|
||||
parent::__construct('frontend/pages/userRegister.tpl');
|
||||
|
||||
// Validation checks for this form
|
||||
$form = $this;
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'username', 'required', 'user.register.form.usernameExists', [Repo::user(), 'getByUsername'], [true], true));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'username', 'required', 'user.profile.form.usernameRequired'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'password', 'required', 'user.profile.form.passwordRequired'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorUsername($this, 'username', 'required', 'user.register.form.usernameAlphaNumeric'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorLength($this, 'password', 'required', 'user.register.form.passwordLengthRestriction', '>=', $site->getMinPasswordLength()));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'password', 'required', 'user.register.form.passwordsDoNotMatch', fn ($password) => $password == $form->getData('password2')));
|
||||
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'givenName', 'required', 'user.profile.form.givenNameRequired'));
|
||||
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'country', 'required', 'user.profile.form.countryRequired'));
|
||||
|
||||
// Email checks
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorEmail($this, 'email', 'required', 'user.profile.form.emailRequired'));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'email', 'required', 'user.register.form.emailExists', [Repo::user(), 'getByEmail'], [true], true));
|
||||
|
||||
$this->captchaEnabled = Config::getVar('captcha', 'captcha_on_register') && Config::getVar('captcha', 'recaptcha');
|
||||
if ($this->captchaEnabled) {
|
||||
$request = Application::get()->getRequest();
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorReCaptcha($this, $request->getRemoteAddr(), 'common.captcha.error.invalid-input-response', $request->getServerHost()));
|
||||
}
|
||||
|
||||
$context = Application::get()->getRequest()->getContext();
|
||||
if ($context && $context->getData('privacyStatement')) {
|
||||
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'privacyConsent', 'required', 'user.profile.form.privacyConsentRequired'));
|
||||
}
|
||||
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorPost($this));
|
||||
$this->addCheck(new \PKP\form\validation\FormValidatorCSRF($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::fetch()
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function fetch($request, $template = null, $display = false)
|
||||
{
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
$site = $request->getSite();
|
||||
|
||||
if ($this->captchaEnabled) {
|
||||
$templateMgr->assign('recaptchaPublicKey', Config::getVar('captcha', 'recaptcha_public_key'));
|
||||
}
|
||||
|
||||
$countries = [];
|
||||
foreach (Locale::getCountries() as $country) {
|
||||
$countries[$country->getAlpha2()] = $country->getLocalName();
|
||||
}
|
||||
asort($countries);
|
||||
$templateMgr->assign('countries', $countries);
|
||||
|
||||
$userFormHelper = new UserFormHelper();
|
||||
$userFormHelper->assignRoleContent($templateMgr, $request);
|
||||
|
||||
$templateMgr->assign([
|
||||
'source' => $request->getUserVar('source'),
|
||||
'minPasswordLength' => $site->getMinPasswordLength(),
|
||||
'enableSiteWidePrivacyStatement' => Config::getVar('general', 'sitewide_privacy_statement'),
|
||||
'siteWidePrivacyStatement' => $site->getData('privacyStatement'),
|
||||
]);
|
||||
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::initData()
|
||||
*/
|
||||
public function initData()
|
||||
{
|
||||
$this->_data = [
|
||||
'locales' => [],
|
||||
'userGroupIds' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
parent::readInputData();
|
||||
|
||||
$this->readUserVars([
|
||||
'username',
|
||||
'password',
|
||||
'password2',
|
||||
'givenName',
|
||||
'familyName',
|
||||
'affiliation',
|
||||
'email',
|
||||
'country',
|
||||
'interests',
|
||||
'emailConsent',
|
||||
'privacyConsent',
|
||||
'readerGroup',
|
||||
'reviewerGroup',
|
||||
]);
|
||||
|
||||
if ($this->captchaEnabled) {
|
||||
$this->readUserVars([
|
||||
'g-recaptcha-response',
|
||||
]);
|
||||
}
|
||||
|
||||
// Collect the specified user group IDs into a single piece of data
|
||||
$this->setData('userGroupIds', array_merge(
|
||||
array_keys((array) $this->getData('readerGroup')),
|
||||
array_keys((array) $this->getData('reviewerGroup'))
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::validate()
|
||||
*/
|
||||
public function validate($callHooks = true)
|
||||
{
|
||||
$request = Application::get()->getRequest();
|
||||
|
||||
// Ensure the consent checkbox has been completed for the site and any user
|
||||
// group sign-ups if we're in the site-wide registration form
|
||||
if (!$request->getContext()) {
|
||||
if ($request->getSite()->getData('privacyStatement')) {
|
||||
$privacyConsent = $this->getData('privacyConsent');
|
||||
if (!is_array($privacyConsent) || !array_key_exists(Application::CONTEXT_ID_NONE, $privacyConsent)) {
|
||||
$this->addError('privacyConsent[' . Application::CONTEXT_ID_NONE . ']', __('user.register.form.missingSiteConsent'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!Config::getVar('general', 'sitewide_privacy_statement')) {
|
||||
$contextIds = [];
|
||||
foreach ($this->getData('userGroupIds') as $userGroupId) {
|
||||
$userGroup = Repo::userGroup()->get($userGroupId);
|
||||
$contextIds[] = $userGroup->getContextId();
|
||||
}
|
||||
|
||||
$contextIds = array_unique($contextIds);
|
||||
if (!empty($contextIds)) {
|
||||
$contextDao = Application::getContextDao();
|
||||
$privacyConsent = (array) $this->getData('privacyConsent');
|
||||
foreach ($contextIds as $contextId) {
|
||||
$context = $contextDao->getById($contextId);
|
||||
if ($context->getData('privacyStatement') && !array_key_exists($contextId, $privacyConsent)) {
|
||||
$this->addError('privacyConsent[' . $contextId . ']', __('user.register.form.missingContextConsent'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::validate($callHooks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
*
|
||||
* @return int|null User ID, or false on failure
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$requireValidation = Config::getVar('email', 'require_validation');
|
||||
|
||||
// New user
|
||||
$this->user = $user = Repo::user()->newDataObject();
|
||||
|
||||
$user->setUsername($this->getData('username'));
|
||||
|
||||
// The multilingual user data (givenName, familyName and affiliation) will be saved
|
||||
// in the current UI locale and copied in the site's primary locale too
|
||||
$request = Application::get()->getRequest();
|
||||
$site = $request->getSite();
|
||||
$sitePrimaryLocale = $site->getPrimaryLocale();
|
||||
$currentLocale = Locale::getLocale();
|
||||
|
||||
// Set the base user fields (name, etc.)
|
||||
$user->setGivenName($this->getData('givenName'), $currentLocale);
|
||||
$user->setFamilyName($this->getData('familyName'), $currentLocale);
|
||||
$user->setEmail($this->getData('email'));
|
||||
$user->setCountry($this->getData('country'));
|
||||
$user->setAffiliation($this->getData('affiliation'), $currentLocale);
|
||||
|
||||
if ($sitePrimaryLocale != $currentLocale) {
|
||||
$user->setGivenName($this->getData('givenName'), $sitePrimaryLocale);
|
||||
$user->setFamilyName($this->getData('familyName'), $sitePrimaryLocale);
|
||||
$user->setAffiliation($this->getData('affiliation'), $sitePrimaryLocale);
|
||||
}
|
||||
|
||||
$user->setDateRegistered(Core::getCurrentDate());
|
||||
$user->setInlineHelp(1); // default new users to having inline help visible.
|
||||
$user->setPassword(Validation::encryptCredentials($this->getData('username'), $this->getData('password')));
|
||||
|
||||
if ($requireValidation) {
|
||||
// The account should be created in a disabled
|
||||
// state.
|
||||
$user->setDisabled(true);
|
||||
$user->setDisabledReason(__('user.login.accountNotValidated', ['email' => $this->getData('email')]));
|
||||
}
|
||||
|
||||
parent::execute(...$functionArgs);
|
||||
|
||||
Repo::user()->add($user);
|
||||
$userId = $user->getId();
|
||||
if (!$userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Associate the new user with the existing session
|
||||
$sessionManager = SessionManager::getManager();
|
||||
$session = $sessionManager->getUserSession();
|
||||
$session->setSessionVar('username', $user->getUsername());
|
||||
|
||||
// Save the selected roles or assign the Reader role if none selected
|
||||
if ($request->getContext() && !$this->getData('reviewerGroup')) {
|
||||
$defaultReaderGroup = Repo::userGroup()->getByRoleIds([Role::ROLE_ID_READER], $request->getContext()->getId(), true)->first();
|
||||
if ($defaultReaderGroup) {
|
||||
Repo::userGroup()->assignUserToGroup($user->getId(), $defaultReaderGroup->getId(), $request->getContext()->getId());
|
||||
}
|
||||
} else {
|
||||
$userFormHelper = new UserFormHelper();
|
||||
$userFormHelper->saveRoleContent($this, $user);
|
||||
}
|
||||
|
||||
// Save the email notification preference
|
||||
if ($request->getContext() && !$this->getData('emailConsent')) {
|
||||
// Get the public notification types
|
||||
$notificationSettingsForm = new NotificationSettingsForm();
|
||||
$notificationCategories = $notificationSettingsForm->getNotificationSettingCategories($request->getContext());
|
||||
foreach ($notificationCategories as $notificationCategory) {
|
||||
if ($notificationCategory['categoryKey'] === 'notification.type.public') {
|
||||
$publicNotifications = $notificationCategory['settings'];
|
||||
}
|
||||
}
|
||||
if (isset($publicNotifications)) {
|
||||
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO'); /** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
|
||||
$notificationSubscriptionSettingsDao->updateNotificationSubscriptionSettings(
|
||||
'blocked_emailed_notification',
|
||||
$publicNotifications,
|
||||
$user->getId(),
|
||||
$request->getContext()->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the user interests
|
||||
$interestManager = new InterestManager();
|
||||
$interestManager->setInterestsForUser($user, $this->getData('interests'));
|
||||
|
||||
return $userId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\RegistrationForm', '\RegistrationForm');
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/ResetPasswordForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class ResetPasswordForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to reset a user's password.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\form\Form;
|
||||
use PKP\form\validation\FormValidator;
|
||||
use PKP\form\validation\FormValidatorCSRF;
|
||||
use PKP\form\validation\FormValidatorCustom;
|
||||
use PKP\form\validation\FormValidatorLength;
|
||||
use PKP\form\validation\FormValidatorPost;
|
||||
use PKP\security\Validation;
|
||||
use PKP\session\SessionManager;
|
||||
|
||||
class ResetPasswordForm extends Form
|
||||
{
|
||||
/** @var object */
|
||||
protected $_user;
|
||||
|
||||
/** @var object */
|
||||
protected $_site;
|
||||
|
||||
/** @var string */
|
||||
protected $_hash;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct($user, $site, $hash)
|
||||
{
|
||||
parent::__construct('user/userPasswordReset.tpl');
|
||||
|
||||
$this->_user = $user;
|
||||
$this->_site = $site;
|
||||
$this->_hash = $hash;
|
||||
|
||||
$this->addCheck(new FormValidatorLength($this, 'password', 'required', 'user.register.form.passwordLengthRestriction', '>=', $site->getMinPasswordLength()));
|
||||
$this->addCheck(new FormValidator($this, 'password', 'required', 'user.profile.form.newPasswordRequired'));
|
||||
$form = $this;
|
||||
$this->addCheck(new FormValidatorCustom($this, 'password', 'required', 'user.register.form.passwordsDoNotMatch', function ($password) use ($form) {
|
||||
return $password == $form->getData('password2');
|
||||
}));
|
||||
|
||||
$this->addCheck(new FormValidatorPost($this));
|
||||
$this->addCheck(new FormValidatorCSRF($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user associated with this password
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site
|
||||
*/
|
||||
public function getSite()
|
||||
{
|
||||
return $this->_site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password reset hash
|
||||
*/
|
||||
public function getHash()
|
||||
{
|
||||
return $this->_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::display
|
||||
*
|
||||
* @param null|mixed $request
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function display($request = null, $template = null)
|
||||
{
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
$templateMgr->assign([
|
||||
'minPasswordLength' => $this->getSite()->getMinPasswordLength(),
|
||||
'username' => $this->getUser()->getUsername(),
|
||||
'hash' => $this->getHash(),
|
||||
]);
|
||||
parent::display($request, $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
$this->readUserVars(['username', 'hash', 'password', 'password2']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$user->setPassword(Validation::encryptCredentials($user->getUsername(), $this->getData('password')));
|
||||
$user->setMustChangePassword(0);
|
||||
|
||||
SessionManager::getManager()->invalidateSessions($user->getId());
|
||||
Repo::user()->edit($user);
|
||||
|
||||
parent::execute(...$functionArgs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the password reset hash
|
||||
*/
|
||||
public function validatePasswordResetHash()
|
||||
{
|
||||
if (Validation::verifyPasswordResetHash($this->getUser()->getId(), $this->getHash())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the error page when passed invalid password reset hash
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function displayInvalidHashErrorMessage($request, $template = null)
|
||||
{
|
||||
$this->setTemplate('frontend/pages/error.tpl');
|
||||
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
|
||||
$templateMgr->assign([
|
||||
'errorMsg' => 'user.login.lostPassword.invalidHash',
|
||||
'backLink' => $request->url(null, null, 'lostPassword'),
|
||||
'backLinkLabel' => 'user.login.resetPassword',
|
||||
]);
|
||||
|
||||
parent::display($request, $template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/RolesForm.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class RolesForm
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Form to edit the roles area of the user profile.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use APP\template\TemplateManager;
|
||||
use PKP\user\InterestManager;
|
||||
use PKP\user\User;
|
||||
|
||||
class RolesForm extends BaseProfileForm
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
parent::__construct('user/rolesForm.tpl', $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::fetch
|
||||
*
|
||||
* @param null|mixed $template
|
||||
*/
|
||||
public function fetch($request, $template = null, $display = false)
|
||||
{
|
||||
$templateMgr = TemplateManager::getManager($request);
|
||||
|
||||
$userGroupIds = Repo::userGroup()->getCollector()
|
||||
->filterByUserIds([$request->getUser()->getId()])
|
||||
->getIds()
|
||||
->toArray();
|
||||
|
||||
$templateMgr->assign('userGroupIds', $userGroupIds);
|
||||
|
||||
$userFormHelper = new UserFormHelper();
|
||||
$userFormHelper->assignRoleContent($templateMgr, $request);
|
||||
|
||||
return parent::fetch($request, $template, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc BaseProfileForm::initData()
|
||||
*/
|
||||
public function initData()
|
||||
{
|
||||
$interestManager = new InterestManager();
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->_data = [
|
||||
'interests' => $interestManager->getInterestsForUser($user),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign form data to user-submitted data.
|
||||
*/
|
||||
public function readInputData()
|
||||
{
|
||||
parent::readInputData();
|
||||
|
||||
$this->readUserVars([
|
||||
'authorGroup',
|
||||
'reviewerGroup',
|
||||
'readerGroup',
|
||||
'interests',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc Form::execute()
|
||||
*/
|
||||
public function execute(...$functionArgs)
|
||||
{
|
||||
$request = Application::get()->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
// Save the roles
|
||||
$userFormHelper = new UserFormHelper();
|
||||
$userFormHelper->saveRoleContent($this, $user);
|
||||
|
||||
// Insert the user interests
|
||||
$interestManager = new InterestManager();
|
||||
$interestManager->setInterestsForUser($user, $this->getData('interests'));
|
||||
|
||||
parent::execute(...$functionArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\RolesForm', '\RolesForm');
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/user/form/UserFormHelper.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2003-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class UserFormHelper
|
||||
*
|
||||
* @ingroup user_form
|
||||
*
|
||||
* @brief Helper functions for shared user form concerns.
|
||||
*/
|
||||
|
||||
namespace PKP\user\form;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use PKP\core\PKPRequest;
|
||||
use PKP\form\Form;
|
||||
use PKP\security\Role;
|
||||
use PKP\template\PKPTemplateManager;
|
||||
use PKP\user\User;
|
||||
|
||||
class UserFormHelper
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign role selection content to the template manager.
|
||||
*
|
||||
* @param PKPTemplateManager $templateMgr
|
||||
* @param PKPRequest $request
|
||||
*/
|
||||
public function assignRoleContent($templateMgr, $request)
|
||||
{
|
||||
// Need the count in order to determine whether to display
|
||||
// extras-on-demand for role selection in other contexts.
|
||||
$contextDao = Application::getContextDAO();
|
||||
$contexts = $contextDao->getAll(true)->toArray();
|
||||
$contextsWithUserRegistration = [];
|
||||
foreach ($contexts as $context) {
|
||||
if (!$context->getData('disableUserReg')) {
|
||||
$contextsWithUserRegistration[] = $context;
|
||||
}
|
||||
}
|
||||
$templateMgr->assign([
|
||||
'contexts' => $contexts,
|
||||
'showOtherContexts' => !$request->getContext() || count($contextsWithUserRegistration) > 1,
|
||||
]);
|
||||
|
||||
// Expose potential self-registration user groups to template
|
||||
$authorUserGroups = $reviewerUserGroups = $readerUserGroups = [];
|
||||
|
||||
foreach ($contexts as $context) {
|
||||
if ($context->getData('disableUserReg')) {
|
||||
continue;
|
||||
}
|
||||
$reviewerUserGroups[$context->getId()] = Repo::userGroup()->getByRoleIds([Role::ROLE_ID_REVIEWER], $context->getId())->toArray();
|
||||
$authorUserGroups[$context->getId()] = Repo::userGroup()->getByRoleIds([Role::ROLE_ID_AUTHOR], $context->getId())->toArray();
|
||||
$readerUserGroups[$context->getId()] = Repo::userGroup()->getByRoleIds([Role::ROLE_ID_READER], $context->getId())->toArray();
|
||||
}
|
||||
$templateMgr->assign([
|
||||
'reviewerUserGroups' => $reviewerUserGroups,
|
||||
'authorUserGroups' => $authorUserGroups,
|
||||
'readerUserGroups' => $readerUserGroups,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save role elements of an executed user form.
|
||||
*
|
||||
* @param Form $form The form from which to fetch elements
|
||||
* @param User $user The current user
|
||||
*/
|
||||
public function saveRoleContent($form, $user)
|
||||
{
|
||||
$contextDao = Application::getContextDAO();
|
||||
$contexts = $contextDao->getAll(true);
|
||||
while ($context = $contexts->next()) {
|
||||
if ($context->getData('disableUserReg')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ([
|
||||
[
|
||||
'roleId' => Role::ROLE_ID_REVIEWER,
|
||||
'formElement' => 'reviewerGroup'
|
||||
],
|
||||
[
|
||||
'roleId' => Role::ROLE_ID_AUTHOR,
|
||||
'formElement' => 'authorGroup'
|
||||
],
|
||||
[
|
||||
'roleId' => Role::ROLE_ID_READER,
|
||||
'formElement' => 'readerGroup'
|
||||
],
|
||||
] as $groupData) {
|
||||
$groupFormData = (array) $form->getData($groupData['formElement']);
|
||||
$userGroups = Repo::userGroup()->getByRoleIds([$groupData['roleId']], $context->getId());
|
||||
foreach ($userGroups as $userGroup) {
|
||||
if (!$userGroup->getPermitSelfRegistration()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$groupId = $userGroup->getId();
|
||||
$inGroup = Repo::userGroup()->userInGroup($user->getId(), $groupId);
|
||||
if (!$inGroup && array_key_exists($groupId, $groupFormData)) {
|
||||
Repo::userGroup()->assignUserToGroup($user->getId(), $groupId);
|
||||
} elseif ($inGroup && !array_key_exists($groupId, $groupFormData)) {
|
||||
Repo::userGroup()->removeUserFromGroup($user->getId(), $groupId, $context->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\user\form\UserFormHelper', '\UserFormHelper');
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* @file classes/user/maps/Schema.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Schema
|
||||
*
|
||||
* @brief Map users to the properties defined in the user schema
|
||||
*/
|
||||
|
||||
namespace PKP\user\maps;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use PKP\db\DAORegistry;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\services\PKPSchemaService;
|
||||
use PKP\user\User;
|
||||
|
||||
class Schema extends \PKP\core\maps\Schema
|
||||
{
|
||||
public Enumerable $collection;
|
||||
|
||||
public string $schema = PKPSchemaService::SCHEMA_USER;
|
||||
|
||||
/**
|
||||
* Map a publication
|
||||
*
|
||||
* Includes all properties in the user schema.
|
||||
*/
|
||||
public function map(User $item): array
|
||||
{
|
||||
return $this->mapByProperties($this->getProps(), $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize a user
|
||||
*
|
||||
* Includes properties with the apiSummary flag in the user schema.
|
||||
*/
|
||||
public function summarize(User $item): array
|
||||
{
|
||||
return $this->mapByProperties($this->getSummaryProps(), $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize a user with reviewer data
|
||||
*
|
||||
* Includes properties with the apiSummary flag in the user schema.
|
||||
*/
|
||||
public function summarizeReviewer(User $item): array
|
||||
{
|
||||
return $this->mapByProperties(array_merge($this->getSummaryProps(), ['reviewsActive', 'reviewsCompleted', 'reviewsDeclined', 'reviewsCancelled', 'averageReviewCompletionDays', 'dateLastReviewAssignment', 'reviewerRating']), $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a collection of Users
|
||||
*
|
||||
* @see self::map
|
||||
*/
|
||||
public function mapMany(Enumerable $collection): Enumerable
|
||||
{
|
||||
$this->collection = $collection;
|
||||
return $collection->map(function ($item) {
|
||||
return $this->map($item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize a collection of users
|
||||
*
|
||||
* @see self::summarize
|
||||
*/
|
||||
public function summarizeMany(Enumerable $collection): Enumerable
|
||||
{
|
||||
$this->collection = $collection;
|
||||
return $collection->map(function ($item) {
|
||||
return $this->summarize($item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize a collection of reviewers
|
||||
*
|
||||
* @see self::summarizeReviewer
|
||||
*/
|
||||
public function summarizeManyReviewers(Enumerable $collection): Enumerable
|
||||
{
|
||||
$this->collection = $collection;
|
||||
return $collection->map(function ($item) {
|
||||
return $this->summarizeReviewer($item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Map schema properties of a user to an assoc array
|
||||
*/
|
||||
protected function mapByProperties(array $props, User $user): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($props as $prop) {
|
||||
switch ($prop) {
|
||||
case 'id':
|
||||
$output[$prop] = (int) $user->getId();
|
||||
break;
|
||||
case 'fullName':
|
||||
$output[$prop] = $user->getFullName();
|
||||
break;
|
||||
case 'gossip':
|
||||
if (Repo::user()->canCurrentUserGossip($user->getId())) {
|
||||
$output[$prop] = $user->getGossip();
|
||||
}
|
||||
break;
|
||||
case 'reviewsActive':
|
||||
$output[$prop] = $user->getData('incompleteCount');
|
||||
break;
|
||||
case 'reviewsCompleted':
|
||||
$output[$prop] = $user->getData('completeCount');
|
||||
break;
|
||||
case 'reviewsDeclined':
|
||||
$output[$prop] = $user->getData('declinedCount');
|
||||
break;
|
||||
case 'reviewsCancelled':
|
||||
$output[$prop] = $user->getData('cancelledCount');
|
||||
break;
|
||||
case 'averageReviewCompletionDays':
|
||||
$output[$prop] = $user->getData('averageTime');
|
||||
break;
|
||||
case 'dateLastReviewAssignment':
|
||||
$output[$prop] = $user->getData('lastAssigned');
|
||||
break;
|
||||
case 'disabledReason':
|
||||
$output[$prop] = $user->getDisabledReason();
|
||||
break;
|
||||
case '_href':
|
||||
$output[$prop] = $this->getApiUrl(
|
||||
'users/' . $user->getId(),
|
||||
$this->context->getData('urlPath')
|
||||
);
|
||||
break;
|
||||
case 'groups':
|
||||
$output[$prop] = null;
|
||||
if ($this->context) {
|
||||
$userGroups = Repo::userGroup()->userUserGroups($user->getId(), $this->context->getId());
|
||||
$output[$prop] = [];
|
||||
foreach ($userGroups as $userGroup) {
|
||||
$output[$prop][] = [
|
||||
'id' => (int) $userGroup->getId(),
|
||||
'name' => $userGroup->getName(null),
|
||||
'abbrev' => $userGroup->getAbbrev(null),
|
||||
'roleId' => (int) $userGroup->getRoleId(),
|
||||
'showTitle' => (bool) $userGroup->getShowTitle(),
|
||||
'permitSelfRegistration' => (bool) $userGroup->getPermitSelfRegistration(),
|
||||
'permitMetadataEdit' => (bool) $userGroup->getPermitMetadataEdit(),
|
||||
'recommendOnly' => (bool) $userGroup->getRecommendOnly(),
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'interests':
|
||||
$output[$prop] = [];
|
||||
if ($this->context) {
|
||||
$interestDao = DAORegistry::getDAO('InterestDAO'); /** @var \PKP\user\InterestDAO $interestDao */
|
||||
$interestEntryIds = $interestDao->getUserInterestIds($user->getId());
|
||||
if (!empty($interestEntryIds)) {
|
||||
$interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var \PKP\user\InterestEntryDAO $interestEntryDao */
|
||||
$results = $interestEntryDao->getByIds($interestEntryIds);
|
||||
$output[$prop] = [];
|
||||
while ($interest = $results->next()) {
|
||||
$output[$prop][] = [
|
||||
'id' => (int) $interest->getId(),
|
||||
'interest' => $interest->getInterest(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$output[$prop] = $user->getData($prop);
|
||||
break;
|
||||
}
|
||||
|
||||
$output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->context->getSupportedFormLocales());
|
||||
|
||||
Hook::call('UserSchema::getProperties::values', [$this, &$output, $user, $props]);
|
||||
|
||||
ksort($output);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user