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); } }