first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
+685
View File
@@ -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;
}
}
+353
View File
@@ -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();
}
}
+159
View File
@@ -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);
}
+50
View File
@@ -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');
}
+102
View File
@@ -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');
}
+105
View File
@@ -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');
}
+126
View File
@@ -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();
}
}
+425
View File
@@ -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);
}
}
+460
View File
@@ -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');
}
+143
View File
@@ -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');
}
+114
View File
@@ -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);
}
}
+110
View File
@@ -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');
}
+196
View File
@@ -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;
}
}