misuzu/src/Users/UserWarning.php

316 lines
12 KiB
PHP

<?php
namespace Misuzu\Users;
use InvalidArgumentException;
use Misuzu\DB;
use Misuzu\Pagination;
class UserWarningException extends UsersException {}
class UserWarningNotFoundException extends UserWarningException {}
class UserWarningCreationFailedException extends UserWarningException {}
class UserWarning {
// Informational notes on profile, only show up for moderators
public const TYPE_NOTE = 0;
// Warning, only shows up to moderators and the user themselves
public const TYPE_WARN = 1;
// Silences, prevent a user from speaking and is visible to any logged in user
public const TYPE_MUTE = 2;
// Banning, prevents a user from interacting in general
// User will still be able to log in and change certain details but can no longer partake in community things
public const TYPE_BAHN = 3;
private const TYPES = [self::TYPE_NOTE, self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_BAHN];
private const VISIBLE_TO_STAFF = self::TYPES;
private const VISIBLE_TO_USER = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_BAHN];
private const VISIBLE_TO_PUBLIC = [self::TYPE_MUTE, self::TYPE_BAHN];
private const HAS_DURATION = [self::TYPE_MUTE, self::TYPE_BAHN];
private const PROFILE_BACKLOG = 90;
// Database fields
private $warning_id = -1;
private $user_id = -1;
private $user_ip = '::1';
private $issuer_id = -1;
private $issuer_ip = '::1';
private $warning_created = null;
private $warning_duration = null;
private $warning_type = 0;
private $warning_note = '';
private $warning_note_private = '';
private $user = null;
private $issuer = null;
public const TABLE = 'user_warnings';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`warning_id`, %1$s.`user_id`, %1$s.`issuer_id`, %1$s.`warning_type`, %1$s.`warning_note`, %1$s.`warning_note_private`'
. ', UNIX_TIMESTAMP(%1$s.`warning_created`) AS `warning_created`'
. ', UNIX_TIMESTAMP(%1$s.`warning_duration`) AS `warning_duration`'
. ', INET6_NTOA(%1$s.`user_ip`) AS `user_ip`'
. ', INET6_NTOA(%1$s.`issuer_ip`) AS `issuer_ip`';
public function getId(): int {
return $this->warning_id;
}
public function getUserId(): int {
return $this->user_id;
}
public function getUser(): User {
if($this->user === null)
$this->user = User::byId($this->getUserId());
return $this->user;
}
public function getUserRemoteAddress(): string {
return $this->user_ip;
}
public function getIssuerId(): int {
return $this->issuer_id;
}
public function getIssuer(): User {
if($this->issuer === null)
$this->issuer = User::byId($this->getIssuerId());
return $this->issuer;
}
public function getIssuerRemoteAddress(): string {
return $this->issuer_ip;
}
public function getCreatedTime(): int {
return $this->warning_created === null ? -1 : $this->warning_created;
}
public function getExpirationTime(): int {
return $this->warning_duration === null ? -1 : $this->warning_duration;
}
public function hasExpired(): bool {
return $this->hasDuration() && ($this->getExpirationTime() > 0 && $this->getExpirationTime() < time());
}
public function hasDuration(): bool {
return in_array($this->getType(), self::HAS_DURATION);
}
public function getDuration(): int {
return max(-1, $this->getExpirationTime() - $this->getCreatedTime());
}
private const DURATION_DIVS = [
31536000 => 'year',
2592000 => 'month',
604800 => 'week',
86400 => 'day',
3600 => 'hour',
60 => 'minute',
1 => 'second',
];
public function getDurationString(): string {
$duration = $this->getDuration();
if($duration < 1)
return 'permanent';
foreach(self::DURATION_DIVS as $span => $name) {
$display = floor($duration / $span);
if($display > 0)
return number_format($display) . ' ' . $name . ($display == 1 ? '' : 's');
}
return 'an amount of time';
}
public function isPermanent(): bool {
return $this->hasDuration() && $this->getDuration() < 0;
}
public function getType(): int { return $this->warning_type; }
public function isNote(): bool { return $this->getType() === self::TYPE_NOTE; }
public function isWarning(): bool { return $this->getType() === self::TYPE_WARN; }
public function isSilence(): bool { return $this->getType() === self::TYPE_MUTE; }
public function isBan(): bool { return $this->getType() === self::TYPE_BAHN; }
public function isVisibleToUser(): bool {
return in_array($this->getType(), self::VISIBLE_TO_USER);
}
public function isVisibleToPublic(): bool {
return in_array($this->getType(), self::VISIBLE_TO_PUBLIC);
}
public function getPublicNote(): string {
return $this->warning_note;
}
public function getPrivateNote(): string {
return $this->warning_note_private ?? '';
}
public function hasPrivateNote(): bool {
return !empty($this->warning_note_private);
}
public function delete(): void {
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `warning_id` = :warning')
->bind('warning', $this->warning_id)
->execute();
}
public static function create(
User $user,
User $issuer,
int $type,
int $duration,
string $publicNote,
?string $privateNote = null,
?string $targetAddr = null,
?string $issuerAddr = null
): self {
if(!in_array($type, self::TYPES))
throw new InvalidArgumentException('Type was invalid.');
if(!in_array($type, self::HAS_DURATION))
$duration = 0;
else {
if($duration === 0)
throw new InvalidArgumentException('Duration must be non-zero.');
if($duration < 0)
$duration = -1;
}
$targetAddr ??= $user->getLastRemoteAddress();
$issuerAddr ??= $issuer->getLastRemoteAddress();
$warningId = DB::prepare(
'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `user_ip`, `issuer_id`, `issuer_ip`, `warning_created`, `warning_duration`, `warning_type`, `warning_note`, `warning_note_private`)'
. ' VALUES (:user, INET6_ATON(:user_addr), :issuer, INET6_ATON(:issuer_addr), NOW(), IF(:set_duration, NOW() + INTERVAL :duration SECOND, NULL), :type, :public_note, :private_note)'
) ->bind('user', $user->getId())
->bind('user_addr', $targetAddr)
->bind('issuer', $issuer->getId())
->bind('issuer_addr', $issuerAddr)
->bind('set_duration', $duration > 0 ? 1 : 0)
->bind('duration', $duration)
->bind('type', $type)
->bind('public_note', $publicNote)
->bind('private_note', $privateNote)
->executeGetId();
if($warningId < 1)
throw new UserWarningCreationFailedException;
return self::byId($warningId);
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, 'COUNT(*)');
}
public static function countByRemoteAddress(string $address, bool $withDuration = true): int {
return (int)DB::prepare(
self::countQueryBase()
. ' WHERE `user_ip` = INET6_ATON(:address)'
. ' AND `warning_duration` >= NOW()'
. ($withDuration ? ' AND `warning_type` IN (' . implode(',', self::HAS_DURATION) . ')' : '')
)->bind('address', $address)->fetchColumn();
}
public static function countAll(?User $user = null): int {
$getCount = DB::prepare(self::countQueryBase() . ($user === null ? '' : ' WHERE `user_id` = :user'));
if($user !== null)
$getCount->bind('user', $user->getId());
return (int)$getCount->fetchColumn();
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $warningId): self {
$object = DB::prepare(
self::byQueryBase() . ' WHERE `warning_id` = :warning'
) ->bind('warning', $warningId)
->fetchObject(self::class);
if(!$object)
throw new UserWarningNotFoundException;
return $object;
}
public static function byUserActive(User $user): ?self {
return self::byUserIdActive($user->getId());
}
public static function byUserIdActive(int $userId): ?self {
if($userId < 1)
return null;
return DB::prepare(
self::byQueryBase()
. ' WHERE `user_id` = :user'
. ' AND `warning_type` IN (' . implode(',', self::HAS_DURATION) . ')'
. ' AND (`warning_duration` IS NULL OR `warning_duration` >= NOW())'
. ' ORDER BY `warning_type` DESC, `warning_duration` DESC'
) ->bind('user', $userId)
->fetchObject(self::class);
}
public static function byRemoteAddressActive(string $ipAddress): ?self {
return DB::prepare(
self::byQueryBase()
. ' WHERE `user_ip` = INET6_ATON(:address)'
. ' AND `warning_type` IN (' . implode(',', self::HAS_DURATION) . ')'
. ' AND (`warning_duration` IS NULL OR `warning_duration` >= NOW())'
. ' ORDER BY `warning_type` DESC, `warning_duration` DESC'
) ->bind('address', $ipAddress)
->fetchObject(self::class);
}
public static function byProfile(User $user, ?User $viewer = null): array {
if($viewer === null)
return [];
$types = self::VISIBLE_TO_PUBLIC;
if(perms_check_user(MSZ_PERMS_USER, $viewer->getId(), MSZ_PERM_USER_MANAGE_WARNINGS))
$types = self::VISIBLE_TO_STAFF;
elseif($user->getId() === $viewer->getId())
$types = self::VISIBLE_TO_USER;
$getObjects = DB::prepare(
self::byQueryBase()
. ' WHERE `user_id` = :user'
. ' AND `warning_type` IN (' . implode(',', $types) . ')'
. ' AND (`warning_type` = 0 OR `warning_created` >= NOW() - INTERVAL ' . self::PROFILE_BACKLOG . ' DAY OR (`warning_duration` IS NOT NULL AND `warning_duration` >= NOW()))'
. ' ORDER BY `warning_created` DESC'
);
$getObjects->bind('user', $user->getId());
return $getObjects->fetchObjects(self::class);
}
public static function byActive(): array {
return DB::prepare(
self::byQueryBase()
. ' WHERE `warning_type` IN (' . implode(',', self::HAS_DURATION) . ')'
. ' AND (`warning_duration` IS NULL OR `warning_duration` >= NOW())'
. ' ORDER BY `warning_type` DESC, `warning_duration` DESC'
)->fetchObjects(self::class);
}
public static function all(?User $user = null, ?Pagination $pagination = null): array {
$query = self::byQueryBase()
. ($user === null ? '' : ' WHERE `user_id` = :user')
. ' ORDER BY `warning_created` DESC';
if($pagination !== null)
$query .= ' LIMIT :range OFFSET :offset';
$getObjects = DB::prepare($query);
if($user !== null)
$getObjects->bind('user', $user->getId());
if($pagination !== null)
$getObjects->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getObjects->fetchObjects(self::class);
}
}