| Server IP : 195.134.90.114 / Your IP : 216.73.216.86 Web Server : Apache/2.4.58 System : Linux nepub 6.8.0-88-generic #89-Ubuntu SMP PREEMPT_DYNAMIC Sat Oct 11 01:02:46 UTC 2025 x86_64 User : www-data ( 33) PHP Version : 8.2.30 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/public_html/lib/pkp/classes/services/queryBuilders/ |
Upload File : |
<?php
/**
* @file classes/services/QueryBuilders/PKPUserQueryBuilder.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 PKPUserQueryBuilder
* @ingroup query_builders
*
* @brief Submission list Query builder
*/
namespace PKP\Services\QueryBuilders;
use Application;
use AppLocale;
use Carbon\Carbon;
use DomainException;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use PKP\Services\QueryBuilders\Interfaces\EntityQueryBuilderInterface;
use PKPString;
class PKPUserQueryBuilder implements EntityQueryBuilderInterface {
/** @var int Context ID */
protected $contextId = null;
/** @var string order by column */
protected $orderColumn = 'u.user_id';
/** @var string order by direction */
protected $orderDirection = 'DESC';
/** @var string enabled or disabled users */
protected $status = null;
/** @var array list of role ids */
protected $roleIds = null;
/** @var array list of user group ids */
protected $userGroupIds = null;
/** @var array list of user ids */
protected $userIds = [];
/** @var int Assigned as editor to this category id */
protected $assignedToCategoryId = null;
/** @var int Assigned as editor to this section id */
protected $assignedToSectionId = null;
/** @var int submission ID */
protected $assignedToSubmissionId = null;
/** @var int submission stage ID */
protected $assignedToSubmissionStageId = null;
/** @var string get users registered after this date */
protected $registeredAfter = '';
/** @var string get users registered before this date */
protected $registeredBefore = '';
/** @var array user IDs */
protected $includeUsers = null;
/** @var array user IDs */
protected $excludeUsers = null;
/** @var string search phrase */
protected $searchPhrase = null;
/** @var bool whether to return reviewer activity data */
protected $getReviewerData = null;
/** @var int filter by review stage id */
protected $reviewStageId = null;
/** @var int filter by minimum reviewer rating */
protected $reviewerRating = null;
/** @var int|array filter by reviews completed by user */
protected $reviewsCompleted = null;
/** @var int|array filter by active review assignments for user */
protected $reviewsActive = null;
/** @var int|array filter by days since last review assignment */
protected $daysSinceLastAssignment = null;
/** @var int|array filter by average days to complete a review */
protected $averageCompletion = null;
/** @var int|null whether to limit the number of results returned */
protected $limit = null;
/** @var int whether to offset the number of results returned. Use to return a second page of results. */
protected $offset = 0;
/**
* Set context submissions filter
*
* @param int|string $contextId
*
* @return \APP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByContext($contextId) {
$this->contextId = $contextId;
return $this;
}
/**
* Set result order column and direction
*
* @param $column string
* @param $direction string
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function orderBy($column, $direction = 'DESC') {
$orderColumns = [
'id' => 'u.user_id',
IDENTITY_SETTING_GIVENNAME => IDENTITY_SETTING_GIVENNAME,
IDENTITY_SETTING_FAMILYNAME => IDENTITY_SETTING_FAMILYNAME
];
if (!isset($orderColumns[$column])) {
error_log((string) new DomainException("Invalid sort column: {$column}"));
$column = 'id';
}
$this->orderColumn = $orderColumns[$column];
$this->orderDirection = $direction;
return $this;
}
/**
* Set status filter
*
* @param $status string
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByStatus($status) {
if ($status && !in_array($status, ['active', 'disabled'])) {
error_log((string) new DomainException("Invalid status \"{$status}\""));
}
$this->status = $status;
return $this;
}
/**
* Set roles filter
*
* @param $roleIds int|array
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByRoleIds($roleIds) {
if (!is_null($roleIds) && !is_array($roleIds)) {
$roleIds = array($roleIds);
}
$this->roleIds = $roleIds;
return $this;
}
/**
* Set user groups filter
*
* @param array $userGroupIds
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByUserGroupIds(array $userGroupIds) {
$this->userGroupIds = $userGroupIds;
return $this;
}
/**
* Set user ID filter
*
* @param array $userIds
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByUserIds(array $userIds) {
$this->userIds = $userIds;
return $this;
}
/**
* Limit results to users assigned as editors to this category
*
* @param $categoryId int
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function assignedToCategory($categoryId) {
$this->assignedToCategoryId = $categoryId;
return $this;
}
/**
* Limit results to users assigned as editors to this section
*
* @param $sectionId int
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function assignedToSection($sectionId) {
$this->assignedToSectionId = $sectionId;
return $this;
}
/**
* Limit results to users assigned to this submission
*
* @param $submissionId int
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function assignedToSubmission($submissionId, $submissionStage) {
$this->assignedToSubmissionId = $submissionId;
if ($submissionStage && $this->assignedToSubmissionId) {
$this->assignedToSubmissionStageId = $submissionStage;
}
return $this;
}
/**
* Limit results to users who registered after this date
*
* @param $date string
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function registeredAfter($date) {
$this->registeredAfter = $date;
return $this;
}
/**
* Limit results to users who registered before this date
*
* @param $date string
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function registeredBefore($date) {
$this->registeredBefore = $date;
return $this;
}
/**
* Include selected users
*
* This will include a user even if they do not match
* the query conditions.
*
* @param $userIds array
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function includeUsers($userIds) {
$this->includeUsers = $userIds;
return $this;
}
/**
* Exclude selected users
*
* This will exclude a user even if they match all of the
* query conditions.
*
* @param $userIds array
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function excludeUsers($userIds) {
$this->excludeUsers = $userIds;
return $this;
}
/**
* Set query search phrase
*
* @param $phrase string
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function searchPhrase($phrase) {
$this->searchPhrase = $phrase;
return $this;
}
/**
* Whether to return reviewer activity data
*
* @param $enable bool
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function getReviewerData($enable = true) {
$this->getReviewerData = $enable;
return $this;
}
/**
* Limit results to reviewers for a particular stage
*
* @param $reviewStageId int WORKFLOW_STAGE_ID_*_REVIEW
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByReviewStage($reviewStageId = null) {
if (!is_null($reviewStageId)) {
$this->reviewStageId = $reviewStageId;
}
return $this;
}
/**
* Limit results to those who have a minimum reviewer rating
*
* @param $reviewerRating int
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByReviewerRating($reviewerRating = null) {
if (!is_null($reviewerRating)) {
$this->reviewerRating = $reviewerRating;
}
return $this;
}
/**
* Limit results to those who have completed at least this many reviews
*
* @param $reviewsCompleted int|array
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByReviewsCompleted($reviewsCompleted = null) {
if (!is_null($reviewsCompleted)) {
$this->reviewsCompleted = $reviewsCompleted;
}
return $this;
}
/**
* Limit results to those who have at least this many active review assignments
*
* @param $reviewsActive int|array
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByReviewsActive($reviewsActive = null) {
if (!is_null($reviewsActive)) {
$this->reviewsActive = $reviewsActive;
}
return $this;
}
/**
* Limit results to those who's last review assignment was at least this many
* days ago.
*
* @param $daysSinceLastAssignment int|array
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByDaysSinceLastAssignment($daysSinceLastAssignment = null) {
if (!is_null($daysSinceLastAssignment)) {
$this->daysSinceLastAssignment = $daysSinceLastAssignment;
}
return $this;
}
/**
* Limit results to those who complete a review on average less than this many
* days after their assignment.
*
* @param $averageCompletion int|array
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function filterByAverageCompletion($averageCompletion = null) {
if (!is_null($averageCompletion)) {
$this->averageCompletion = $averageCompletion;
}
return $this;
}
/**
* Set query limit
*
* @param int $count
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function limitTo($count) {
$this->limit = $count;
return $this;
}
/**
* Set how many results to skip
*
* @param int $offset
*
* @return \PKP\Services\QueryBuilders\PKPUserQueryBuilder
*/
public function offsetBy($offset) {
$this->offset = $offset;
return $this;
}
/**
* @copydoc PKP\Services\QueryBuilders\Interfaces\EntityQueryBuilderInterface::getCount()
*/
public function getCount() {
return $this
->getQuery()
->getCountForPagination();
}
/**
* @copydoc PKP\Services\QueryBuilders\Interfaces\EntityQueryBuilderInterface::getIds()
*/
public function getIds() {
$q = $this->getQuery();
// Reset the orderBy
$q->orders = [];
return $q->select('u.user_id')
->pluck('u.user_id')
->toArray();
}
/**
* Execute query builder
*
* @return object Query object
*/
public function getQuery() {
$query = Capsule::table('users', 'u')
->select('u.*')
// Filters by registration date
->when(!empty($this->registeredBefore), function (Builder $query) {
// Include users who registered up to the end of the day
$query->where('u.date_registered', '<', Carbon::rawParse($this->registeredBefore)->addDay()->toDateString());
})
->when(!empty($this->registeredAfter), function (Builder $query) {
$query->where('u.date_registered', '>=', $this->registeredAfter);
})
->when(!empty($this->userIds), function (Builder $query) {
$query->whereIn('u.user_id', $this->userIds);
})
->when($this->excludeUsers !== null, function (Builder $query) {
$query->whereNotIn('u.user_id', $this->excludeUsers);
})
// User enabled/disabled state
->when(in_array($this->status, ['active', 'disabled']), function (Builder $query) {
$query->where('u.disabled', '=', $this->status === 'disabled');
})
// Adds limit and offset for pagination
->when($this->limit !== null, function (Builder $query) {
$query->limit($this->limit);
})
->when(!empty($this->offset), function (Builder $query) {
$query->offset($this->offset);
});
$this
->buildReviewerStatistics($query)
->buildUserGroupFilter($query)
->buildSearchFilter($query)
->buildSubEditorFilter($query)
->buildSubmissionAssignmentsFilter($query)
->buildOrderBy($query);
// Forces the inclusion of determined users (must be the last statement)
$query->when(!empty($this->includeUsers), function (Builder $query) {
$query->orWhereIn('u.user_id', $this->includeUsers);
});
// Add app-specific query statements
\HookRegistry::call('User::getMany::queryObject', array(&$query, $this));
return $query;
}
/**
* Builds the filters related to submission assignments (submission, stage, user group)
*/
protected function buildSubmissionAssignmentsFilter(Builder $query): self
{
if ($this->assignedToSubmissionId === null && $this->assignedToSubmissionStageId === null) {
return $this;
}
$query->whereExists(function (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($this->assignedToSubmissionId !== null, function (Builder $query) {
$query->where('sa.submission_id', '=', $this->assignedToSubmissionId);
})
->when($this->assignedToSubmissionStageId, function (Builder $query) {
$query->where('ugs.stage_id', '=', $this->assignedToSubmissionStageId);
});
});
return $this;
}
/**
* Builds the filters related to the user group
*/
protected function buildUserGroupFilter(Builder $query): self
{
if (empty($this->userGroupIds) && $this->roleIds === null && $this->contextId === CONTEXT_ID_ALL && $this->reviewStageId === null) {
return $this;
}
$query->whereExists(function (Builder $query) {
$query->from('user_user_groups', 'uug')
->join('user_groups AS ug', 'uug.user_group_id', '=', 'ug.user_group_id')
->whereColumn('uug.user_id', '=', 'u.user_id')
->when(!empty($this->userGroupIds), function (Builder $query) {
$query->whereIn('uug.user_group_id', $this->userGroupIds);
})
->when($this->reviewStageId !== null, function (Builder $query) {
$query->join('user_group_stage AS ugs', 'ug.user_group_id', '=', 'ugs.user_group_id')
->where('ugs.stage_id', '=', $this->reviewStageId);
})
->when($this->roleIds !== null, function (Builder $query) {
$query->whereIn('ug.role_id', $this->roleIds);
})
->when($this->contextId !== CONTEXT_ID_ALL, function (Builder $query) {
$query->where('ug.context_id', '=', $this->contextId ?? CONTEXT_ID_NONE);
});
});
return $this;
}
/**
* Builds the sub-editor filter
*/
protected function buildSubEditorFilter(Builder $query): self
{
foreach ([ASSOC_TYPE_SECTION => $this->assignedToSectionId, ASSOC_TYPE_CATEGORY => $this->assignedToCategoryId] as $type => $id) {
$query->when($id !== null, function (Builder $query) use ($id, $type) {
$query->whereExists(function (Builder $query) use ($id, $type) {
$query->from('subeditor_submission_group', 'ssg')
->whereColumn('ssg.user_id', '=', 'u.user_id')
->where('ssg.assoc_type', '=', $type)
->where('ssg.assoc_id', '=', $id);
});
});
}
return $this;
}
/**
* Builds the reviewer statistics and related filters
*/
protected function buildReviewerStatistics(Builder $query): self
{
if (empty($this->getReviewerData)) {
return $this;
}
// Compile the statistics into a sub-query
$query->leftJoinSub(function (Builder $query) {
$dateDiff = Capsule::connection() instanceof MySqlConnection
? 'DATEDIFF(ra.date_completed, ra.date_notified)'
: "DATE_PART('day', ra.date_completed - ra.date_notified)";
$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) AS average_time")
->selectRaw('AVG(ra.quality) AS reviewer_rating');
}, 'ra_stats', 'u.user_id', '=', 'ra_stats.reviewer_id')
// Select all statistics columns
->addSelect('ra_stats.*')
// Reviewer rating
->when($this->reviewerRating !== null, function (Builder $query) {
$query->where('ra_stats.reviewer_rating', '>=', $this->reviewerRating);
})
// Completed reviews
->when(($reviewsCompleted = $this->reviewsCompleted[0] ?? $this->reviewsCompleted) !== null, function (Builder $query) use ($reviewsCompleted) {
$query->where('ra_stats.complete_count', '>=', $reviewsCompleted);
})
// Minimum active reviews
->when(($minReviews = $this->reviewsActive[0] ?? $this->reviewsActive) !== null, function (Builder $query) use ($minReviews) {
$query->where('ra_stats.incomplete_count', '>=', $minReviews);
})
// Maximum active reviews
->when(($maxReviews = $this->reviewsActive[1] ?? null) !== null, function (Builder $query) use ($maxReviews) {
$query->where('ra_stats.incomplete_count', '<=', $maxReviews);
})
// Minimum days since last review assignment
->when(($minDays = $this->daysSinceLastAssignment[0] ?? $this->daysSinceLastAssignment) !== null, function (Builder $query) use ($minDays) {
$query->where('ra_stats.last_assigned', '<=', Carbon::now()->subDays($minDays)->toDateString());
})
// Maximum days since last review assignment
->when(($maxDays = $this->daysSinceLastAssignment[1] ?? null) !== null, function (Builder $query) use ($maxDays) {
$query->where('ra_stats.last_assigned', '>=', Carbon::now()->subDays($maxDays + 1)->toDateString()); // Add one to include upper bound
})
// Average days to complete review
->when(($averageCompletion = $this->averageCompletion[0] ?? $this->averageCompletion) !== null, function (Builder $query) use ($averageCompletion) {
$query->where('ra_stats.average_time', '<=', $averageCompletion);
});
return $this;
}
/**
* Builds the user search
*/
protected function buildSearchFilter(Builder $query): self
{
if (!strlen($searchPhrase = trim($this->searchPhrase ?? ''))) {
return $this;
}
$words = array_map(function (string $word) {
return '%' . addcslashes($word, '%_') . '%';
}, PKPString::regexp_split('/\s+/', $searchPhrase));
foreach ($words as $word) {
$query->where(function (Builder $query) use ($word) {
$query->whereRaw('LOWER(u.username) LIKE LOWER(?)', [$word])
->orWhereRaw('LOWER(u.email) LIKE LOWER(?)', [$word])
->orWhereExists(function (Builder $query) use ($word) {
// Settings where the search will be performed
$settings = [IDENTITY_SETTING_GIVENNAME, IDENTITY_SETTING_FAMILYNAME, 'preferredPublicName', 'affiliation', 'biography', 'orcid'];
$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(function (Builder $query) use ($word) {
$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
{
$nameSettings = [IDENTITY_SETTING_GIVENNAME, IDENTITY_SETTING_FAMILYNAME];
if (in_array($this->orderColumn, $nameSettings)) {
$locales = array_unique([AppLocale::getLocale(), Application::get()->getRequest()->getSite()->getPrimaryLocale()]);
$sortedSettings = $this->orderColumn === IDENTITY_SETTING_GIVENNAME ? $nameSettings : array_reverse($nameSettings);
$query->orderBy(
function (Builder $query) use ($sortedSettings, $locales) {
$query->fromSub(function (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}",
function (JoinClause $join) use ($alias, $setting, $locale) {
$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(function (array $aliases) {
return 'COALESCE(' . implode(', ', array_map(function (string $alias) {
return "{$alias}.setting_value";
}, $aliases)) . ", '')";
}, $aliasesBySetting);
$query->selectRaw('CONCAT(' . implode(', ', $coalescedSettings) . ')');
},
$this->orderDirection
);
return $this;
}
$query->orderBy($this->orderColumn, $this->orderDirection);
return $this;
}
}