diff --git a/app/Manager/Invite.php b/app/Manager/Invite.php index 660533510..d6afd648e 100644 --- a/app/Manager/Invite.php +++ b/app/Manager/Invite.php @@ -15,7 +15,7 @@ class Invite extends \Gazelle\Base { INSERT INTO invites (InviterID, InviteKey, Email, Notes, Reason, Expires) VALUES (?, ?, ?, ?, ?, now() + INTERVAL 3 DAY) - ", $user->id(), $inviteKey, $email, $notes, $reason + ", $user->id, $inviteKey, $email, $notes, $reason ); $invite = new \Gazelle\Invite($inviteKey); if (is_number($source)) { @@ -25,7 +25,7 @@ class Invite extends \Gazelle\Base { return $invite; } - public function findUserByKey(string $inviteKey, User $manager): ?\Gazelle\User { + public function findUserByKey(string $inviteKey, User $manager = new User()): ?\Gazelle\User { return $manager->findById( (int)self::$db->scalar(" SELECT InviterID FROM invites WHERE InviteKey = ? @@ -59,7 +59,7 @@ class Invite extends \Gazelle\Base { FROM invites WHERE InviterID = ? AND Email = ? - ", $user->id(), $email + ", $user->id, $email ); } @@ -86,12 +86,15 @@ class Invite extends \Gazelle\Base { self::$db->prepared_query(" SELECT i.InviterID AS user_id, - um.IP AS ipaddr, - i.InviteKey AS `key`, - i.Expires AS expires, - i.Email AS email - FROM invites AS i + um.IP AS ipaddr, + i.InviteKey AS `key`, + i.Expires AS expires, + i.Email AS email, + ivs.name AS source_name + FROM invites i INNER JOIN users_main AS um ON (um.ID = i.InviterID) + LEFT JOIN invite_source_pending ivsp ON (ivsp.invite_key = i.InviteKey) + LEFT JOIN invite_source ivs USING (invite_source_id) $where ORDER BY i.Expires DESC LIMIT ? OFFSET ? @@ -101,7 +104,7 @@ class Invite extends \Gazelle\Base { } /** - * Remove an invite + * Remove an invite without restoring it to the issuer * * @return bool true if something was actually removed */ @@ -115,25 +118,24 @@ class Invite extends \Gazelle\Base { } /** - * Expire unused invitations + * Expire unused invitations and return them to the user */ - public function expire(\Gazelle\Task|null $task = null): int { - self::$db->begin_transaction(); - self::$db->prepared_query("SELECT InviterID FROM invites WHERE Expires < now()"); - $list = self::$db->collect(0, false); - - self::$db->prepared_query("DELETE FROM invites WHERE Expires < now()"); - self::$db->prepared_query(" - DELETE isp FROM invite_source_pending isp - LEFT JOIN invites i ON (i.InviteKey = isp.invite_key) - WHERE i.InviteKey IS NULL - "); - + public function expire(\Gazelle\Task|null $task = null, User $manager = new User()): int { $expired = 0; - foreach ($list as $userId) { - self::$db->prepared_query("UPDATE users_main SET Invites = Invites + 1 WHERE ID = ?", $userId); - self::$cache->delete_value("u_$userId"); - $task?->debug("Expired invite from user $userId", $userId); + self::$db->begin_transaction(); + self::$db->prepared_query(" + SELECT InviterID AS 'user_id', + InviteKey AS 'invite_key' + FROM invites + WHERE Expires < now() + "); + foreach (self::$db->to_array(false, MYSQLI_ASSOC, false) as $row) { + $user = $manager->findById($row['user_id']); + if (is_null($user)) { + continue; + } + $user->invite()->revoke($row['invite_key']); + $task?->debug("Expired invite {$row['invite_key']} for user {$user->username()}", $row['user_id']); $expired++; } self::$db->commit(); diff --git a/app/Manager/Registration.php b/app/Manager/Registration.php index e25a2cfc8..4c6d860bf 100644 --- a/app/Manager/Registration.php +++ b/app/Manager/Registration.php @@ -9,7 +9,7 @@ class Registration extends \Gazelle\Base { protected string $afterDate; public function __construct( - protected User $manager, + protected User $manager = new User(), ) {} public function setBeforeDate(string $date): static { diff --git a/app/User.php b/app/User.php index 44ab4c196..5f83357cc 100644 --- a/app/User.php +++ b/app/User.php @@ -12,7 +12,7 @@ use Gazelle\Util\Time; class User extends BaseObject { final public const tableName = 'users_main'; - final protected const CACHE_KEY = 'u2_%d'; + final protected const CACHE_KEY = 'u_%d'; final protected const CACHE_NOTIFY = 'u_notify_%d'; final protected const CACHE_REFERRAL = 'u_refer_%d'; final protected const USER_RECENT_UPLOAD = 'u_recent_up_%d'; @@ -46,7 +46,7 @@ class User extends BaseObject { $this->stats()->flush(); $this->ordinal()->flush(); $this->privilege()->flush(); - unset($this->info, $this->ordinal, $this->privilege, $this->stats, $this->tokenCache); + unset($this->info, $this->invite, $this->ordinal, $this->privilege, $this->stats, $this->tokenCache); return $this; } @@ -157,6 +157,7 @@ class User extends BaseObject { um.torrent_pass, um.updated, um.Visible, + um.ipcc, ui.AdminComment, ui.BanDate, ui.NavItems, @@ -472,6 +473,10 @@ class User extends BaseObject { return $this->info()['IP']; } + public function ipCountryIso(): string { + return $this->info()['ipcc']; + } + public function IRCKey(): ?string { return $this->info()['IRCKey']; } @@ -1583,6 +1588,15 @@ class User extends BaseObject { return (int)$this->info()['inviter_user_id']; } + public function inviteSource(): ?string { + return $this->getSingleValue('user_invitesource', " + SELECT ivs.name + FROM user_has_invite_source uhivs + LEFT JOIN invite_source ivs using (invite_source_id) + WHERE uhivs.user_id = ? + "); + } + /** * This can be used to add a signature to a URL to prevent tampering with the parameter list. */ diff --git a/app/User/Invite.php b/app/User/Invite.php index db160a22e..df5731765 100644 --- a/app/User/Invite.php +++ b/app/User/Invite.php @@ -27,35 +27,6 @@ class Invite extends \Gazelle\BaseUser { return $affected > 0; } - /** - * Revoke an active invitation (restore previous invite total) - */ - public function revoke(string $key): bool { - self::$db->begin_transaction(); - self::$db->prepared_query(" - DELETE FROM invites WHERE InviteKey = ? - ", $key - ); - if (self::$db->affected_rows() == 0) { - self::$db->rollback(); - return false; - } - if ($this->user()->permitted('site_send_unlimited_invites')) { - self::$db->commit(); - return true; - } - - self::$db->prepared_query(" - UPDATE users_main SET - Invites = Invites + 1 - WHERE ID = ? - ", $this->id() - ); - self::$db->commit(); - $this->user()->flush(); - return true; - } - public function pendingTotal(): int { return (int)self::$db->scalar(" SELECT count(*) FROM invites WHERE InviterID = ? @@ -65,12 +36,15 @@ class Invite extends \Gazelle\BaseUser { public function pendingList(): array { self::$db->prepared_query(" - SELECT InviteKey AS invite_key, - Email AS email, - Expires AS expires - FROM invites - WHERE InviterID = ? - ORDER BY Expires + SELECT i.InviteKey AS invite_key, + i.Email AS email, + i.Expires AS expires, + ivs.name AS source_name + FROM invites i + LEFT JOIN invite_source_pending ivsp ON (ivsp.invite_key = i.InviteKey) + LEFT JOIN invite_source ivs USING (invite_source_id) + WHERE i.InviterID = ? + ORDER BY i.Expires ", $this->id() ); return self::$db->to_array('invite_key', MYSQLI_ASSOC, false); @@ -96,4 +70,39 @@ class Invite extends \Gazelle\BaseUser { ); return self::$db->collect(0, false); } + + /** + * Revoke an active invitation (restore previous invite total) + */ + public function revoke(string $key): int { + self::$db->begin_transaction(); + self::$db->prepared_query(" + DELETE FROM invites WHERE InviteKey = ? + ", $key + ); + $affected = self::$db->affected_rows(); + if ($affected == 0) { + self::$db->rollback(); + return 0; + } + if ($this->user()->permitted('site_send_unlimited_invites')) { + self::$db->commit(); + return $affected; + } + self::$db->prepared_query(" + DELETE FROM invite_source_pending WHERE invite_key = ? + ", $key + ); + $affected += self::$db->affected_rows(); + self::$db->prepared_query(" + UPDATE users_main SET + Invites = Invites + 1 + WHERE ID = ? + ", $this->id() + ); + $affected += self::$db->affected_rows(); + self::$db->commit(); + $this->user()->flush(); + return $affected; + } } diff --git a/sections/tools/data/registration_log.php b/sections/tools/data/registration_log.php index 523ef8055..620a753f1 100644 --- a/sections/tools/data/registration_log.php +++ b/sections/tools/data/registration_log.php @@ -10,7 +10,7 @@ if (!$Viewer->permittedAny('users_view_ips', 'users_view_email')) { error(403); } -$registration = new Manager\Registration(new Manager\User()); +$registration = new Manager\Registration(); if (isset($_REQUEST['before_date'])) { if (!str_contains($_SERVER['REQUEST_URI'], '&before_date=')) { diff --git a/sections/user/invite_handle.php b/sections/user/invite_handle.php index a3a853198..0460de174 100644 --- a/sections/user/invite_handle.php +++ b/sections/user/invite_handle.php @@ -35,7 +35,7 @@ if ($Viewer->isInterviewer() || $Viewer->isStaff()) { } $inviteSourceMan = null; -if ($Viewer->isRecruiter()) { +if ($Viewer->isRecruiter() || $Viewer->isStaff()) { $inviteSourceMan = new Manager\InviteSource(); } diff --git a/templates/admin/registration.twig b/templates/admin/registration.twig index b7733ef84..fd2eb5aa1 100644 --- a/templates/admin/registration.twig +++ b/templates/admin/registration.twig @@ -1,5 +1,23 @@ {{ header('Registration log', {'js': 'resolve-ip'}) }}
+
+{% if not list %} +

No new User Registrations +{%- else -%} +

{{ paginator.total|number_format }} new User Registration{{ paginator.total|plural }} +{%- endif %} +{%- if after -%} + {%- if before %} between {{ after }} and {{ before }} + {%- else %} after {{ after -}} + {%- endif -%} +{%- elseif before %} before {{ before -}} +{%- else %} in the last 72 hours +{%- endif -%} +

+
+
@@ -12,22 +30,9 @@
-{% if not list %} -

No new user registrations -{%- else -%} -

{{ paginator.total|number_format }} New user registration{{ paginator.total|plural }} -{%- endif %} -{%- if after -%} - {%- if before %} between {{ after }} and {{ before }} - {%- else %} after {{ after -}} - {%- endif -%} -{%- elseif before %} before {{ before -}} -{%- else %} in the last 72 hours -{%- endif -%} -

- -{% if list %} - {{ paginator.linkbox|raw }} +{% for user in list %} +{% if loop.first %} +{{ paginator.linkbox|raw }} @@ -39,8 +44,9 @@ + - {% for user in list %} +{% endif %} + - {% endfor %} +{% if loop.last %}
UserHost Country RegisteredSource
{{ user.id|user_full }} @@ -49,24 +55,24 @@ {{- user.uploadedSize|octet_size -}} - {%- if user.inviter -%} +{% if user.inviter -%}
{{- user.inviter.uploadedSize|octet_size -}} - {%- endif -%} +{% endif -%}
{{- user.downloadedSize|octet_size -}} - {%- if user.inviter -%} +{% if user.inviter -%}
{{- user.inviter.downloadedSize|octet_size -}} - {%- endif -%} +{% endif -%}
{{- ratio(user.uploadedSize, user.downloadedSize) -}} - {%- if user.inviter -%} +{% if user.inviter -%}
{{- ratio(user.inviter.uploadedSize, user.inviter.downloadedSize) -}} - {%- endif -%} +{% endif -%}
{{- user.email -}} @@ -76,62 +82,66 @@ H S - {%- if user.inviter -%} +{% if user.inviter -%}
H S - {%- endif -%} +{% endif -%}
{{- user.ipaddr -}} - {%- if user.inviter.id -%} +{% if user.inviter.id -%}
{{- user.inviter.ipaddr -}} - {%- endif -%} +{% endif -%}
- {%- if user.inviter and user.ipaddr == user.inviter.ipaddr -%} +{% if user.inviter and user.ipaddr == user.inviter.ipaddr -%} - {%- endif -%} +{% endif -%}
{{- ipv4.duplicateTotal(user) -}} - {%- if user.inviter and user.ipaddr != user.inviter.ipaddr -%} +{% if user.inviter and user.ipaddr != user.inviter.ipaddr -%}
{{- ipv4.duplicateTotal(user.inviter) -}} - {% endif %} +{% endif %}
Resolving... - {% if user.inviter.id and user.inviter.ipaddr != user.ipaddr %} +{% if user.inviter.id and user.inviter.ipaddr != user.ipaddr %}
Resolving... - {% endif %} +{% endif %}
H S WI - {% if user.inviter.id and user.inviter.ipaddr != user.ipaddr %} +{% if user.inviter.id and user.inviter.ipaddr != user.ipaddr %}
H S WI - {% endif %} +{% endif %}
- TODO + {{ user.ipCountryIso }} {{- user.created|time_diff -}}
{{- user.inviter.created|time_diff -}}
+ {{ user.inviteSource }} +
{{ paginator.linkbox|raw }} -{% endif %} +{% endif %} +{% endfor %} {{ footer() }} diff --git a/templates/invite/pool.twig b/templates/invite/pool.twig index 8ca249930..6b52754ae 100644 --- a/templates/invite/pool.twig +++ b/templates/invite/pool.twig @@ -2,7 +2,9 @@

Invite Pool

- +

{{ pending|number_format }} unused invites have been sent.

{% if removed is not empty %} @@ -34,6 +36,7 @@ Inviter Email address + Source IP address Invite link Expires @@ -46,8 +49,9 @@ {{ invite.user_id|user_full }} {{ invite.email }} + {{ invite.source_name }} {{ ipaddr(invite.ipaddr) }} - {{ invite.key }} + {{ constant('SITE_URL') }}/register.php?invite={{ invite.key }} {{ invite.expires|time_diff }} {% if viewer.permitted('users_edit_invites') %} diff --git a/templates/user/invited.twig b/templates/user/invited.twig index bc1f510cb..d32ea1b59 100644 --- a/templates/user/invited.twig +++ b/templates/user/invited.twig @@ -122,20 +122,26 @@ {% endif %} {% for p in user.invite.pendingList %} - {% if loop.first %} +{% if loop.first %}

Pending invites

+{% if is_site_inviter %} + +{% endif %} - {% endif %} +{% endif %} +{% if is_site_inviter %} + +{% endif %} diff --git a/tests/phpunit/InviteTest.php b/tests/phpunit/InviteTest.php index c9029e71b..3fb636120 100644 --- a/tests/phpunit/InviteTest.php +++ b/tests/phpunit/InviteTest.php @@ -330,7 +330,7 @@ class InviteTest extends TestCase { 'invite-source-create-pending' ); - // create auser from the invite + // create user from the invite $this->invitee = (new UserCreator()) ->setUsername('create.' . randomString(6)) ->setEmail(randomString(6) . '@example.com') @@ -342,6 +342,7 @@ class InviteTest extends TestCase { $inviteSourceMan->findSourceNameByUser($this->invitee), 'invitee-invite-source' ); + $this->assertEquals($sourceName, $this->invitee->inviteSource(), 'invitee-source-name'); $this->assertEquals($profile, $this->invitee->externalProfile()->profile(), 'invite-source-profile'); $this->assertStringContainsString( 'phpunit notes', @@ -421,16 +422,99 @@ class InviteTest extends TestCase { $this->assertEquals(1, $inviteSourceMan->remove($sourceId), 'invite-source-create-remove'); } + public function testRemoveInvite(): void { + $this->user->setField('Invites', 1)->modify(); + $manager = new Manager\Invite(); + $invite = $manager->create( + $this->user, + 'invite-unittest@unitte.st', + 'unittest invite remove notes', + 'unittest invite remove reason', + '' + ); + $this->assertTrue($manager->removeInviteKey($invite->key()), 'invite-remove-key'); + $this->assertNull($manager->findUserByKey($invite->key()), 'invite-removed-key'); + } + + public function testExpireInvite(): void { + $this->user->setField('Invites', 1)->modify(); + $this->assertEquals(1, $this->user->unusedInviteTotal(), 'invite-user-has-invite'); + $manager = new Manager\Invite(); + $invite = $manager->create( + $this->user, + 'invite-unittest@unitte.st', + 'unittest invite remove notes', + 'unittest invite remove reason', + '' + ); + $this->assertEquals(0, $this->user->unusedInviteTotal(), 'invite-user-all-used'); + // invites are not BaseObjects + DB::DB()->prepared_query(" + UPDATE invites SET + Expires = now() - INTERVAL 1 SECOND + WHERE InviteKey = ? + ", $invite->key() + ); + $this->assertEquals(1, DB::DB()->affected_rows(), 'invite-modify-expiry'); + $this->assertEquals(1, $manager->expire(), 'invite-manager-expire'); + $this->assertEquals(1, $this->user->flush()->unusedInviteTotal(), 'invite-user-invite-restored'); + } + public function testRevokeInvite(): void { $this->user->setField('Invites', 1)->modify(); $manager = new Manager\Invite(); + $initial = $manager->totalPending(); $email = randomString(10) . "@invitee.example.com"; - $this->assertFalse($manager->emailExists($this->user, $email), 'invitee-email-not-pending'); - $invite = $manager->create($this->user, $email, 'unittest notes', 'unittest reason', ''); - $this->assertFalse($this->user->invite()->revoke('nosuchthing'), 'invite-revoke-inexistant'); - $this->assertTrue($this->user->invite()->revoke($invite->key()), 'invite-revoke-existing'); - $this->assertEquals(1, $this->user->unusedInviteTotal(), 'invite-unused-1'); + $this->assertFalse( + $manager->emailExists($this->user, $email), + 'invitee-email-not-pending' + ); + $invite = $manager->create( + $this->user, + $email, + 'unittest notes', + 'unittest reason', + '' + ); + $this->assertEquals( + $initial + 1, + $manager->totalPending(), + 'invite-total-pending-invites', + ); + $this->assertEquals( + $this->user->id, + $manager->findUserByKey($invite->key(), new Manager\User())->id, + 'invite-manager-find-by-key', + ); + $pending = $manager->pendingInvites(1, 0); + $this->assertGreaterThan(0, count($pending), 'invite-list-pending'); + $this->assertEquals( + ['user_id', 'ipaddr', 'key', 'expires', 'email', 'source_name'], + array_keys($pending[0]), + 'invite-pending-array-keys', + ); + $this->assertEquals($this->user->id, $pending[0]['user_id'], 'invite-pending-id'); + $this->assertEquals($invite->key(), $pending[0]['key'], 'invite-pending-id'); + $this->assertTrue( + $manager->emailExists($this->user, $email), + 'invite-email-exists', + ); + $this->assertEquals( + 0, + $this->user->invite()->revoke('nosuchthing'), + 'invite-revoke-inexistant', + ); + $this->assertEquals( + 2, + $this->user->invite()->revoke($invite->key()), + 'invite-revoke-existing', + ); + $this->assertEquals( + 1, + $this->user->unusedInviteTotal(), + 'invite-unused-1', + ); } public function testEtm(): void { diff --git a/tests/phpunit/UserTest.php b/tests/phpunit/UserTest.php index 565c7d6de..7095f6054 100644 --- a/tests/phpunit/UserTest.php +++ b/tests/phpunit/UserTest.php @@ -166,6 +166,7 @@ class UserTest extends TestCase { $this->assertEquals(0.0, $this->user->requiredRatio(), 'utest-required-ratio'); $this->assertEquals('', $this->user->forbiddenForumsList(), 'utest-forbidden-forum-list'); $this->assertEquals('', $this->user->referral(), 'utest-referral'); + $this->assertEquals('??', $this->user->ipCountryIso(), 'utest-country-iso'); $this->assertEquals([], $this->user->tagSnatchCounts(), 'utest-tag-snatch-counts'); $this->assertEquals([], $this->user->tokenList(new Manager\Torrent(), 0, 0), 'utest-token-list'); $this->assertEquals([], $this->user->navigationList(), 'utest-navigation-list');
Email address Expires inSourceInvite link Revoke invite
{{ p.email }} {{ p.expires|time_diff }}{{ p.source_name }}{{ constant('SITE_URL') }}/register.php?invite={{ p.invite_key }} Revoke invite