mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
run advanced user search from Pg
This commit is contained in:
48
app/Search/User.php
Normal file
48
app/Search/User.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle\Search;
|
||||
|
||||
class User {
|
||||
public function __construct(
|
||||
protected string $mode,
|
||||
) {}
|
||||
|
||||
public function matchField(string $field): string {
|
||||
return match ($this->mode) {
|
||||
'regexp' => "$field ~* ?",
|
||||
'strict' => "$field = ?",
|
||||
default => "$field ~~* concat('%', ?::text, '%')",
|
||||
};
|
||||
}
|
||||
|
||||
public function leftMatch(string $field): string {
|
||||
return match ($this->mode) {
|
||||
'regexp' => "$field ~* ?",
|
||||
'strict' => "$field = ?",
|
||||
default => "$field ~~* concat(?::text, '%')",
|
||||
};
|
||||
}
|
||||
|
||||
public function op(string $field, string $compare): string {
|
||||
return match ($compare) {
|
||||
'above' => "$field > ?",
|
||||
'below' => "$field < ?",
|
||||
'between' => "$field BETWEEN ? AND ?",
|
||||
'isnotnull' => "$field IS NOT NULL",
|
||||
'isnull' => "$field IS NULL",
|
||||
'no', 'not_equal' => "$field != ?",
|
||||
default => "$field = ?",
|
||||
};
|
||||
}
|
||||
|
||||
public function date(string $field, string $compare): string {
|
||||
return match ($compare) {
|
||||
'after' => "$field > ? + '1 DAY'::interval",
|
||||
'before' => "$field < ?",
|
||||
'between' => "$field BETWEEN ? AND ? + '1 DAY'::interval",
|
||||
default => "$field >= ? AND $field < ? + '1 DAY'::interval",
|
||||
};
|
||||
}
|
||||
}
|
||||
39
misc/pg-migrations/20250531000000_read_relay.php
Normal file
39
misc/pg-migrations/20250531000000_read_relay.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
|
||||
require_once __DIR__ . '/../../lib/config.php';
|
||||
// phpcs:enable PSR1.Files.SideEffects.FoundWithSymbols
|
||||
|
||||
final class ReadRelay extends AbstractMigration {
|
||||
public function up(): void {
|
||||
$login = MYSQL_RO_USER;
|
||||
$pass = MYSQL_RO_PASS;
|
||||
$pgUser = PG_RO_USER;
|
||||
|
||||
$this->query("
|
||||
create user mapping for $pgUser
|
||||
server relayer
|
||||
options (
|
||||
username '$login',
|
||||
password '$pass'
|
||||
)
|
||||
");
|
||||
$this->query("
|
||||
grant usage on schema relay to $pgUser
|
||||
");
|
||||
$this->query("
|
||||
grant select on all tables in schema relay to $pgUser
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
$pgUser = PG_RO_USER;
|
||||
$this->query("
|
||||
drop user mapping for $pgUser server relayer
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -39,49 +39,6 @@ foreach (['ip', 'email', 'username', 'comment'] as $field) {
|
||||
}
|
||||
}
|
||||
|
||||
class SQLMatcher {
|
||||
public function __construct(
|
||||
protected string $key,
|
||||
) {}
|
||||
|
||||
public function matchField(string $field): string {
|
||||
return match ($this->key) {
|
||||
'regexp' => "$field REGEXP ?",
|
||||
'strict' => "$field = ?",
|
||||
default => "$field LIKE concat('%', ?, '%')",
|
||||
};
|
||||
}
|
||||
|
||||
public function left_match(string $field): string {
|
||||
return match ($this->key) {
|
||||
'regexp' => "$field REGEXP ?",
|
||||
'strict' => "$field = ?",
|
||||
default => "$field LIKE concat(?, '%')",
|
||||
};
|
||||
}
|
||||
|
||||
public function op(string $field, string $compare): string {
|
||||
return match ($compare) {
|
||||
'above' => "$field > ?",
|
||||
'below' => "$field < ?",
|
||||
'between' => "$field BETWEEN ? AND ?",
|
||||
'isnotnull' => "$field IS NOT NULL",
|
||||
'isnull' => "$field IS NULL",
|
||||
'no', 'not_equal' => "$field != ?",
|
||||
default => "$field = ?",
|
||||
};
|
||||
}
|
||||
|
||||
public function date(string $field, string $compare): string {
|
||||
return match ($compare) {
|
||||
'after' => "$field > ? + INTERVAL 1 DAY",
|
||||
'before' => "$field < ?",
|
||||
'between' => "$field BETWEEN ? AND ? + INTERVAL 1 DAY",
|
||||
default => "$field >= ? AND $field < ? + INTERVAL 1 DAY",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function option(string $field, string $value, string $label): string {
|
||||
return sprintf('<option value="%s"%s>%s</option>',
|
||||
$value,
|
||||
@@ -89,7 +46,7 @@ function option(string $field, string $value, string $label): string {
|
||||
$label);
|
||||
}
|
||||
|
||||
$OrderTable = [
|
||||
$orderBy = [
|
||||
'Bounty' => 'Bounty',
|
||||
'Downloaded' => 'uls1.Downloaded',
|
||||
'Downloads' => 'Downloads',
|
||||
@@ -103,18 +60,18 @@ $OrderTable = [
|
||||
'Uploaded' => 'uls1.Uploaded',
|
||||
'Username' => 'um1.Username',
|
||||
];
|
||||
$WayTable = ['Ascending' => 'ASC', 'Descending' => 'DESC'];
|
||||
$dir = ['Ascending' => 'ASC', 'Descending' => 'DESC'];
|
||||
|
||||
// Arrays, regexps, and all that fun stuff we can use for validation, form generation, etc
|
||||
$OrderVals = ['inarray' => array_keys($OrderTable)];
|
||||
$WayVals = ['inarray' => array_keys($WayTable)];
|
||||
$orderByValue = ['inarray' => array_keys($orderBy)];
|
||||
$dirValue = ['inarray' => array_keys($dir)];
|
||||
|
||||
$DateChoices = ['inarray' => ['on', 'before', 'after', 'between']];
|
||||
$SingleDateChoices = ['inarray' => ['on', 'before', 'after']];
|
||||
$NumberChoices = ['inarray' => ['equal', 'above', 'below', 'between', 'buffer']];
|
||||
$OffNumberChoices = ['inarray' => ['equal', 'above', 'below', 'between', 'buffer', 'off']];
|
||||
$YesNo = ['inarray' => ['any', 'yes', 'no']];
|
||||
$Nullable = ['inarray' => ['any', 'isnull', 'isnotnull']];
|
||||
$dateChoice = ['inarray' => ['on', 'before', 'after', 'between']];
|
||||
$singledateChoice = ['inarray' => ['on', 'before', 'after']];
|
||||
$numberChoice = ['inarray' => ['equal', 'above', 'below', 'between', 'buffer']];
|
||||
$offNumberChoice = ['inarray' => ['equal', 'above', 'below', 'between', 'buffer', 'off']];
|
||||
$yesNo = ['inarray' => ['any', 'yes', 'no']];
|
||||
$nullable = ['inarray' => ['any', 'isnull', 'isnotnull']];
|
||||
|
||||
$emailHistoryChecked = false;
|
||||
$ipHistoryChecked = false;
|
||||
@@ -122,7 +79,7 @@ $disabledIpChecked = false;
|
||||
$trackerLiveSource = true;
|
||||
|
||||
$paginator = new Util\Paginator(USERS_PER_PAGE, (int)($_GET['page'] ?? 1));
|
||||
$Stylesheets = new \Gazelle\Manager\Stylesheet()->list();
|
||||
$stylesheet = new \Gazelle\Manager\Stylesheet()->list();
|
||||
|
||||
$matchMode = ($_GET['matchtype'] ?? 'fuzzy');
|
||||
$searchDisabledInvites = (isset($_GET['disabled_invites']) && $_GET['disabled_invites'] != '');
|
||||
@@ -131,353 +88,373 @@ $searchLockedAccount = (($_GET['lockedaccount'] ?? '') == 'locked');
|
||||
$showInvited = (($_GET['invited'] ?? 'off') !== 'off');
|
||||
|
||||
if (empty($_GET)) {
|
||||
$Results = [];
|
||||
$result = [];
|
||||
} else {
|
||||
$emailHistoryChecked = !empty($_GET['email_history']);
|
||||
$disabledIpChecked = !empty($_GET['disabled_ip']);
|
||||
$ipHistoryChecked = !empty($_GET['ip_history']);
|
||||
$trackerLiveSource = ($_GET['tracker-src'] ?? 'live') == 'live';
|
||||
$DateRegexp = ['regexp' => '/\d{4}-\d{2}-\d{2}/'];
|
||||
$ClassIDs = [];
|
||||
$SecClassIDs = [];
|
||||
$Classes = $userMan->classList();
|
||||
foreach ($Classes as $id => $value) {
|
||||
$disabledIpChecked = !empty($_GET['disabled_ip']);
|
||||
$ipHistoryChecked = !empty($_GET['ip_history']);
|
||||
$trackerLiveSource = ($_GET['tracker-src'] ?? 'live') == 'live';
|
||||
$dateRegexp = ['regexp' => '/\d{4}-\d{2}-\d{2}/'];
|
||||
$userclassList = [];
|
||||
$secClassList = [];
|
||||
foreach ($userMan->classList() as $id => $value) {
|
||||
if ($value['Secondary']) {
|
||||
$SecClassIDs[] = $id;
|
||||
$secClassList[] = $id;
|
||||
} else {
|
||||
$ClassIDs[] = $id;
|
||||
$userclassList[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
$validator = new Util\Validator();
|
||||
$validator->setFields([
|
||||
['avatar', false, 'string', 'Avatar URL too long', ['maxlength' => 512]],
|
||||
['bounty', false, 'inarray', "Invalid bounty field", $OffNumberChoices],
|
||||
['bounty', false, 'inarray', "Invalid bounty field", $offNumberChoice],
|
||||
['cc', false, 'inarray', 'Invalid Country Code', ['maxlength' => 2]],
|
||||
['comment', false, 'string', 'Comment is too long.', ['maxlength' => 512]],
|
||||
['disabled_invites', false, 'inarray', 'Invalid disabled_invites field', $YesNo],
|
||||
['disabled_uploads', false, 'inarray', 'Invalid disabled_uploads field', $YesNo],
|
||||
['downloaded', false, 'inarray', 'Invalid downloaded field', $NumberChoices],
|
||||
['disabled_invites', false, 'inarray', 'Invalid disabled_invites field', $yesNo],
|
||||
['disabled_uploads', false, 'inarray', 'Invalid disabled_uploads field', $yesNo],
|
||||
['downloaded', false, 'inarray', 'Invalid downloaded field', $numberChoice],
|
||||
['enabled', false, 'inarray', 'Invalid enabled field', ['inarray' => ['', 0, 1, 2]]],
|
||||
['join1', false, 'regexp', 'Invalid join1 field', $DateRegexp],
|
||||
['join2', false, 'regexp', 'Invalid join2 field', $DateRegexp],
|
||||
['joined', false, 'inarray', 'Invalid joined field', $DateChoices],
|
||||
['lastactive', false, 'inarray', 'Invalid lastactive field', $DateChoices],
|
||||
['lastactive1', false, 'regexp', 'Invalid lastactive1 field', $DateRegexp],
|
||||
['lastactive2', false, 'regexp', 'Invalid lastactive2 field', $DateRegexp],
|
||||
['join1', false, 'regexp', 'Invalid join1 field', $dateRegexp],
|
||||
['join2', false, 'regexp', 'Invalid join2 field', $dateRegexp],
|
||||
['joined', false, 'inarray', 'Invalid joined field', $dateChoice],
|
||||
['lastactive', false, 'inarray', 'Invalid lastactive field', $dateChoice],
|
||||
['lastactive1', false, 'regexp', 'Invalid lastactive1 field', $dateRegexp],
|
||||
['lastactive2', false, 'regexp', 'Invalid lastactive2 field', $dateRegexp],
|
||||
['lockedaccount', false, 'inarray', 'Invalid locked account field', ['inarray' => ['any', 'locked', 'unlocked']]],
|
||||
['matchtype', false, 'inarray', 'Invalid matchtype field', ['inarray' => ['strict', 'fuzzy', 'regexp']]],
|
||||
['order', false, 'inarray', 'Invalid ordering', $OrderVals],
|
||||
['order', false, 'inarray', 'Invalid ordering', $orderByValue],
|
||||
['passkey', false, 'string', 'Invalid passkey', ['maxlength' => 32]],
|
||||
['ratio', false, 'inarray', 'Invalid ratio field', $NumberChoices],
|
||||
['secclass', false, 'inarray', 'Invalid class', ['inarray' => $SecClassIDs]],
|
||||
['seeding', false, 'inarray', "Invalid seeding field", $OffNumberChoices],
|
||||
['snatched', false, 'inarray', "Invalid snatched field", $OffNumberChoices],
|
||||
['stylesheet', false, 'inarray', 'Invalid stylesheet', ['inarray' => array_keys($Stylesheets)]],
|
||||
['uploaded', false, 'inarray', 'Invalid uploaded field', $NumberChoices],
|
||||
['warned', false, 'inarray', 'Invalid warned field', $Nullable],
|
||||
['way', false, 'inarray', 'Invalid way', $WayVals],
|
||||
['ratio', false, 'inarray', 'Invalid ratio field', $numberChoice],
|
||||
['secclass', false, 'inarray', 'Invalid class', ['inarray' => $secClassList]],
|
||||
['seeding', false, 'inarray', "Invalid seeding field", $offNumberChoice],
|
||||
['snatched', false, 'inarray', "Invalid snatched field", $offNumberChoice],
|
||||
['stylesheet', false, 'inarray', 'Invalid stylesheet', ['inarray' => array_keys($stylesheet)]],
|
||||
['uploaded', false, 'inarray', 'Invalid uploaded field', $numberChoice],
|
||||
['warned', false, 'inarray', 'Invalid warned field', $nullable],
|
||||
['way', false, 'inarray', 'Invalid way', $dirValue],
|
||||
]);
|
||||
if (!$validator->validate($_GET)) {
|
||||
Error400::error($validator->errorMessage());
|
||||
}
|
||||
|
||||
$m = new SQLMatcher($matchMode);
|
||||
$Where = [];
|
||||
$Args = [];
|
||||
$Join = [];
|
||||
$Distinct = false;
|
||||
$Order = '';
|
||||
$m = new Search\User($matchMode);
|
||||
$where = [];
|
||||
$args = [];
|
||||
$join = [];
|
||||
$distinct = false;
|
||||
$order = '';
|
||||
|
||||
$invitedValue = $showInvited
|
||||
? '(SELECT count(*) FROM users_main AS umi WHERE umi.inviter_user_id = um1.ID)'
|
||||
? '(SELECT count(*) FROM relay.users_main AS umi WHERE umi.inviter_user_id = um1."ID")'
|
||||
: "'X'";
|
||||
$seedingValue = ($_GET['seeding'] ?? 'off') == 'off'
|
||||
? "'X'"
|
||||
: '(SELECT count(DISTINCT fid)
|
||||
FROM xbt_files_users xfu
|
||||
FROM relay.xbt_files_users xfu
|
||||
WHERE xfu.active = 1 AND xfu.remaining = 0 AND xfu.mtime > unix_timestamp(now() - INTERVAL 1 HOUR)
|
||||
AND xfu.uid = um1.ID)';
|
||||
AND xfu.uid = um1."ID")';
|
||||
$snatchesValue = ($_GET['snatched'] ?? 'off') == 'off'
|
||||
? "'X'"
|
||||
: '(SELECT count(DISTINCT fid) FROM xbt_snatched AS xs WHERE xs.uid = um1.ID)';
|
||||
: '(SELECT count(DISTINCT fid) FROM relay.xbt_snatched AS xs WHERE xs.uid = um1."ID")';
|
||||
|
||||
$columns = "
|
||||
um1.ID AS user_id,
|
||||
um1.\"ID\" AS user_id,
|
||||
$seedingValue AS seeding,
|
||||
$snatchesValue AS snatches,
|
||||
$invitedValue AS invited
|
||||
";
|
||||
|
||||
$from = "FROM users_main AS um1
|
||||
INNER JOIN permissions p ON (p.ID = um1.PermissionID)
|
||||
INNER JOIN users_leech_stats AS uls1 ON (uls1.UserID = um1.ID)
|
||||
INNER JOIN users_info AS ui1 ON (ui1.UserID = um1.ID)
|
||||
LEFT JOIN user_last_access AS ula ON (ula.user_id = um1.ID)
|
||||
";
|
||||
$from = 'FROM relay.users_main AS um1
|
||||
INNER JOIN relay.permissions p ON (p."ID" = um1."PermissionID")
|
||||
INNER JOIN relay.users_leech_stats AS uls1 ON (uls1."UserID" = um1."ID")
|
||||
INNER JOIN relay.users_info AS ui1 ON (ui1."UserID" = um1."ID")
|
||||
LEFT JOIN relay.user_last_access AS ula ON (ula.user_id = um1."ID")
|
||||
';
|
||||
|
||||
if (!empty($_GET['username'])) {
|
||||
$Where[] = $m->matchField('um1.Username');
|
||||
$Args[] = $_GET['username'];
|
||||
$where[] = $m->matchField('um1."Username"');
|
||||
$args[] = $_GET['username'];
|
||||
}
|
||||
|
||||
if (!empty($_GET['email'])) {
|
||||
if (isset($_GET['email_history'])) {
|
||||
$Distinct = true;
|
||||
$Join['he'] = 'INNER JOIN users_history_emails AS he ON (he.UserID = um1.ID)';
|
||||
$Where[] = $m->matchField('he.Email');
|
||||
$distinct = true;
|
||||
$join['he'] = 'INNER JOIN relay.users_history_emails AS he ON (he."UserID" = um1."ID")';
|
||||
$where[] = $m->matchField('he."Email"');
|
||||
} else {
|
||||
$Where[] = $m->matchField('um1.Email');
|
||||
$where[] = $m->matchField('um1."Email"');
|
||||
}
|
||||
$Args[] = $_GET['email'];
|
||||
$args[] = $_GET['email'];
|
||||
}
|
||||
|
||||
if (isset($_GET['email_opt']) && isset($_GET['email_cnt']) && strlen($_GET['email_cnt'])) {
|
||||
$Where[] = sprintf('um1.ID IN (%s)',
|
||||
$m->op("
|
||||
SELECT UserID FROM users_history_emails GROUP BY UserID HAVING count(DISTINCT Email)
|
||||
", $_GET['emails_opt']
|
||||
$where[] = sprintf('um1."ID" IN (%s)',
|
||||
$m->op('
|
||||
SELECT "UserID" FROM relay.users_history_emails GROUP BY "UserID" HAVING count(DISTINCT "Email")
|
||||
', $_GET['emails_opt']
|
||||
)
|
||||
);
|
||||
$Args[] = (int)$_GET['email_cnt'];
|
||||
$args[] = (int)$_GET['email_cnt'];
|
||||
}
|
||||
|
||||
if (!empty($_GET['ip'])) {
|
||||
if ($ipHistoryChecked) {
|
||||
$Distinct = true;
|
||||
$Join['hi'] = 'INNER JOIN users_history_ips AS hi ON (hi.UserID = um1.ID)';
|
||||
$Where[] = $m->left_match('hi.IP');
|
||||
$distinct = true;
|
||||
$join['hi'] = 'INNER JOIN relay.users_history_ips AS hi ON (hi."UserID" = um1."ID")';
|
||||
$where[] = $m->leftMatch('hi."IP"');
|
||||
} else {
|
||||
$Where[] = $m->left_match('um1.IP');
|
||||
$where[] = $m->leftMatch('um1."IP"');
|
||||
}
|
||||
$Args[] = trim($_GET['ip']);
|
||||
$args[] = trim($_GET['ip']);
|
||||
}
|
||||
|
||||
if ($searchLockedAccount) {
|
||||
$Join['la'] = 'INNER JOIN locked_accounts AS la ON (la.UserID = um1.ID)';
|
||||
$join['la'] = 'INNER JOIN relay.locked_accounts AS la ON (la."UserID" = um1."ID")';
|
||||
} elseif (isset($_GET['lockedaccount']) && $_GET['lockedaccount'] == 'unlocked') {
|
||||
$Join['la'] = 'LEFT JOIN locked_accounts AS la ON (la.UserID = um1.ID)';
|
||||
$Where[] = 'la.UserID IS NULL';
|
||||
$join['la'] = 'LEFT JOIN relay.locked_accounts AS la ON (la."UserID" = um1."ID")';
|
||||
$where[] = 'la."UserID" IS NULL';
|
||||
}
|
||||
|
||||
if (!empty($_GET['cc'])) {
|
||||
$Where[] = $m->op('um1.ipcc', $_GET['cc_op']);
|
||||
$Args[] = trim($_GET['cc']);
|
||||
$where[] = $m->op('um1.ipcc', $_GET['cc_op']);
|
||||
$args[] = trim($_GET['cc']);
|
||||
}
|
||||
|
||||
if (!empty($_GET['tracker_ip'])) {
|
||||
$Distinct = true;
|
||||
$Join['xfu'] = $trackerLiveSource
|
||||
? 'INNER JOIN xbt_files_users AS xfu ON (um1.ID = xfu.uid)'
|
||||
: 'INNER JOIN xbt_snatched AS xfu ON (um1.ID = xfu.uid)';
|
||||
$Where[] = $m->left_match('xfu.ip');
|
||||
$Args[] = trim($_GET['tracker_ip']);
|
||||
$distinct = true;
|
||||
$join['xfu'] = $trackerLiveSource
|
||||
? 'INNER JOIN relay.xbt_files_users AS xfu ON (um1."ID" = xfu.uid)'
|
||||
: 'INNER JOIN relay.xbt_snatched AS xfu ON (um1."ID" = xfu.uid)';
|
||||
$where[] = $m->leftMatch('xfu.ip');
|
||||
$args[] = trim($_GET['tracker_ip']);
|
||||
}
|
||||
|
||||
if (!empty($_GET['comment'])) {
|
||||
$distinct = true;
|
||||
$join['audit'] = 'inner join user_audit_trail uat on (uat.id_user = um1."ID")';
|
||||
$where[] = "note_ts @@ websearch_to_tsquery('simple', ?)";
|
||||
$args[] = $_GET['comment'];
|
||||
}
|
||||
|
||||
if (!empty($_GET['lastfm'])) {
|
||||
$Distinct = true;
|
||||
$Join['lfm'] = 'INNER JOIN lastfm_users AS lfm ON (lfm.ID = um1.ID)';
|
||||
$Where[] = $m->matchField('lfm.Username');
|
||||
$Args[] = $_GET['lastfm'];
|
||||
$distinct = true;
|
||||
$join['lfm'] = 'INNER JOIN relay.lastfm_users AS lfm ON (lfm."ID" = um1."ID")';
|
||||
$where[] = $m->matchField('lfm."Username"');
|
||||
$args[] = $_GET['lastfm'];
|
||||
}
|
||||
|
||||
if (isset($_GET['invites']) && !empty($_GET['invites']) && isset($_GET['invites1']) && strlen($_GET['invites1'])) {
|
||||
$op = $_GET['invites'];
|
||||
$Where[] = $m->op('um1.Invites', $op);
|
||||
$Args = array_merge($Args, [$_GET['invites1']], ($op === 'between' ? [$_GET['invites2']] : []));
|
||||
$where[] = $m->op('um1."Invites"', $op);
|
||||
$args = array_merge($args, [$_GET['invites1']], ($op === 'between' ? [$_GET['invites2']] : []));
|
||||
}
|
||||
|
||||
if ($showInvited && isset($_GET['invited1']) && strlen($_GET['invited1'])) {
|
||||
$op = $_GET['invited'];
|
||||
$Where[] = "um1.ID IN ("
|
||||
. $m->op("SELECT umi.ID FROM users_main umii INNER JOIN users_main umi ON (umi.ID = umii.inviter_user_id) GROUP BY umi.ID HAVING count(*)", $op)
|
||||
. ")";
|
||||
$Args = array_merge($Args, [$_GET['invited1']], ($op === 'between' ? [$_GET['invited2']] : []));
|
||||
$where[] = 'um1.ID IN ('
|
||||
. $m->op('SELECT umi."ID"
|
||||
FROM relay.users_main umii
|
||||
INNER JOIN relay.users_main umi ON (umi."ID" = umii.inviter_user_id)
|
||||
GROUP BY umi."ID"
|
||||
HAVING count(*)', $op
|
||||
) . ')';
|
||||
$args = array_merge($args, [$_GET['invited1']], ($op === 'between' ? [$_GET['invited2']] : []));
|
||||
}
|
||||
|
||||
if ($searchDisabledInvites) {
|
||||
$allow = $_GET['disabled_invites'] == 'yes' ? '' : 'NOT ';
|
||||
$Where[] = "$allow EXISTS(SELECT 1 FROM user_has_attr uha_invite where uha_invite.UserID = um1.ID and uha_invite.UserAttrID = (SELECT ID FROM user_attr WHERE Name = ?))";
|
||||
$Args[] = 'disable-invites';
|
||||
$where[] = $allow . ' EXISTS(
|
||||
SELECT 1
|
||||
FROM relay.user_has_attr uha_invite
|
||||
WHERE uha_invite."UserID" = um1."ID"
|
||||
AND uha_invite."UserAttrID" = (SELECT "ID" FROM user_attr WHERE "Name" = ?)
|
||||
)';
|
||||
$args[] = 'disable-invites';
|
||||
}
|
||||
|
||||
if ($searchDisabledUploads) {
|
||||
$allow = $_GET['disabled_uploads'] == 'yes' ? '' : 'NOT ';
|
||||
$Where[] = "$allow EXISTS(SELECT 1 FROM user_has_attr uha_upload where uha_upload.UserID = um1.ID and uha_upload.UserAttrID = (SELECT ID FROM user_attr WHERE Name = ?))";
|
||||
$Args[] = 'disable-upload';
|
||||
$where[] = $allow . ' EXISTS(
|
||||
SELECT 1
|
||||
FROM relay.user_has_attr uha_upload
|
||||
WHERE uha_upload."UserID" = um1."ID"
|
||||
AND uha_upload."UserAttrID" = (SELECT "ID" FROM user_attr WHERE "Name" = ?)
|
||||
)';
|
||||
$args[] = 'disable-upload';
|
||||
}
|
||||
|
||||
if (isset($_GET['joined']) && !empty($_GET['joined']) && isset($_GET['join1']) && !empty($_GET['join1'])) {
|
||||
$op = $_GET['joined'];
|
||||
$Where[] = $m->date('um1.created', $op);
|
||||
$Args[] = $_GET['join1'];
|
||||
$where[] = $m->date('um1.created', $op);
|
||||
$args[] = $_GET['join1'];
|
||||
if ($op === 'on') {
|
||||
$Args[] = $_GET['join1'];
|
||||
$args[] = $_GET['join1'];
|
||||
} elseif ($op === 'between') {
|
||||
$Args[] = $_GET['join2'];
|
||||
$args[] = $_GET['join2'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['lastactive']) && !empty($_GET['lastactive']) && isset($_GET['lastactive1']) && !empty($_GET['lastactive1'])) {
|
||||
$op = $_GET['lastactive'];
|
||||
$Where[] = $m->date('ula.last_access', $op);
|
||||
$Args[] = $_GET['lastactive1'];
|
||||
$where[] = $m->date('ula.last_access', $op);
|
||||
$args[] = $_GET['lastactive1'];
|
||||
if ($op === 'on') {
|
||||
$Args[] = $_GET['lastactive1'];
|
||||
$args[] = $_GET['lastactive1'];
|
||||
} elseif ($op === 'between') {
|
||||
$Args[] = $_GET['lastactive2'];
|
||||
$args[] = $_GET['lastactive2'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['ratio']) && !empty($_GET['ratio']) && isset($_GET['ratio1']) && strlen($_GET['ratio1'])) {
|
||||
$frac = explode('.', $_GET['ratio1']);
|
||||
$Decimals = strlen(end($frac));
|
||||
if (!$Decimals) {
|
||||
$Decimals = 0;
|
||||
$decimals = strlen(end($frac));
|
||||
if (!$decimals) {
|
||||
$decimals = 0;
|
||||
}
|
||||
$op = $_GET['ratio'];
|
||||
$Where[] = $m->op('CASE WHEN uls1.Downloaded = 0 then 0 ELSE round(uls1.Uploaded/uls1.Downloaded, ?) END', $op);
|
||||
$Args = array_merge($Args, [$Decimals, $_GET['ratio1']], ($op === 'between' ? [$_GET['ratio2']] : []));
|
||||
$where[] = $m->op('CASE WHEN uls1."Downloaded" = 0 then 0 ELSE round(uls1."Uploaded"/uls1."Downloaded", ?) END', $op);
|
||||
$args = array_merge($args, [$decimals, $_GET['ratio1']], ($op === 'between' ? [$_GET['ratio2']] : []));
|
||||
}
|
||||
|
||||
if (isset($_GET['bounty']) && !empty($_GET['bounty']) && $_GET['bounty'] !== 'off' && isset($_GET['bounty1']) && strlen($_GET['bounty1'])) {
|
||||
$op = $_GET['bounty'];
|
||||
$Where[] = $m->op('(SELECT sum(Bounty) FROM requests_votes rv WHERE rv.UserID = um1.ID)', $op);
|
||||
$Args = array_merge($Args, [$_GET['bounty1'] * 1024 ** 3], ($op === 'between' ? [$_GET['bounty2'] * 1024 ** 3] : []));
|
||||
$where[] = $m->op('(SELECT sum("Bounty") FROM relay.requests_votes rv WHERE rv."UserID" = um1."ID")', $op);
|
||||
$args = array_merge($args, [$_GET['bounty1'] * 1024 ** 3], ($op === 'between' ? [$_GET['bounty2'] * 1024 ** 3] : []));
|
||||
}
|
||||
|
||||
if (isset($_GET['downloads']) && !empty($_GET['downloads']) && $_GET['downloads'] !== 'off' && isset($_GET['downloads1']) && strlen($_GET['downloads1'])) {
|
||||
$op = $_GET['downloads'];
|
||||
$Where[] = $m->op('(SELECT count(DISTINCT TorrentID) FROM users_downloads ud WHERE ud.UserID = um1.ID)', $op);
|
||||
$Args = array_merge($Args, [$_GET['downloads1']], ($op === 'between' ? [$_GET['downloads2']] : []));
|
||||
$where[] = $m->op('(SELECT count(DISTINCT "TorrentID") FROM relay.users_downloads ud WHERE ud."UserID" = um1."ID")', $op);
|
||||
$args = array_merge($args, [$_GET['downloads1']], ($op === 'between' ? [$_GET['downloads2']] : []));
|
||||
}
|
||||
|
||||
if (isset($_GET['seeding']) && $_GET['seeding'] !== 'off' && isset($_GET['seeding1'])) {
|
||||
$op = $_GET['seeding'];
|
||||
$Where[] = $m->op('(SELECT count(DISTINCT fid)
|
||||
FROM xbt_files_users xfu
|
||||
$where[] = $m->op('(SELECT count(DISTINCT fid)
|
||||
FROM relay.xbt_files_users xfu
|
||||
WHERE xfu.active = 1 AND xfu.remaining = 0 AND xfu.mtime > unix_timestamp(now() - INTERVAL 1 HOUR)
|
||||
AND xfu.uid = um1.ID)', $op);
|
||||
$Args = array_merge($Args, [$_GET['seeding1']], ($op === 'between' ? [$_GET['seeding2']] : []));
|
||||
AND xfu.uid = um1."ID")', $op);
|
||||
$args = array_merge($args, [$_GET['seeding1']], ($op === 'between' ? [$_GET['seeding2']] : []));
|
||||
}
|
||||
|
||||
if (isset($_GET['snatched']) && $_GET['snatched'] !== 'off' && isset($_GET['snatched1'])) {
|
||||
$op = $_GET['snatched'];
|
||||
$Where[] = $m->op('(SELECT count(DISTINCT fid) FROM xbt_snatched AS xs WHERE xs.uid = um1.ID)', $op);
|
||||
$Args = array_merge($Args, [$_GET['snatched1']], ($op === 'between' ? [$_GET['snatched2']] : []));
|
||||
$where[] = $m->op('(SELECT count(DISTINCT fid) FROM relay.xbt_snatched AS xs WHERE xs.uid = um1."ID")', $op);
|
||||
$args = array_merge($args, [$_GET['snatched1']], ($op === 'between' ? [$_GET['snatched2']] : []));
|
||||
}
|
||||
|
||||
if (isset($_GET['uploaded']) && !empty($_GET['uploaded']) && isset($_GET['uploaded1']) && strlen($_GET['uploaded1'])) {
|
||||
$op = $_GET['uploaded'];
|
||||
if ($op === 'buffer') {
|
||||
$Where[] = 'uls1.Uploaded - uls1.Downloaded BETWEEN ? AND ?';
|
||||
$Args = array_merge($Args, [0.9 * $_GET['uploaded1'] * 1024 ** 3, 1.1 * $_GET['uploaded1'] * 1024 ** 3]);
|
||||
$where[] = 'uls1."Uploaded" - uls1."Downloaded" BETWEEN ? AND ?';
|
||||
$args = array_merge($args, [0.9 * $_GET['uploaded1'] * 1024 ** 3, 1.1 * $_GET['uploaded1'] * 1024 ** 3]);
|
||||
} else {
|
||||
$Where[] = $m->op('uls1.Uploaded', $op);
|
||||
$Args[] = $_GET['uploaded1'] * 1024 ** 3;
|
||||
$where[] = $m->op('uls1."Uploaded"', $op);
|
||||
$args[] = $_GET['uploaded1'] * 1024 ** 3;
|
||||
if ($op === 'on') {
|
||||
$Args[] = $_GET['uploaded1'] * 1024 ** 3;
|
||||
$args[] = $_GET['uploaded1'] * 1024 ** 3;
|
||||
} elseif ($op === 'between') {
|
||||
$Args[] = $_GET['uploaded2'] * 1024 ** 3;
|
||||
$args[] = $_GET['uploaded2'] * 1024 ** 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['downloaded']) && !empty($_GET['downloaded']) && isset($_GET['downloaded1']) && strlen($_GET['downloaded1'])) {
|
||||
$op = $_GET['downloaded'];
|
||||
$Where[] = $m->op('uls1.Downloaded', $op);
|
||||
$Args[] = $_GET['downloaded1'] * 1024 ** 3;
|
||||
$where[] = $m->op('uls1."Downloaded"', $op);
|
||||
$args[] = $_GET['downloaded1'] * 1024 ** 3;
|
||||
if ($op === 'on') {
|
||||
$Args[] = $_GET['downloaded1'] * 1024 ** 3;
|
||||
$args[] = $_GET['downloaded1'] * 1024 ** 3;
|
||||
} elseif ($op === 'between') {
|
||||
$Args[] = $_GET['downloaded2'] * 1024 ** 3;
|
||||
$args[] = $_GET['downloaded2'] * 1024 ** 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['enabled']) && $_GET['enabled'] != '') {
|
||||
$Where[] = 'um1.Enabled = ?';
|
||||
$Args[] = $_GET['enabled'];
|
||||
$where[] = 'um1."Enabled" = ?';
|
||||
$args[] = $_GET['enabled'];
|
||||
}
|
||||
|
||||
if (isset($_GET['class']) && is_array($_GET['class'])) {
|
||||
$Where[] = 'um1.PermissionID IN (' . placeholders($_GET['class']) . ')';
|
||||
$Args = array_merge($Args, $_GET['class']);
|
||||
$where[] = 'um1."PermissionID" IN (' . placeholders($_GET['class']) . ')';
|
||||
$args = array_merge($args, $_GET['class']);
|
||||
}
|
||||
|
||||
if (isset($_GET['secclass']) && $_GET['secclass'] != '') {
|
||||
$Join['ul'] = 'INNER JOIN users_levels AS ul ON (um1.ID = ul.UserID)';
|
||||
$Where[] = 'ul.PermissionID = ?';
|
||||
$Args[] = $_GET['secclass'];
|
||||
$join['ul'] = 'INNER JOIN relay.users_levels AS ul ON (um1."ID" = ul."UserID")';
|
||||
$where[] = 'ul."PermissionID" = ?';
|
||||
$args[] = $_GET['secclass'];
|
||||
}
|
||||
|
||||
if (isset($_GET['warned']) && !empty($_GET['warned'])) {
|
||||
$Where[] = $m->op('ui1.Warned', $_GET['warned']);
|
||||
$where[] = $m->op('ui1."Warned"', $_GET['warned']);
|
||||
}
|
||||
|
||||
if ($disabledIpChecked) {
|
||||
$Distinct = true;
|
||||
$distinct = true;
|
||||
if ($ipHistoryChecked) {
|
||||
if (!isset($Join['hi'])) {
|
||||
$Join['hi'] = 'LEFT JOIN users_history_ips AS hi ON (hi.UserID = um1.ID)';
|
||||
if (!isset($join['hi'])) {
|
||||
$join['hi'] = 'LEFT JOIN relay.users_history_ips AS hi ON (hi."UserID" = um1."ID")';
|
||||
}
|
||||
$Join['um2'] = 'LEFT JOIN users_main AS um2 ON (um2.ID != um1.ID AND um2.Enabled = \'2\' AND um2.ID = hi.UserID)';
|
||||
$join['um2'] = 'LEFT JOIN relay.users_main AS um2 ON (um2."ID" != um1."ID" AND um2."Enabled" = \'2\' AND um2."ID" = hi."UserID")';
|
||||
} else {
|
||||
$Join['um2'] = 'LEFT JOIN users_main AS um2 ON (um2.ID != um1.ID AND um2.Enabled = \'2\' AND um2.IP = um1.IP)';
|
||||
$join['um2'] = 'LEFT JOIN relay.users_main AS um2 ON (um2."ID" != um1."ID" AND um2."Enabled" = \'2\' AND um2."IP" = um1."IP")';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['passkey']) && !empty($_GET['passkey'])) {
|
||||
$Where[] = $m->matchField('um1.torrent_pass');
|
||||
$Args[] = $_GET['passkey'];
|
||||
$where[] = $m->matchField('um1.torrent_pass');
|
||||
$args[] = $_GET['passkey'];
|
||||
}
|
||||
|
||||
if (isset($_GET['avatar']) && !empty($_GET['avatar'])) {
|
||||
$Where[] = $m->matchField('um1.avatar');
|
||||
$Args[] = $_GET['avatar'];
|
||||
$where[] = $m->matchField('um1.avatar');
|
||||
$args[] = $_GET['avatar'];
|
||||
}
|
||||
|
||||
if (isset($_GET['stylesheet']) && !empty($_GET['stylesheet'])) {
|
||||
$Where[] = $m->matchField('um1.stylesheet_id');
|
||||
$Args[] = $_GET['stylesheet'];
|
||||
$where[] = $m->matchField('um1.stylesheet_id');
|
||||
$args[] = $_GET['stylesheet'];
|
||||
}
|
||||
|
||||
$Order = 'ORDER BY ' . $OrderTable[$_GET['order'] ?? 'Joined'] . ' ' . $WayTable[$_GET['way'] ?? 'Descending'];
|
||||
$order = 'ORDER BY ' . $orderBy[$_GET['order'] ?? 'Joined'] . ' ' . $dir[$_GET['way'] ?? 'Descending'];
|
||||
|
||||
//---------- Build the query
|
||||
$SQL = "SELECT count(*) $from " . implode("\n", $Join);
|
||||
if (count($Where)) {
|
||||
$SQL .= "WHERE " . implode("\nAND ", $Where);
|
||||
$query = "SELECT count(*) $from " . implode("\n", $join);
|
||||
if (count($where)) {
|
||||
$query .= " WHERE " . implode("\nAND ", $where);
|
||||
}
|
||||
if ($Distinct) {
|
||||
$SQL .= "\nGROUP BY um1.ID";
|
||||
if ($distinct) {
|
||||
$query .= ' GROUP BY um1."ID"';
|
||||
}
|
||||
$db = DB::DB();
|
||||
$paginator->setTotal((int)$db->scalar($SQL, ...$Args));
|
||||
|
||||
$SQL = "SELECT $columns $from " . implode("\n", $Join);
|
||||
if (count($Where)) {
|
||||
$SQL .= "WHERE " . implode("\nAND ", $Where);
|
||||
}
|
||||
if ($Distinct) {
|
||||
$SQL .= "\nGROUP BY um1.ID";
|
||||
}
|
||||
$SQL .= "\n$Order LIMIT ? OFFSET ?";
|
||||
$pg = new DB\Pg(PG_RW_DSN);
|
||||
$paginator->setTotal((int)$pg->scalar($query, ...$args));
|
||||
|
||||
$db->prepared_query($SQL, ...array_merge($Args, [$paginator->limit(), $paginator->offset()]));
|
||||
$Results = $db->to_array(false, MYSQLI_ASSOC);
|
||||
foreach ($Results as &$r) {
|
||||
$query = "SELECT $columns $from " . implode("\n", $join);
|
||||
if (count($where)) {
|
||||
$query .= ' WHERE ' . implode("\nAND ", $where);
|
||||
}
|
||||
if ($distinct) {
|
||||
$query .= 'GROUP BY um1."ID", um1.created';
|
||||
}
|
||||
$query .= " $order LIMIT ? OFFSET ?";
|
||||
|
||||
$result = $pg->all($query, ...array_merge($args, [$paginator->limit(), $paginator->offset()]));
|
||||
foreach ($result as &$r) {
|
||||
$r['user'] = $userMan->findById($r['user_id']);
|
||||
}
|
||||
unset($r);
|
||||
}
|
||||
|
||||
// Neither level nor ID is particularly useful when searching secondary classes, so sort them alphabetically.
|
||||
$ClassLevels = $userMan->classLevelList();
|
||||
$Secondaries = array_filter($ClassLevels, fn ($c) => $c['Secondary'] == '1');
|
||||
usort($Secondaries, fn($c1, $c2) => $c1['Name'] <=> $c2['Name']);
|
||||
$userclassLevel = $userMan->classLevelList();
|
||||
$secondary = array_filter($userclassLevel, fn ($c) => $c['Secondary'] == '1');
|
||||
usort($secondary, fn($c1, $c2) => $c1['Name'] <=> $c2['Name']);
|
||||
|
||||
echo $Twig->render('admin/advanced-user-search.twig', [
|
||||
'page' => $Results,
|
||||
'page' => $result,
|
||||
'paginator' => $paginator,
|
||||
'show_invited' => $showInvited,
|
||||
'url_stem' => new User\Stylesheet($Viewer)->imagePath(),
|
||||
@@ -490,12 +467,12 @@ echo $Twig->render('admin/advanced-user-search.twig', [
|
||||
'check_email_history' => $emailHistoryChecked,
|
||||
|
||||
// third column
|
||||
'primary_class' => array_filter($ClassLevels, fn ($c) => $c['Secondary'] == '0'),
|
||||
'secondary_class' => $Secondaries,
|
||||
'stylesheet' => $Stylesheets,
|
||||
'primary_class' => array_filter($userclassLevel, fn ($c) => $c['Secondary'] == '0'),
|
||||
'secondary_class' => $secondary,
|
||||
'stylesheet' => $stylesheet,
|
||||
'match_mode' => $matchMode,
|
||||
|
||||
// sorting widgets
|
||||
'field_by' => array_shift($OrderVals),
|
||||
'order_by' => array_shift($WayVals),
|
||||
'field_by' => array_shift($orderByValue),
|
||||
'order_by' => array_shift($dirValue),
|
||||
]);
|
||||
|
||||
104
tests/phpunit/search/UserAdvancedTest.php
Normal file
104
tests/phpunit/search/UserAdvancedTest.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use GazelleUnitTest\Helper;
|
||||
|
||||
class UserAdvancedTest extends TestCase {
|
||||
protected User $user;
|
||||
|
||||
public function setUp(): void {
|
||||
$this->user = Helper::makeUser('usearch.' . randomString(10), 'usersearch');
|
||||
}
|
||||
|
||||
public function tearDown(): void {
|
||||
$this->user->remove();
|
||||
}
|
||||
|
||||
public function testSearchUserFuzzy(): void {
|
||||
$fuzzy = new Search\User('');
|
||||
$this->assertEquals(
|
||||
"f ~~* concat('%', ?::text, '%')",
|
||||
$fuzzy->matchField('f'),
|
||||
'usearch-fuzzy-match-field',
|
||||
);
|
||||
$this->assertEquals(
|
||||
"f ~~* concat(?::text, '%')",
|
||||
$fuzzy->leftMatch('f'),
|
||||
'usearch-fuzzy-match-left',
|
||||
);
|
||||
}
|
||||
|
||||
public function testSearchUserRegexp(): void {
|
||||
$regexp = new Search\User('regexp');
|
||||
$this->assertEquals(
|
||||
"f ~* ?",
|
||||
$regexp->matchField('f'),
|
||||
'usearch-regexp-match-field',
|
||||
);
|
||||
$this->assertEquals(
|
||||
// FIXME: should be ^ anchored
|
||||
"f ~* ?",
|
||||
$regexp->leftMatch('f'),
|
||||
'usearch-regexp-match-left',
|
||||
);
|
||||
}
|
||||
|
||||
public function testSearchUserStrict(): void {
|
||||
$strict = new Search\User('strict');
|
||||
$this->assertEquals(
|
||||
"f = ?",
|
||||
$strict->matchField('f'),
|
||||
'usearch-strict-match-field',
|
||||
);
|
||||
$this->assertEquals(
|
||||
// FIXME: should be left substring
|
||||
"f = ?",
|
||||
$strict->leftMatch('f'),
|
||||
'usearch-strict-match-left',
|
||||
);
|
||||
}
|
||||
|
||||
#[DataProvider('dataDate')]
|
||||
public function testSearchUserDate(string $name, string $compare, string $expected): void {
|
||||
$search = new Search\User('');
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
new Search\User('')->date('f', $compare),
|
||||
$name,
|
||||
);
|
||||
}
|
||||
|
||||
public static function dataDate(): array {
|
||||
return [
|
||||
['usearch-date-after', 'after', "f > ? + '1 DAY'::interval"],
|
||||
['usearch-date-before', 'before', "f < ?"],
|
||||
['usearch-date-between', 'between', "f BETWEEN ? AND ? + '1 DAY'::interval"],
|
||||
['usearch-date-default', 'default', "f >= ? AND f < ? + '1 DAY'::interval"],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('dataOp')]
|
||||
public function testOp(string $name, string $compare, string $expected): void {
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
new Search\User('')->op('f', $compare),
|
||||
$name,
|
||||
);
|
||||
}
|
||||
|
||||
public static function dataOp(): array {
|
||||
return [
|
||||
['usearch-op-equal', 'equal', 'f = ?'],
|
||||
['usearch-op-above', 'above', 'f > ?'],
|
||||
['usearch-op-below', 'below', 'f < ?'],
|
||||
['usearch-op-between', 'between', 'f BETWEEN ? AND ?'],
|
||||
['usearch-op-isnotnull', 'isnotnull', 'f IS NOT NULL'],
|
||||
['usearch-op-isnull', 'isnull', 'f IS NULL'],
|
||||
['usearch-op-no', 'no', 'f != ?'],
|
||||
['usearch-op-not_equal', 'not_equal', 'f != ?'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user