Raised checking level from 5 to 6.

This commit is contained in:
flash 2024-12-02 21:33:15 +00:00
parent 5cf2529209
commit 5e0a2530a8
70 changed files with 596 additions and 183 deletions

View file

@ -1,5 +1,6 @@
parameters: parameters:
level: 5 level: 6
treatPhpDocTypesAsCertain: false
paths: paths:
- database - database
- src - src

View file

@ -57,6 +57,9 @@ if(empty($config['tokens']['token']))
$isGitea = isset($_SERVER['HTTP_X_GITEA_DELIVERY']) && isset($_SERVER['HTTP_X_GITEA_EVENT']); $isGitea = isset($_SERVER['HTTP_X_GITEA_DELIVERY']) && isset($_SERVER['HTTP_X_GITEA_EVENT']);
$rawData = file_get_contents('php://input'); $rawData = file_get_contents('php://input');
if(!is_string($rawData))
die('no input data');
$sigParts = $isGitea $sigParts = $isGitea
? ['sha256', $_SERVER['HTTP_X_GITEA_SIGNATURE']] ? ['sha256', $_SERVER['HTTP_X_GITEA_SIGNATURE']]
: explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '', 2); : explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '', 2);
@ -94,6 +97,11 @@ if($data->repository->full_name !== $repoName)
if($_SERVER['HTTP_X_GITHUB_EVENT'] !== 'push') if($_SERVER['HTTP_X_GITHUB_EVENT'] !== 'push')
die('only push event is supported'); die('only push event is supported');
if(!property_exists($data, 'commits'))
die('commits property missing');
if(!property_exists($data, 'ref'))
die('ref property missing');
$commitCount = count($data->commits); $commitCount = count($data->commits);
if($commitCount < 1) if($commitCount < 1)
die('no commits received'); die('no commits received');

View file

@ -44,7 +44,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
} }
// make errors not echos lol // make errors not echos lol
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { // @phpstan-ignore-line: this while is just weird, i don't blame it while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$action = trim((string)filter_input(INPUT_POST, 'cl_action')); $action = trim((string)filter_input(INPUT_POST, 'cl_action'));
$summary = trim((string)filter_input(INPUT_POST, 'cl_summary')); $summary = trim((string)filter_input(INPUT_POST, 'cl_summary'));
$body = trim((string)filter_input(INPUT_POST, 'cl_body')); $body = trim((string)filter_input(INPUT_POST, 'cl_body'));

View file

@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
return; return;
} }
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { // @phpstan-ignore-line: this while is just weird, i don't blame it while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$name = trim((string)filter_input(INPUT_POST, 'ct_name')); $name = trim((string)filter_input(INPUT_POST, 'ct_name'));
$description = trim((string)filter_input(INPUT_POST, 'ct_desc')); $description = trim((string)filter_input(INPUT_POST, 'ct_desc'));
$archive = !empty($_POST['ct_archive']); $archive = !empty($_POST['ct_archive']);

View file

@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
return; return;
} }
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { // @phpstan-ignore-line: this while is just weird, i don't blame it while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$name = trim((string)filter_input(INPUT_POST, 'nc_name')); $name = trim((string)filter_input(INPUT_POST, 'nc_name'));
$description = trim((string)filter_input(INPUT_POST, 'nc_desc')); $description = trim((string)filter_input(INPUT_POST, 'nc_desc'));
$hidden = !empty($_POST['nc_hidden']); $hidden = !empty($_POST['nc_hidden']);

View file

@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
return; return;
} }
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { // @phpstan-ignore-line: this while is just weird, i don't blame it while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$title = trim((string)filter_input(INPUT_POST, 'np_title')); $title = trim((string)filter_input(INPUT_POST, 'np_title'));
$category = (string)filter_input(INPUT_POST, 'np_category', FILTER_SANITIZE_NUMBER_INT); $category = (string)filter_input(INPUT_POST, 'np_category', FILTER_SANITIZE_NUMBER_INT);
$featured = !empty($_POST['np_featured']); $featured = !empty($_POST['np_featured']);

View file

@ -35,7 +35,7 @@ try {
$modInfo = $msz->authInfo->userInfo; $modInfo = $msz->authInfo->userInfo;
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { // @phpstan-ignore-line: this while is just weird, i don't blame it while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT); $expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT);
$expiresCustom = (string)filter_input(INPUT_POST, 'ub_expires_custom'); $expiresCustom = (string)filter_input(INPUT_POST, 'ub_expires_custom');
$publicReason = trim((string)filter_input(INPUT_POST, 'ub_reason_pub')); $publicReason = trim((string)filter_input(INPUT_POST, 'ub_reason_pub'));

View file

@ -33,7 +33,7 @@ try {
$modInfo = $msz->authInfo->userInfo; $modInfo = $msz->authInfo->userInfo;
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { // @phpstan-ignore-line: this while is just weird, i don't blame it while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$body = trim((string)filter_input(INPUT_POST, 'uw_body')); $body = trim((string)filter_input(INPUT_POST, 'uw_body'));
Template::set('warn_value_body', $body); Template::set('warn_value_body', $body);

View file

@ -13,7 +13,14 @@ if(!$msz->authInfo->isLoggedIn)
$dbConn = $msz->dbConn; $dbConn = $msz->dbConn;
function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, array $fieldInfos, string $userIdField = 'user_id'): string { /** @param string[] $fieldInfos */
function db_to_zip(
ZipArchive $archive,
UserInfo $userInfo,
string $baseName,
array $fieldInfos,
string $userIdField = 'user_id'
): string {
global $dbConn; global $dbConn;
$userId = $userInfo->id; $userId = $userInfo->id;

View file

@ -52,6 +52,7 @@ class AuditLog {
return $count; return $count;
} }
/** @return \Iterator<int, AuditLogInfo> */
public function getLogs( public function getLogs(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?string $remoteAddr = null, ?string $remoteAddr = null,
@ -95,6 +96,7 @@ class AuditLog {
return $stmt->getResult()->getIterator(AuditLogInfo::fromResult(...)); return $stmt->getResult()->getIterator(AuditLogInfo::fromResult(...));
} }
/** @param mixed[] $params */
public function createLog( public function createLog(
UserInfo|string|null $userInfo, UserInfo|string|null $userInfo,
string $action, string $action,

View file

@ -6,6 +6,7 @@ use Carbon\CarbonImmutable;
use Index\Db\DbResult; use Index\Db\DbResult;
class AuditLogInfo { class AuditLogInfo {
/** @param scalar[] $params */
public function __construct( public function __construct(
public private(set) ?string $userId, public private(set) ?string $userId,
public private(set) string $action, public private(set) string $action,

View file

@ -4,7 +4,7 @@ namespace Misuzu\Auth;
use Misuzu\Auth\SessionInfo; use Misuzu\Auth\SessionInfo;
use Misuzu\Forum\ForumCategoryInfo; use Misuzu\Forum\ForumCategoryInfo;
use Misuzu\Perms\IPermissionResult; use Misuzu\Perms\IPermissionResult;
use Misuzu\Perms\Permissions; use Misuzu\Perms\{Permissions,PermissionResult};
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;
class AuthInfo { class AuthInfo {
@ -12,6 +12,8 @@ class AuthInfo {
public private(set) ?UserInfo $userInfo; public private(set) ?UserInfo $userInfo;
public private(set) ?SessionInfo $sessionInfo; public private(set) ?SessionInfo $sessionInfo;
public private(set) ?UserInfo $realUserInfo; public private(set) ?UserInfo $realUserInfo;
/** @var array<string, PermissionResult> */
public private(set) array $perms; public private(set) array $perms;
public function __construct( public function __construct(

View file

@ -23,6 +23,7 @@ final class AuthRpcHandler implements RpcHandler {
return in_array($targetId, $whitelist, true); return in_array($targetId, $whitelist, true);
} }
/** @return array{method: string, error: string}|array{method: string, type: string, user: string, expires: int} */
#[RpcAction('misuzu:auth:attemptMisuzuAuth')] #[RpcAction('misuzu:auth:attemptMisuzuAuth')]
public function procAttemptMisuzuAuth(string $remoteAddr, string $token): array { public function procAttemptMisuzuAuth(string $remoteAddr, string $token): array {
$tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token); $tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token);

View file

@ -5,15 +5,12 @@ use Misuzu\Auth\SessionInfo;
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;
class AuthTokenBuilder { class AuthTokenBuilder {
private array $props; /** @var array<string, scalar> */
public private(set) array $props;
private bool $edited = false; private bool $edited = false;
public function __construct(?AuthTokenInfo $baseTokenInfo = null) { public function __construct(?AuthTokenInfo $baseTokenInfo = null) {
$this->props = $baseTokenInfo === null ? [] : $baseTokenInfo->getProperties(); $this->props = $baseTokenInfo === null ? [] : $baseTokenInfo->props;
}
public function getProperties(): array {
return $this->props;
} }
public function setEdited(): void { public function setEdited(): void {

View file

@ -9,15 +9,12 @@ class AuthTokenInfo {
public const SESSION_TOKEN_HEX = 't'; // Session token that should be hex encoded public const SESSION_TOKEN_HEX = 't'; // Session token that should be hex encoded
public const IMPERSONATED_USER_ID = 'i'; // Impersonated user ID public const IMPERSONATED_USER_ID = 'i'; // Impersonated user ID
/** @param array<string, scalar> $props */
public function __construct( public function __construct(
public private(set) int $timestamp = 0, public private(set) int $timestamp = 0,
private array $props = [] public private(set) array $props = []
) {} ) {}
public function getProperties(): array {
return $this->props;
}
public function toBuilder(): AuthTokenBuilder { public function toBuilder(): AuthTokenBuilder {
return new AuthTokenBuilder($this); return new AuthTokenBuilder($this);
} }

View file

@ -10,7 +10,7 @@ class AuthTokenPacker {
public function __construct(private string $secretKey) {} public function __construct(private string $secretKey) {}
public function pack(AuthTokenBuilder|AuthTokenInfo $tokenInfo): string { public function pack(AuthTokenBuilder|AuthTokenInfo $tokenInfo): string {
$props = $tokenInfo->getProperties(); $props = $tokenInfo->props;
$timestamp = $tokenInfo instanceof AuthTokenInfo ? $tokenInfo->timestamp : time(); $timestamp = $tokenInfo instanceof AuthTokenInfo ? $tokenInfo->timestamp : time();
$data = ''; $data = '';

View file

@ -70,6 +70,7 @@ class LoginAttempts {
); );
} }
/** @return \Iterator<int, LoginAttemptInfo> */
public function getAttempts( public function getAttempts(
?bool $success = null, ?bool $success = null,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,

View file

@ -10,11 +10,11 @@ use Misuzu\Pagination;
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;
class Sessions { class Sessions {
private DbConnection $dbConn;
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) { public function __construct(
$this->dbConn = $dbConn; private DbConnection $dbConn
) {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
@ -52,6 +52,7 @@ class Sessions {
return $count; return $count;
} }
/** @return \Iterator<int, SessionInfo> */
public function getSessions( public function getSessions(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
@ -143,6 +144,11 @@ class Sessions {
return $this->getSession(sessionId: (string)$this->dbConn->getLastInsertId()); return $this->getSession(sessionId: (string)$this->dbConn->getLastInsertId());
} }
/**
* @param SessionInfo|string|array<SessionInfo|string>|null $sessionInfos
* @param string|array<string>|null $sessionTokens
* @param UserInfo|string|array<UserInfo|string>|null $userInfos
*/
public function deleteSessions( public function deleteSessions(
SessionInfo|string|array|null $sessionInfos = null, SessionInfo|string|array|null $sessionInfos = null,
string|array|null $sessionTokens = null, string|array|null $sessionTokens = null,

View file

@ -12,13 +12,14 @@ class Changelog {
// not a strict list but useful to have // not a strict list but useful to have
public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert']; public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert'];
private DbConnection $dbConn;
private DbStatementCache $cache; private DbStatementCache $cache;
/** @var array<string, ChangeTagInfo> */
private array $tags = []; private array $tags = [];
public function __construct(DbConnection $dbConn) { public function __construct(
$this->dbConn = $dbConn; private DbConnection $dbConn
) {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
@ -61,6 +62,7 @@ class Changelog {
}; };
} }
/** @param ?array<\Stringable|string> $tags */
public function countChanges( public function countChanges(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTimeInterface|int|null $dateTime = null, DateTimeInterface|int|null $dateTime = null,
@ -113,6 +115,10 @@ class Changelog {
return $count; return $count;
} }
/**
* @param ?array<\Stringable|string> $tags
* @return \Iterator<int, ChangeInfo>
*/
public function getChanges( public function getChanges(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTimeInterface|int|null $dateTime = null, DateTimeInterface|int|null $dateTime = null,
@ -267,6 +273,7 @@ class Changelog {
$stmt->execute(); $stmt->execute();
} }
/** @return ChangeTagInfo[] */
public function getTags( public function getTags(
ChangeInfo|string|null $changeInfo = null ChangeInfo|string|null $changeInfo = null
): array { ): array {

View file

@ -3,6 +3,7 @@ namespace Misuzu\Changelog;
use ErrorException; use ErrorException;
use RuntimeException; use RuntimeException;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Syndication\FeedBuilder; use Index\Syndication\FeedBuilder;
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
@ -30,7 +31,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
#[HttpGet('/changelog')] #[HttpGet('/changelog')]
#[UrlFormat('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])] #[UrlFormat('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
public function getIndex($response, $request) { public function getIndex(HttpResponseBuilder $response, HttpRequest $request): int|string {
$filterDate = (string)$request->getParam('date'); $filterDate = (string)$request->getParam('date');
$filterUser = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT); $filterUser = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
$filterTags = (string)$request->getParam('tags'); $filterTags = (string)$request->getParam('tags');
@ -100,7 +101,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
#[HttpGet('/changelog/change/([0-9]+)')] #[HttpGet('/changelog/change/([0-9]+)')]
#[UrlFormat('changelog-change', '/changelog/change/<change>')] #[UrlFormat('changelog-change', '/changelog/change/<change>')]
#[UrlFormat('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')] #[UrlFormat('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
public function getChange($response, $request, string $changeId) { public function getChange(HttpResponseBuilder $response, HttpRequest $request, string $changeId): int|string {
try { try {
$changeInfo = $this->changelog->getChange($changeId); $changeInfo = $this->changelog->getChange($changeId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
@ -121,7 +122,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
#[HttpGet('/changelog.(xml|rss|atom)')] #[HttpGet('/changelog.(xml|rss|atom)')]
#[UrlFormat('changelog-feed', '/changelog.xml')] #[UrlFormat('changelog-feed', '/changelog.xml')]
public function getFeed($response) { public function getFeed(HttpResponseBuilder $response): string {
$response->setContentType('application/rss+xml; charset=utf-8'); $response->setContentType('application/rss+xml; charset=utf-8');
$siteName = $this->siteInfo->name; $siteName = $this->siteInfo->name;

View file

@ -13,6 +13,11 @@ class ClientInfo implements Stringable, JsonSerializable {
private const SERIALIZE_VERSION = 1; private const SERIALIZE_VERSION = 1;
/**
* @param bool|null|array{name: string} $botInfo
* @param ?array{name: string, version?: string} $clientInfo
* @param ?array{name: string, version?: string, platform?: string} $osInfo
*/
public function __construct( public function __construct(
private array|bool|null $botInfo, private array|bool|null $botInfo,
private ?array $clientInfo, private ?array $clientInfo,
@ -26,6 +31,7 @@ class ClientInfo implements Stringable, JsonSerializable {
return self::SERIALIZE_VERSION; return self::SERIALIZE_VERSION;
} }
/** @return bool|null|array{name: string} */
#[JsonProperty('bot')] #[JsonProperty('bot')]
public function getBotInfo(): bool|array|null { public function getBotInfo(): bool|array|null {
if($this->botInfo === true || is_array($this->botInfo)) if($this->botInfo === true || is_array($this->botInfo))
@ -34,11 +40,13 @@ class ClientInfo implements Stringable, JsonSerializable {
return null; return null;
} }
/** @return ?array{name: string, version?: string} */
#[JsonProperty('client')] #[JsonProperty('client')]
public function getClientInfo(): ?array { public function getClientInfo(): ?array {
return $this->clientInfo; return $this->clientInfo;
} }
/** @return ?array{name: string, version?: string, platform?: string} */
#[JsonProperty('os')] #[JsonProperty('os')]
public function getOsInfo(): ?array { public function getOsInfo(): ?array {
return $this->osInfo; return $this->osInfo;
@ -114,6 +122,7 @@ class ClientInfo implements Stringable, JsonSerializable {
); );
} }
/** @param mixed[]|string $serverVarsOrUserAgent */
public static function parse(array|string $serverVarsOrUserAgent): self { public static function parse(array|string $serverVarsOrUserAgent): self {
static $dd = null; static $dd = null;
$dd ??= new DeviceDetector(); $dd ??= new DeviceDetector();

View file

@ -41,6 +41,7 @@ class Comments {
return $count; return $count;
} }
/** @return \Iterator<int, CommentsCategoryInfo> */
public function getCategories( public function getCategories(
UserInfo|string|null $owner = null, UserInfo|string|null $owner = null,
?Pagination $pagination = null ?Pagination $pagination = null
@ -260,6 +261,7 @@ class Comments {
return $count; return $count;
} }
/** @return \Iterator<int, CommentsPostInfo> */
public function getPosts( public function getPosts(
CommentsCategoryInfo|string|null $categoryInfo = null, CommentsCategoryInfo|string|null $categoryInfo = null,
CommentsPostInfo|string|null $parentInfo = null, CommentsPostInfo|string|null $parentInfo = null,

View file

@ -20,6 +20,7 @@ class Counters {
'updated' => 'counter_updated', 'updated' => 'counter_updated',
]; ];
/** @return \Iterator<int, CounterInfo> */
public function getCounters( public function getCounters(
?string $orderBy = null, ?string $orderBy = null,
?Pagination $pagination = null ?Pagination $pagination = null
@ -48,6 +49,10 @@ class Counters {
return $stmt->getResult()->getIterator(CounterInfo::fromResult(...)); return $stmt->getResult()->getIterator(CounterInfo::fromResult(...));
} }
/**
* @param string|string[] $names
* @return array<string, int>|int
*/
public function get(array|string $names): array|int { public function get(array|string $names): array|int {
if(is_string($names)) { if(is_string($names)) {
$returnFirst = true; $returnFirst = true;
@ -72,6 +77,7 @@ class Counters {
return $returnFirst ? $values[array_key_first($values)] : $values; return $returnFirst ? $values[array_key_first($values)] : $values;
} }
/** @param string|array<string, int> $nameOrValues */
public function set(string|array $nameOrValues, ?int $value = null): void { public function set(string|array $nameOrValues, ?int $value = null): void {
if(empty($nameOrValues)) if(empty($nameOrValues))
throw new InvalidArgumentException('$nameOrValues may not be empty.'); throw new InvalidArgumentException('$nameOrValues may not be empty.');
@ -97,6 +103,7 @@ class Counters {
$stmt->execute(); $stmt->execute();
} }
/** @param string|string[] $names */
public function reset(string|array $names): void { public function reset(string|array $names): void {
if(empty($names)) if(empty($names))
throw new InvalidArgumentException('$names may not be empty.'); throw new InvalidArgumentException('$names may not be empty.');

View file

@ -33,11 +33,15 @@ class Emotes {
return EmoteInfo::fromResult($result); return EmoteInfo::fromResult($result);
} }
/** @return string[] */
public static function emoteOrderOptions(): array { public static function emoteOrderOptions(): array {
return array_keys(self::EMOTE_ORDER); return array_keys(self::EMOTE_ORDER);
} }
// TODO: pagination /**
* @todo pagination
* @return \Iterator<int, EmoteInfo>
*/
public function getEmotes( public function getEmotes(
?int $minRank = null, ?int $minRank = null,
?string $orderBy = null, ?string $orderBy = null,
@ -149,6 +153,7 @@ class Emotes {
$stmt->execute(); $stmt->execute();
} }
/** @return \Iterator<int, EmoteStringInfo> */
public function getEmoteStrings(EmoteInfo|string $infoOrId): iterable { public function getEmoteStrings(EmoteInfo|string $infoOrId): iterable {
if($infoOrId instanceof EmoteInfo) if($infoOrId instanceof EmoteInfo)
$infoOrId = $infoOrId->id; $infoOrId = $infoOrId->id;

View file

@ -11,6 +11,7 @@ final class EmotesRpcHandler implements RpcHandler {
private Emotes $emotes private Emotes $emotes
) {} ) {}
/** @return array<array{url: string, strings: string[], id?: string, order?: int, min_rank?: int}> */
#[RpcQuery('misuzu:emotes:all')] #[RpcQuery('misuzu:emotes:all')]
public function queryAll(bool $includeId = false, bool $includeOrder = false): array { public function queryAll(bool $includeId = false, bool $includeOrder = false): array {
return XArray::select( return XArray::select(

View file

@ -16,6 +16,10 @@ class ForumCategories {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
/**
* @param \Iterator<int, ForumCategoryInfo>|ForumCategoryInfo[] $catInfos
* @return object{info: ForumCategoryInfo, colour: Colour, children: mixed[], childIds: string[]}[]
*/
public static function convertCategoryListToTree( public static function convertCategoryListToTree(
iterable $catInfos, iterable $catInfos,
ForumCategoryInfo|string|null $parentInfo = null, ForumCategoryInfo|string|null $parentInfo = null,
@ -96,6 +100,7 @@ class ForumCategories {
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }
/** @return \Iterator<int, ForumCategoryInfo>|object{info: ForumCategoryInfo, colour: Colour, children: mixed[], childIds: string[]}[] */
public function getCategories( public function getCategories(
ForumCategoryInfo|string|null|false $parentInfo = false, ForumCategoryInfo|string|null|false $parentInfo = false,
string|int|null $type = null, string|int|null $type = null,
@ -254,6 +259,7 @@ class ForumCategories {
$stmt->execute(); $stmt->execute();
} }
/** @return \Iterator<int, ForumCategoryInfo> */
public function getCategoryAncestry( public function getCategoryAncestry(
ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
): iterable { ): iterable {
@ -275,6 +281,7 @@ class ForumCategories {
return $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...)); return $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
} }
/** @return \Iterator<int, ForumCategoryInfo>|object{info: ForumCategoryInfo, colour: Colour, children: mixed[], childIds: string[]}[] */
public function getCategoryChildren( public function getCategoryChildren(
ForumCategoryInfo|string $parentInfo, ForumCategoryInfo|string $parentInfo,
bool $includeSelf = false, bool $includeSelf = false,
@ -316,6 +323,7 @@ class ForumCategories {
return $cats; return $cats;
} }
/** @param ForumCategoryInfo|string|array<ForumCategoryInfo|string|int> $categoryInfos */
public function checkCategoryUnread( public function checkCategoryUnread(
ForumCategoryInfo|string|array $categoryInfos, ForumCategoryInfo|string|array $categoryInfos,
UserInfo|string|null $userInfo UserInfo|string|null $userInfo
@ -388,6 +396,10 @@ class ForumCategories {
return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none(); return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none();
} }
/**
* @param array<ForumCategoryInfo|string|int> $exceptCategoryInfos
* @param array<ForumTopicInfo|string|int> $exceptTopicInfos
*/
public function getMostActiveCategoryInfo( public function getMostActiveCategoryInfo(
UserInfo|string $userInfo, UserInfo|string $userInfo,
array $exceptCategoryInfos = [], array $exceptCategoryInfos = [],

View file

@ -11,7 +11,10 @@ class ForumContext {
public private(set) ForumTopicRedirects $topicRedirects; public private(set) ForumTopicRedirects $topicRedirects;
public private(set) ForumPosts $posts; public private(set) ForumPosts $posts;
/** @var array<string, int> */
private array $totalUserTopics = []; private array $totalUserTopics = [];
/** @var array<string, int> */
private array $totalUserPosts = []; private array $totalUserPosts = [];
public function __construct(DbConnection $dbConn) { public function __construct(DbConnection $dbConn) {

View file

@ -71,6 +71,11 @@ class ForumPosts {
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }
/**
* @param ForumCategoryInfo|string|null|array<ForumCategoryInfo|string|int> $categoryInfo
* @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery
* @return \Iterator<int, ForumPostInfo>|ForumPostInfo[]
*/
public function getPosts( public function getPosts(
ForumCategoryInfo|string|array|null $categoryInfo = null, ForumCategoryInfo|string|array|null $categoryInfo = null,
ForumTopicInfo|string|null $topicInfo = null, ForumTopicInfo|string|null $topicInfo = null,
@ -189,6 +194,7 @@ class ForumPosts {
return $stmt->getResult()->getIterator(ForumPostInfo::fromResult(...)); return $stmt->getResult()->getIterator(ForumPostInfo::fromResult(...));
} }
/** @param ForumCategoryInfo|string|null|array<ForumCategoryInfo|string|int> $categoryInfos */
public function getPost( public function getPost(
?string $postId = null, ?string $postId = null,
ForumTopicInfo|string|null $topicInfo = null, ForumTopicInfo|string|null $topicInfo = null,
@ -390,6 +396,11 @@ class ForumPosts {
return CarbonImmutable::createFromTimestampUTC($this->getUserLastPostCreatedTime($userInfo)); return CarbonImmutable::createFromTimestampUTC($this->getUserLastPostCreatedTime($userInfo));
} }
/**
* @param array<ForumCategoryInfo|string|int> $exceptCategoryInfos
* @param array<ForumTopicInfo|string|int> $exceptTopicInfos
* @return array<stdClass>
*/
public function generatePostRankings( public function generatePostRankings(
int $year = 0, int $year = 0,
int $month = 0, int $month = 0,

View file

@ -34,6 +34,7 @@ class ForumTopicRedirects {
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }
/** @return \Iterator<int, ForumTopicRedirectInfo> */
public function getTopicRedirects( public function getTopicRedirects(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null

View file

@ -17,6 +17,7 @@ class ForumTopics {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
/** @param ForumCategoryInfo|string|null|array<ForumCategoryInfo|string|int> $categoryInfo */
public function countTopics( public function countTopics(
ForumCategoryInfo|string|array|null $categoryInfo = null, ForumCategoryInfo|string|array|null $categoryInfo = null,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
@ -82,6 +83,11 @@ class ForumTopics {
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }
/**
* @param ForumCategoryInfo|string|null|array<ForumCategoryInfo|string|int> $categoryInfo
* @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery
* @return \Iterator<int, ForumTopicInfo>|ForumTopicInfo[]
*/
public function getTopics( public function getTopics(
ForumCategoryInfo|string|array|null $categoryInfo = null, ForumCategoryInfo|string|array|null $categoryInfo = null,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
@ -418,6 +424,11 @@ class ForumTopics {
return $result->getInteger(0) < $topicInfo->bumpedTime; return $result->getInteger(0) < $topicInfo->bumpedTime;
} }
/**
* @param array<ForumCategoryInfo|string|int> $exceptCategoryInfos
* @param array<ForumTopicInfo|string|int> $exceptTopicInfos
* @return object{success: false}|object{success: true, topicId: string, categoryId: string, postCount: int}
*/
public function getMostActiveTopicInfo( public function getMostActiveTopicInfo(
UserInfo|string $userInfo, UserInfo|string $userInfo,
array $exceptCategoryInfos = [], array $exceptCategoryInfos = [],

View file

@ -13,6 +13,7 @@ use Index\Urls\UrlRegistry;
final class HanyuuRpcHandler implements RpcHandler { final class HanyuuRpcHandler implements RpcHandler {
use RpcHandlerCommon; use RpcHandlerCommon;
/** @param callable(): string $getBaseUrl */
public function __construct( public function __construct(
private $getBaseUrl, private $getBaseUrl,
private Config $impersonateConfig, private Config $impersonateConfig,
@ -21,6 +22,10 @@ final class HanyuuRpcHandler implements RpcHandler {
private AuthContext $authCtx private AuthContext $authCtx
) {} ) {}
/**
* @param array<string, ?mixed> $attrs
* @return array{name: string, attrs: array<string, mixed>}
*/
private static function createPayload(string $name, array $attrs = []): array { private static function createPayload(string $name, array $attrs = []): array {
$payload = ['name' => $name, 'attrs' => []]; $payload = ['name' => $name, 'attrs' => []];
foreach($attrs as $name => $value) { foreach($attrs as $name => $value) {
@ -32,6 +37,7 @@ final class HanyuuRpcHandler implements RpcHandler {
return $payload; return $payload;
} }
/** @return array{name: string, attrs: array{code: string, text?: string}} */
private static function createErrorPayload(string $code, ?string $text = null): array { private static function createErrorPayload(string $code, ?string $text = null): array {
$attrs = ['code' => $code]; $attrs = ['code' => $code];
if($text !== null && $text !== '') if($text !== null && $text !== '')
@ -51,8 +57,9 @@ final class HanyuuRpcHandler implements RpcHandler {
); );
} }
/** @return array{name: string, attrs: array<string, mixed>} */
#[RpcAction('mszhau:authCheck')] #[RpcAction('mszhau:authCheck')]
public function procAuthCheck(string $method, string $remoteAddr, string $token, string $avatars = '') { public function procAuthCheck(string $method, string $remoteAddr, string $token, string $avatars = ''): array {
if($method !== 'Misuzu') if($method !== 'Misuzu')
return self::createErrorPayload('auth:check:method', 'Requested auth method is not supported.'); return self::createErrorPayload('auth:check:method', 'Requested auth method is not supported.');

View file

@ -4,7 +4,9 @@ namespace Misuzu\Home;
use RuntimeException; use RuntimeException;
use Index\XDateTime; use Index\XDateTime;
use Index\Config\Config; use Index\Config\Config;
use Index\Colour\Colour;
use Index\Db\{DbConnection,DbTools}; use Index\Db\{DbConnection,DbTools};
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait}; use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait};
use Misuzu\{Pagination,SiteInfo,Template}; use Misuzu\{Pagination,SiteInfo,Template};
@ -12,8 +14,8 @@ use Misuzu\Auth\AuthInfo;
use Misuzu\Changelog\Changelog; use Misuzu\Changelog\Changelog;
use Misuzu\Comments\Comments; use Misuzu\Comments\Comments;
use Misuzu\Counters\Counters; use Misuzu\Counters\Counters;
use Misuzu\News\News; use Misuzu\News\{News,NewsCategoryInfo,NewsPostInfo};
use Misuzu\Users\UsersContext; use Misuzu\Users\{UsersContext,UserInfo};
class HomeRoutes implements RouteHandler, UrlSource { class HomeRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait; use RouteHandlerTrait, UrlSourceTrait;
@ -30,6 +32,16 @@ class HomeRoutes implements RouteHandler, UrlSource {
private UsersContext $usersCtx private UsersContext $usersCtx
) {} ) {}
/**
* @return array{
* 'users:active': int,
* 'users:online:recent': int,
* 'users:online:today': int,
* 'comments:posts:visible': int,
* 'forum:topics:visible': int,
* 'forum:posts:visible': int,
* }
*/
private function getStats(): array { private function getStats(): array {
return $this->counters->get([ return $this->counters->get([
'users:active', 'users:active',
@ -41,6 +53,7 @@ class HomeRoutes implements RouteHandler, UrlSource {
]); ]);
} }
/** @return \Iterator<int, UserInfo> */
private function getOnlineUsers(): iterable { private function getOnlineUsers(): iterable {
return $this->usersCtx->users->getUsers( return $this->usersCtx->users->getUsers(
lastActiveInMinutes: 5, lastActiveInMinutes: 5,
@ -49,8 +62,18 @@ class HomeRoutes implements RouteHandler, UrlSource {
); );
} }
/** @var array<string, NewsCategoryInfo> */
private array $newsCategoryInfos = []; private array $newsCategoryInfos = [];
/**
* @return array{
* post: NewsPostInfo,
* category: NewsCategoryInfo,
* user: UserInfo,
* user_colour: Colour,
* comments_count: int,
* }[]|NewsPostInfo[]
*/
private function getFeaturedNewsPosts(int $amount, bool $decorate): iterable { private function getFeaturedNewsPosts(int $amount, bool $decorate): iterable {
$postInfos = $this->news->getPosts( $postInfos = $this->news->getPosts(
onlyFeatured: true, onlyFeatured: true,
@ -86,6 +109,17 @@ class HomeRoutes implements RouteHandler, UrlSource {
return $posts; return $posts;
} }
/**
* @param string[] $categoryIds
* @return array{
* topic_id: int,
* forum_id: int,
* topic_title: string,
* forum_icon: string,
* topic_count_views: int,
* topic_count_posts: int,
* }[]
*/
public function getPopularForumTopics(array $categoryIds): array { public function getPopularForumTopics(array $categoryIds): array {
$args = 0; $args = 0;
$stmt = $this->dbConn->prepare( $stmt = $this->dbConn->prepare(
@ -118,6 +152,18 @@ class HomeRoutes implements RouteHandler, UrlSource {
return $topics; return $topics;
} }
/**
* @param string[] $categoryIds
* @return array{
* topic_id: int,
* forum_id: int,
* topic_title: string,
* forum_icon: string,
* topic_count_views: int,
* topic_count_posts: int,
* latest_post_id: int,
* }[]
*/
public function getActiveForumTopics(array $categoryIds): array { public function getActiveForumTopics(array $categoryIds): array {
$args = 0; $args = 0;
$stmt = $this->dbConn->prepare( $stmt = $this->dbConn->prepare(
@ -154,13 +200,13 @@ class HomeRoutes implements RouteHandler, UrlSource {
#[HttpGet('/')] #[HttpGet('/')]
#[UrlFormat('index', '/')] #[UrlFormat('index', '/')]
public function getIndex(...$args) { public function getIndex(HttpResponseBuilder $response, HttpRequest $request): string {
return $this->authInfo->isLoggedIn return $this->authInfo->isLoggedIn
? $this->getHome(...$args) ? $this->getHome()
: $this->getLanding(...$args); : $this->getLanding($response, $request);
} }
public function getHome() { public function getHome(): string {
$stats = $this->getStats(); $stats = $this->getStats();
$onlineUserInfos = iterator_to_array($this->getOnlineUsers()); $onlineUserInfos = iterator_to_array($this->getOnlineUsers());
$featuredNews = $this->getFeaturedNewsPosts(5, true); $featuredNews = $this->getFeaturedNewsPosts(5, true);
@ -200,7 +246,7 @@ class HomeRoutes implements RouteHandler, UrlSource {
} }
#[HttpGet('/_landing')] #[HttpGet('/_landing')]
public function getLanding($response, $request) { public function getLanding(HttpResponseBuilder $response, HttpRequest $request): string {
$config = $this->config->getValues([ $config = $this->config->getValues([
['social.embed_linked:b'], ['social.embed_linked:b'],
['landing.forum_categories:a'], ['landing.forum_categories:a'],

View file

@ -4,7 +4,8 @@ namespace Misuzu\Imaging;
use InvalidArgumentException; use InvalidArgumentException;
final class GdImage extends Image { final class GdImage extends Image {
private $gd; private \GdImage $gd;
private int $type; private int $type;
private const CONSTRUCTORS = [ private const CONSTRUCTORS = [
@ -25,11 +26,10 @@ final class GdImage extends Image {
IMAGETYPE_WEBP => 'imagewebp', IMAGETYPE_WEBP => 'imagewebp',
]; ];
public function __construct($pathOrWidth, int $height = -1) { public function __construct(int|string $pathOrWidth, int $height = -1) {
parent::__construct($pathOrWidth); $handle = false;
if(is_int($pathOrWidth)) { if(is_int($pathOrWidth)) {
$this->gd = imagecreatetruecolor($pathOrWidth, $height < 1 ? $pathOrWidth : $height); $handle = imagecreatetruecolor($pathOrWidth, $height < 1 ? $pathOrWidth : $height);
$this->type = IMAGETYPE_PNG; $this->type = IMAGETYPE_PNG;
} elseif(is_string($pathOrWidth)) { } elseif(is_string($pathOrWidth)) {
$imageInfo = getimagesize($pathOrWidth); $imageInfo = getimagesize($pathOrWidth);
@ -38,17 +38,18 @@ final class GdImage extends Image {
$this->type = $imageInfo[2]; $this->type = $imageInfo[2];
if(isset(self::CONSTRUCTORS[$this->type])) if(isset(self::CONSTRUCTORS[$this->type]))
$this->gd = self::CONSTRUCTORS[$this->type]($pathOrWidth); $handle = self::CONSTRUCTORS[$this->type]($pathOrWidth);
} }
} }
if(!isset($this->gd)) if(!($handle instanceof \GdImage))
throw new InvalidArgumentException('Unsupported image format.'); throw new InvalidArgumentException('Unsupported image format.');
$this->gd = $handle;
} }
public function __destruct() { public function __destruct() {
if(isset($this->gd)) $this->destroy();
$this->destroy();
} }
public function getWidth(): int { public function getWidth(): int {
@ -103,9 +104,7 @@ final class GdImage extends Image {
} }
public function destroy(): void { public function destroy(): void {
if(imagedestroy($this->gd)) { if(imagedestroy($this->gd))
$this->gd = null;
$this->type = 0; $this->type = 0;
}
} }
} }

View file

@ -5,12 +5,7 @@ use InvalidArgumentException;
use UnexpectedValueException; use UnexpectedValueException;
abstract class Image { abstract class Image {
public function __construct($pathOrWidth) { public static function create(int|string $pathOrWidth, int $height = -1): Image {
if(!is_int($pathOrWidth) && !is_string($pathOrWidth))
throw new InvalidArgumentException('The first argument must be or type string to open an image file, or int to set a width for a new image.');
}
public static function create($pathOrWidth, int $height = -1): Image {
if(extension_loaded('imagick')) if(extension_loaded('imagick'))
return new ImagickImage($pathOrWidth, $height); return new ImagickImage($pathOrWidth, $height);
if(extension_loaded('gd')) if(extension_loaded('gd'))

View file

@ -7,9 +7,7 @@ use InvalidArgumentException;
final class ImagickImage extends Image { final class ImagickImage extends Image {
private ?Imagick $imagick = null; private ?Imagick $imagick = null;
public function __construct($pathOrWidth, int $height = -1) { public function __construct(int|string $pathOrWidth, int $height = -1) {
parent::__construct($pathOrWidth);
if(is_int($pathOrWidth)) { if(is_int($pathOrWidth)) {
$this->imagick = new Imagick(); $this->imagick = new Imagick();
$this->imagick->newImage($pathOrWidth, $height < 1 ? $pathOrWidth : $height, 'none'); $this->imagick->newImage($pathOrWidth, $height < 1 ? $pathOrWidth : $height, 'none');

View file

@ -1,6 +1,7 @@
<?php <?php
namespace Misuzu\Info; namespace Misuzu\Info;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait}; use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait};
use Misuzu\Template; use Misuzu\Template;
@ -21,14 +22,14 @@ class InfoRoutes implements RouteHandler, UrlSource {
#[HttpGet('/info')] #[HttpGet('/info')]
#[UrlFormat('info-index', '/info')] #[UrlFormat('info-index', '/info')]
public function getIndex() { public function getIndex(): string {
return Template::renderRaw('info.index'); return Template::renderRaw('info.index');
} }
#[HttpGet('/info/([A-Za-z0-9_]+)')] #[HttpGet('/info/([A-Za-z0-9_]+)')]
#[UrlFormat('info', '/info/<title>')] #[UrlFormat('info', '/info/<title>')]
#[UrlFormat('info-doc', '/info/<title>')] #[UrlFormat('info-doc', '/info/<title>')]
public function getDocsPage($response, $request, string $name) { public function getDocsPage(HttpResponseBuilder $response, HttpRequest $request, string $name): string {
return $this->serveMarkdownDocument( return $this->serveMarkdownDocument(
sprintf('%s/%s.md', self::DOCS_PATH, $name) sprintf('%s/%s.md', self::DOCS_PATH, $name)
); );
@ -74,7 +75,7 @@ class InfoRoutes implements RouteHandler, UrlSource {
#[HttpGet('/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')] #[HttpGet('/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')]
#[UrlFormat('info-project-doc', '/info/<project>/<title>')] #[UrlFormat('info-project-doc', '/info/<project>/<title>')]
public function getProjectPage($response, $request, string $project, string $name) { public function getProjectPage(HttpResponseBuilder $response, HttpRequest $request, string $project, string $name): int|string {
if(!array_key_exists($project, self::PROJECT_PATHS)) if(!array_key_exists($project, self::PROJECT_PATHS))
return 404; return 404;
@ -99,7 +100,7 @@ class InfoRoutes implements RouteHandler, UrlSource {
return $this->serveMarkdownDocument($path, $titleSuffix); return $this->serveMarkdownDocument($path, $titleSuffix);
} }
private function serveMarkdownDocument(string $path, string $titleFormat = '') { private function serveMarkdownDocument(string $path, string $titleFormat = ''): int|string {
if(!is_file($path)) if(!is_file($path))
return 404; return 404;

View file

@ -1,6 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource};
@ -126,27 +127,27 @@ class LegacyRoutes implements RouteHandler, UrlSource {
} }
#[HttpGet('/index.php')] #[HttpGet('/index.php')]
public function getIndexPHP($response): void { public function getIndexPHP(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('index'), true); $response->redirect($this->urls->format('index'), true);
} }
#[HttpGet('/info.php')] #[HttpGet('/info.php')]
public function getInfoPHP($response): void { public function getInfoPHP(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('info'), true); $response->redirect($this->urls->format('info'), true);
} }
#[HttpGet('/info.php/([A-Za-z0-9_]+)')] #[HttpGet('/info.php/([A-Za-z0-9_]+)')]
public function getInfoDocsPHP($response, $request, string $name): void { public function getInfoDocsPHP(HttpResponseBuilder $response, HttpRequest $request, string $name): void {
$response->redirect($this->urls->format('info', ['title' => $name]), true); $response->redirect($this->urls->format('info', ['title' => $name]), true);
} }
#[HttpGet('/info.php/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')] #[HttpGet('/info.php/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')]
public function getInfoProjectPHP($response, $request, string $project, string $name): void { public function getInfoProjectPHP(HttpResponseBuilder $response, HttpRequest $request, string $project, string $name): void {
$response->redirect($this->urls->format('info', ['title' => $project . '/' . $name]), true); $response->redirect($this->urls->format('info', ['title' => $project . '/' . $name]), true);
} }
#[HttpGet('/news.php')] #[HttpGet('/news.php')]
public function getNewsPHP($response, $request): void { public function getNewsPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$postId = (int)($request->getParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('p', FILTER_SANITIZE_NUMBER_INT)); $postId = (int)($request->getParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('p', FILTER_SANITIZE_NUMBER_INT));
if($postId > 0) if($postId > 0)
@ -161,14 +162,14 @@ class LegacyRoutes implements RouteHandler, UrlSource {
} }
#[HttpGet('/news/index.php')] #[HttpGet('/news/index.php')]
public function getNewsIndexPHP($response, $request): void { public function getNewsIndexPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$response->redirect($this->urls->format('news-index', [ $response->redirect($this->urls->format('news-index', [
'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT), 'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT),
]), true); ]), true);
} }
#[HttpGet('/news/category.php')] #[HttpGet('/news/category.php')]
public function getNewsCategoryPHP($response, $request): void { public function getNewsCategoryPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$response->redirect($this->urls->format('news-category', [ $response->redirect($this->urls->format('news-category', [
'category' => $request->getParam('c', FILTER_SANITIZE_NUMBER_INT), 'category' => $request->getParam('c', FILTER_SANITIZE_NUMBER_INT),
'page' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), 'page' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
@ -176,7 +177,7 @@ class LegacyRoutes implements RouteHandler, UrlSource {
} }
#[HttpGet('/news/post.php')] #[HttpGet('/news/post.php')]
public function getNewsPostPHP($response, $request): void { public function getNewsPostPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$response->redirect($this->urls->format('news-post', [ $response->redirect($this->urls->format('news-post', [
'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), 'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]), true); ]), true);
@ -187,7 +188,7 @@ class LegacyRoutes implements RouteHandler, UrlSource {
#[HttpGet('/news/feed.php')] #[HttpGet('/news/feed.php')]
#[HttpGet('/news/feed.php/rss')] #[HttpGet('/news/feed.php/rss')]
#[HttpGet('/news/feed.php/atom')] #[HttpGet('/news/feed.php/atom')]
public function getNewsFeedPHP($response, $request): void { public function getNewsFeedPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT); $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$response->redirect($this->urls->format( $response->redirect($this->urls->format(
$catId > 0 ? 'news-category-feed' : 'news-feed', $catId > 0 ? 'news-category-feed' : 'news-feed',
@ -196,7 +197,7 @@ class LegacyRoutes implements RouteHandler, UrlSource {
} }
#[HttpGet('/changelog.php')] #[HttpGet('/changelog.php')]
public function getChangelogPHP($response, $request): void { public function getChangelogPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT); $changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
if($changeId) { if($changeId) {
$response->redirect($this->urls->format('changelog-change', ['change' => $changeId]), true); $response->redirect($this->urls->format('changelog-change', ['change' => $changeId]), true);
@ -210,7 +211,7 @@ class LegacyRoutes implements RouteHandler, UrlSource {
} }
#[HttpGet('/auth.php')] #[HttpGet('/auth.php')]
public function getAuthPHP($response, $request): void { public function getAuthPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$response->redirect($this->urls->format(match($request->getParam('m')) { $response->redirect($this->urls->format(match($request->getParam('m')) {
'logout' => 'auth-logout', 'logout' => 'auth-logout',
'reset' => 'auth-reset', 'reset' => 'auth-reset',
@ -222,43 +223,43 @@ class LegacyRoutes implements RouteHandler, UrlSource {
#[HttpGet('/auth')] #[HttpGet('/auth')]
#[HttpGet('/auth/index.php')] #[HttpGet('/auth/index.php')]
public function getAuthIndexPHP($response, $request): void { public function getAuthIndexPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$response->redirect($this->urls->format('auth-login'), true); $response->redirect($this->urls->format('auth-login'), true);
} }
#[HttpGet('/settings.php')] #[HttpGet('/settings.php')]
public function getSettingsPHP($response): void { public function getSettingsPHP(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('settings-index'), true); $response->redirect($this->urls->format('settings-index'), true);
} }
#[HttpGet('/settings')] #[HttpGet('/settings')]
public function getSettingsIndex($response): void { public function getSettingsIndex(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('settings-account')); $response->redirect($this->urls->format('settings-account'));
} }
#[HttpGet('/settings/index.php')] #[HttpGet('/settings/index.php')]
public function getSettingsIndexPHP($response): void { public function getSettingsIndexPHP(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('settings-account'), true); $response->redirect($this->urls->format('settings-account'), true);
} }
#[HttpGet('/manage')] #[HttpGet('/manage')]
#[UrlFormat('manage-index', '/manage')] #[UrlFormat('manage-index', '/manage')]
public function getManageIndex($response): void { public function getManageIndex(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('manage-general-overview')); $response->redirect($this->urls->format('manage-general-overview'));
} }
#[HttpGet('/manage/index.php')] #[HttpGet('/manage/index.php')]
public function getManageIndexPHP($response): void { public function getManageIndexPHP(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('manage-general-overview'), true); $response->redirect($this->urls->format('manage-general-overview'), true);
} }
#[HttpGet('/manage/news')] #[HttpGet('/manage/news')]
public function getManageNewsIndex($response): void { public function getManageNewsIndex(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('manage-news-categories')); $response->redirect($this->urls->format('manage-news-categories'));
} }
#[HttpGet('/manage/news/index.php')] #[HttpGet('/manage/news/index.php')]
public function getManageNewsIndexPHP($response): void { public function getManageNewsIndexPHP(HttpResponseBuilder $response): void {
$response->redirect($this->urls->format('manage-news-categories'), true); $response->redirect($this->urls->format('manage-news-categories'), true);
} }
} }

View file

@ -6,15 +6,17 @@ use Index\Config\Config;
use Symfony\Component\Mime\Email as SymfonyMessage; use Symfony\Component\Mime\Email as SymfonyMessage;
use Symfony\Component\Mime\Address as SymfonyAddress; use Symfony\Component\Mime\Address as SymfonyAddress;
use Symfony\Component\Mailer\Transport as SymfonyTransport; use Symfony\Component\Mailer\Transport as SymfonyTransport;
use Symfony\Component\Mailer\Transport\TransportInterface as SymfonyTransportInterface;
final class Mailer { final class Mailer {
private const TEMPLATE_PATH = MSZ_ROOT . '/config/emails/%s.txt'; private const TEMPLATE_PATH = MSZ_ROOT . '/config/emails/%s.txt';
private static Config $config; private static Config $config;
private static $transport = null; private static ?SymfonyTransportInterface $transport;
public static function init(Config $config): void { public static function init(Config $config): void {
self::$config = $config; self::$config = $config;
self::$transport = null;
} }
private static function createDsn(): string { private static function createDsn(): string {
@ -47,15 +49,15 @@ final class Mailer {
return $dsn; return $dsn;
} }
public static function getTransport() { public static function getTransport(): SymfonyTransportInterface {
if(self::$transport === null) self::$transport ??= SymfonyTransport::fromDsn(self::createDsn());
self::$transport = SymfonyTransport::fromDsn(self::createDsn());
return self::$transport; return self::$transport;
} }
/** @param array<int|string, mixed> $to */
public static function sendMessage(array $to, string $subject, string $contents, bool $bcc = false): bool { public static function sendMessage(array $to, string $subject, string $contents, bool $bcc = false): bool {
foreach($to as $email => $name) { foreach($to as $email => $name) {
$to = new SymfonyAddress($email, $name); $to = new SymfonyAddress((string)$email, (string)$name);
break; break;
} }
@ -83,6 +85,10 @@ final class Mailer {
return true; return true;
} }
/**
* @param array<string, scalar> $vars
* @return array{subject: string, message: string}
*/
public static function template(string $name, array $vars = []): array { public static function template(string $name, array $vars = []): array {
$path = sprintf(self::TEMPLATE_PATH, $name); $path = sprintf(self::TEMPLATE_PATH, $name);

View file

@ -82,6 +82,7 @@ class MessagesDatabase {
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }
/** @return \Iterator<int, MessageInfo> */
public function getMessages( public function getMessages(
UserInfo|string|null $ownerInfo = null, UserInfo|string|null $ownerInfo = null,
UserInfo|string|null $authorInfo = null, UserInfo|string|null $authorInfo = null,
@ -277,6 +278,7 @@ class MessagesDatabase {
$stmt->execute(); $stmt->execute();
} }
/** @param MessageInfo|string|null|array<MessageInfo|string> $messageInfos */
public function deleteMessages( public function deleteMessages(
UserInfo|string|null $ownerInfo, UserInfo|string|null $ownerInfo,
MessageInfo|array|string|null $messageInfos MessageInfo|array|string|null $messageInfos
@ -315,6 +317,7 @@ class MessagesDatabase {
$stmt->execute(); $stmt->execute();
} }
/** @param MessageInfo|string|null|array<MessageInfo|string> $messageInfos */
public function restoreMessages( public function restoreMessages(
UserInfo|string|null $ownerInfo, UserInfo|string|null $ownerInfo,
MessageInfo|array|string|null $messageInfos MessageInfo|array|string|null $messageInfos
@ -353,6 +356,7 @@ class MessagesDatabase {
$stmt->execute(); $stmt->execute();
} }
/** @param MessageInfo|string|null|array<MessageInfo|string> $messageInfos */
public function nukeMessages( public function nukeMessages(
UserInfo|string|null $ownerInfo, UserInfo|string|null $ownerInfo,
MessageInfo|array|string|null $messageInfos MessageInfo|array|string|null $messageInfos

View file

@ -6,6 +6,8 @@ use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\XString; use Index\XString;
use Index\Config\Config; use Index\Config\Config;
use Index\Colour\Colour;
use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
use Misuzu\{CSRF,Pagination,Perm,Template}; use Misuzu\{CSRF,Pagination,Perm,Template};
@ -35,8 +37,9 @@ class MessagesRoutes implements RouteHandler, UrlSource {
private bool $canSendMessages; private bool $canSendMessages;
/** @return void|int|array{error: array{name: string, text: string}} */
#[HttpMiddleware('/messages')] #[HttpMiddleware('/messages')]
public function checkAccess($response, $request) { public function checkAccess(HttpResponseBuilder $response, HttpRequest $request) {
// should probably be a permission or something too // should probably be a permission or something too
if(!$this->authInfo->isLoggedIn) if(!$this->authInfo->isLoggedIn)
return 401; return 401;
@ -52,8 +55,11 @@ class MessagesRoutes implements RouteHandler, UrlSource {
$this->canSendMessages = $globalPerms->check(Perm::G_MESSAGES_SEND) $this->canSendMessages = $globalPerms->check(Perm::G_MESSAGES_SEND)
&& !$this->usersCtx->hasActiveBan($this->authInfo->userInfo); && !$this->usersCtx->hasActiveBan($this->authInfo->userInfo);
if($request->getMethod() === 'POST' && $request->isFormContent()) { if($request->getMethod() === 'POST') {
$content = $request->getContent(); $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
if(!$content->hasParam('_csrfp') || !CSRF::validate((string)$content->getParam('_csrfp'))) if(!$content->hasParam('_csrfp') || !CSRF::validate((string)$content->getParam('_csrfp')))
return [ return [
'error' => [ 'error' => [
@ -66,6 +72,15 @@ class MessagesRoutes implements RouteHandler, UrlSource {
} }
} }
/**
* @return object{
* info: MessageInfo,
* author_info: UserInfo,
* author_colour: Colour,
* recipient_info: UserInfo,
* recipient_colour: Colour
* }
*/
private function populateMessage(MessageInfo $messageInfo): object { private function populateMessage(MessageInfo $messageInfo): object {
$message = new stdClass; $message = new stdClass;
@ -80,7 +95,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
#[HttpGet('/messages')] #[HttpGet('/messages')]
#[UrlFormat('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])] #[UrlFormat('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])]
public function getIndex($response, $request, string $folderName = '') { public function getIndex(HttpResponseBuilder $response, HttpRequest $request, string $folderName = ''): int|string {
$folderName = (string)$request->getParam('folder'); $folderName = (string)$request->getParam('folder');
if($folderName === '') if($folderName === '')
$folderName = 'inbox'; $folderName = 'inbox';
@ -131,9 +146,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
]); ]);
} }
/** @return array{unread: int} */
#[HttpGet('/messages/stats')] #[HttpGet('/messages/stats')]
#[UrlFormat('messages-stats', '/messages/stats')] #[UrlFormat('messages-stats', '/messages/stats')]
public function getStats() { public function getStats(): array {
$selfInfo = $this->authInfo->userInfo; $selfInfo = $this->authInfo->userInfo;
$msgsDb = $this->msgsCtx->database; $msgsDb = $this->msgsCtx->database;
@ -148,15 +164,24 @@ class MessagesRoutes implements RouteHandler, UrlSource {
]; ];
} }
/**
* @return int|array{avatar: string}|array{
* id: string,
* name: string,
* ban: bool,
* avatar: string
* }
*/
#[HttpPost('/messages/recipient')] #[HttpPost('/messages/recipient')]
#[UrlFormat('messages-recipient', '/messages/recipient')] #[UrlFormat('messages-recipient', '/messages/recipient')]
public function postRecipient($response, $request) { public function postRecipient(HttpResponseBuilder $response, HttpRequest $request): int|array {
if(!$request->isFormContent())
return 400;
if(!$this->canSendMessages) if(!$this->canSendMessages)
return 403; return 403;
$content = $request->getContent(); $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
$name = trim((string)$content->getParam('name')); $name = trim((string)$content->getParam('name'));
// flappy hacks // flappy hacks
@ -190,7 +215,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
#[HttpGet('/messages/compose')] #[HttpGet('/messages/compose')]
#[UrlFormat('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])] #[UrlFormat('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])]
public function getEditor($response, $request) { public function getEditor(HttpResponseBuilder $response, HttpRequest $request): int|string {
if(!$this->canSendMessages) if(!$this->canSendMessages)
return 403; return 403;
@ -201,7 +226,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
#[HttpGet('/messages/([A-Za-z0-9]+)')] #[HttpGet('/messages/([A-Za-z0-9]+)')]
#[UrlFormat('messages-view', '/messages/<message>')] #[UrlFormat('messages-view', '/messages/<message>')]
public function getView($response, $request, string $messageId) { public function getView(HttpResponseBuilder $response, HttpRequest $request, string $messageId): int|string {
if(strlen($messageId) !== 8) if(strlen($messageId) !== 8)
return 404; return 404;
@ -257,6 +282,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
]); ]);
} }
/** @return ?array{error: array{name: string, text: string}} */
private function checkCanReceiveMessages(UserInfo|string $userInfo): ?array { private function checkCanReceiveMessages(UserInfo|string $userInfo): ?array {
$globalPerms = $this->perms->getPermissions('global', $userInfo); $globalPerms = $this->perms->getPermissions('global', $userInfo);
if(!$globalPerms->check(Perm::G_MESSAGES_VIEW)) if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
@ -270,6 +296,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
return null; return null;
} }
/** @return ?array{error: array{name: string, text: string, args?: scalar[]}} */
private function checkMessageFields(string $title, string $body, int $parser): ?array { private function checkMessageFields(string $title, string $body, int $parser): ?array {
if(!Parser::isValid($parser)) if(!Parser::isValid($parser))
return [ return [
@ -329,15 +356,17 @@ class MessagesRoutes implements RouteHandler, UrlSource {
return null; return null;
} }
/** @return int|array{error: array{name: string, text: string}}|array{id: string, url: string} */
#[HttpPost('/messages/create')] #[HttpPost('/messages/create')]
#[UrlFormat('messages-create', '/messages/create')] #[UrlFormat('messages-create', '/messages/create')]
public function postCreate($response, $request) { public function postCreate(HttpResponseBuilder $response, HttpRequest $request): int|array {
if(!$request->isFormContent())
return 400;
if(!$this->canSendMessages) if(!$this->canSendMessages)
return 403; return 403;
$content = $request->getContent(); $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
$recipient = (string)$content->getParam('recipient'); $recipient = (string)$content->getParam('recipient');
$replyTo = (string)$content->getParam('reply'); $replyTo = (string)$content->getParam('reply');
$title = (string)$content->getParam('title'); $title = (string)$content->getParam('title');
@ -432,15 +461,17 @@ class MessagesRoutes implements RouteHandler, UrlSource {
]; ];
} }
/** @return int|array{error: array{name: string, text: string}}|array{id: string, url: string} */
#[HttpPost('/messages/([A-Za-z0-9]+)')] #[HttpPost('/messages/([A-Za-z0-9]+)')]
#[UrlFormat('messages-update', '/messages/<message>')] #[UrlFormat('messages-update', '/messages/<message>')]
public function postUpdate($response, $request, string $messageId) { public function postUpdate(HttpResponseBuilder $response, HttpRequest $request, string $messageId): int|array {
if(!$request->isFormContent())
return 400;
if(!$this->canSendMessages) if(!$this->canSendMessages)
return 403; return 403;
$content = $request->getContent(); $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
$title = (string)$content->getParam('title'); $title = (string)$content->getParam('title');
$body = (string)$content->getParam('body'); $body = (string)$content->getParam('body');
$parser = (int)$content->getParam('parser', FILTER_SANITIZE_NUMBER_INT); $parser = (int)$content->getParam('parser', FILTER_SANITIZE_NUMBER_INT);
@ -523,13 +554,14 @@ class MessagesRoutes implements RouteHandler, UrlSource {
]; ];
} }
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
#[HttpPost('/messages/mark')] #[HttpPost('/messages/mark')]
#[UrlFormat('messages-mark', '/messages/mark')] #[UrlFormat('messages-mark', '/messages/mark')]
public function postMark($response, $request) { public function postMark(HttpResponseBuilder $response, HttpRequest $request): int|array {
if(!$request->isFormContent()) $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400; return 400;
$content = $request->getContent();
$type = (string)$content->getParam('type'); $type = (string)$content->getParam('type');
$messages = explode(',', (string)$content->getParam('messages')); $messages = explode(',', (string)$content->getParam('messages'));
@ -554,13 +586,13 @@ class MessagesRoutes implements RouteHandler, UrlSource {
return []; return [];
} }
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
#[HttpPost('/messages/delete')] #[HttpPost('/messages/delete')]
#[UrlFormat('messages-delete', '/messages/delete')] #[UrlFormat('messages-delete', '/messages/delete')]
public function postDelete($response, $request) { public function postDelete(HttpResponseBuilder $response, HttpRequest $request): int|array {
if(!$request->isFormContent())
return 400;
$content = $request->getContent(); $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
$messages = (string)$content->getParam('messages'); $messages = (string)$content->getParam('messages');
if($messages === '') if($messages === '')
@ -581,13 +613,13 @@ class MessagesRoutes implements RouteHandler, UrlSource {
return []; return [];
} }
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
#[HttpPost('/messages/restore')] #[HttpPost('/messages/restore')]
#[UrlFormat('messages-restore', '/messages/restore')] #[UrlFormat('messages-restore', '/messages/restore')]
public function postRestore($response, $request) { public function postRestore(HttpResponseBuilder $response, HttpRequest $request) {
if(!$request->isFormContent())
return 400;
$content = $request->getContent(); $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
$messages = (string)$content->getParam('messages'); $messages = (string)$content->getParam('messages');
if($messages === '') if($messages === '')
@ -608,13 +640,13 @@ class MessagesRoutes implements RouteHandler, UrlSource {
return []; return [];
} }
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
#[HttpPost('/messages/nuke')] #[HttpPost('/messages/nuke')]
#[UrlFormat('messages-nuke', '/messages/nuke')] #[UrlFormat('messages-nuke', '/messages/nuke')]
public function postNuke($response, $request) { public function postNuke(HttpResponseBuilder $response, HttpRequest $request) {
if(!$request->isFormContent())
return 400;
$content = $request->getContent(); $content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400;
$messages = (string)$content->getParam('messages'); $messages = (string)$content->getParam('messages');
if($messages === '') if($messages === '')

View file

@ -88,6 +88,7 @@ class MisuzuContext {
return new FsDbMigrationRepo(MSZ_MIGRATIONS); return new FsDbMigrationRepo(MSZ_MIGRATIONS);
} }
/** @param mixed[] $params */
public function createAuditLog(string $action, array $params = [], UserInfo|string|null $userInfo = null): void { public function createAuditLog(string $action, array $params = [], UserInfo|string|null $userInfo = null): void {
if($userInfo === null && $this->authInfo->isLoggedIn) if($userInfo === null && $this->authInfo->isLoggedIn)
$userInfo = $this->authInfo->userInfo; $userInfo = $this->authInfo->userInfo;

View file

@ -36,6 +36,7 @@ class News {
return $count; return $count;
} }
/** @return \Iterator<int, NewsCategoryInfo> */
public function getCategories( public function getCategories(
?bool $hidden = null, ?bool $hidden = null,
?Pagination $pagination = null ?Pagination $pagination = null
@ -209,6 +210,7 @@ class News {
return $count; return $count;
} }
/** @return \Iterator<int, NewsPostInfo> */
public function getPosts( public function getPosts(
NewsCategoryInfo|string|null $categoryInfo = null, NewsCategoryInfo|string|null $categoryInfo = null,
?string $searchQuery = null, ?string $searchQuery = null,

View file

@ -2,6 +2,8 @@
namespace Misuzu\News; namespace Misuzu\News;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Syndication\FeedBuilder; use Index\Syndication\FeedBuilder;
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
@ -9,7 +11,7 @@ use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\Auth\AuthInfo; use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\{Comments,CommentsCategory,CommentsEx}; use Misuzu\Comments\{Comments,CommentsCategory,CommentsEx};
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\UsersContext; use Misuzu\Users\{UsersContext,UserInfo};
class NewsRoutes implements RouteHandler, UrlSource { class NewsRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait; use RouteHandlerTrait, UrlSourceTrait;
@ -23,8 +25,18 @@ class NewsRoutes implements RouteHandler, UrlSource {
private Comments $comments private Comments $comments
) {} ) {}
/** @var array<string, NewsCategoryInfo> */
private array $categoryInfos = []; private array $categoryInfos = [];
/**
* @return array{
* post: NewsPostInfo,
* category: NewsCategoryInfo,
* user: UserInfo,
* user_colour: Colour,
* comments_count: int
* }[]
*/
private function getNewsPostsForView(Pagination $pagination, ?NewsCategoryInfo $categoryInfo = null): array { private function getNewsPostsForView(Pagination $pagination, ?NewsCategoryInfo $categoryInfo = null): array {
$posts = []; $posts = [];
$postInfos = $this->news->getPosts( $postInfos = $this->news->getPosts(
@ -58,6 +70,13 @@ class NewsRoutes implements RouteHandler, UrlSource {
return $posts; return $posts;
} }
/**
* @return array{
* post: NewsPostInfo,
* category: NewsCategoryInfo,
* user: UserInfo
* }[]
*/
private function getNewsPostsForFeed(?NewsCategoryInfo $categoryInfo = null): array { private function getNewsPostsForFeed(?NewsCategoryInfo $categoryInfo = null): array {
$posts = []; $posts = [];
$postInfos = $this->news->getPosts( $postInfos = $this->news->getPosts(
@ -82,7 +101,7 @@ class NewsRoutes implements RouteHandler, UrlSource {
#[HttpGet('/news')] #[HttpGet('/news')]
#[UrlFormat('news-index', '/news', ['p' => '<page>'])] #[UrlFormat('news-index', '/news', ['p' => '<page>'])]
public function getIndex() { public function getIndex(): int|string {
$categories = $this->news->getCategories(hidden: false); $categories = $this->news->getCategories(hidden: false);
$pagination = new Pagination($this->news->countPosts(onlyFeatured: true), 5); $pagination = new Pagination($this->news->countPosts(onlyFeatured: true), 5);
@ -100,7 +119,7 @@ class NewsRoutes implements RouteHandler, UrlSource {
#[HttpGet('/news/([0-9]+)(?:\.(xml|rss|atom))?')] #[HttpGet('/news/([0-9]+)(?:\.(xml|rss|atom))?')]
#[UrlFormat('news-category', '/news/<category>', ['p' => '<page>'])] #[UrlFormat('news-category', '/news/<category>', ['p' => '<page>'])]
public function getCategory($response, $request, string $categoryId, string $type = '') { public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryId, string $type = ''): int|string {
try { try {
$categoryInfo = $this->news->getCategory(categoryId: $categoryId); $categoryInfo = $this->news->getCategory(categoryId: $categoryId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
@ -126,7 +145,7 @@ class NewsRoutes implements RouteHandler, UrlSource {
#[HttpGet('/news/post/([0-9]+)')] #[HttpGet('/news/post/([0-9]+)')]
#[UrlFormat('news-post', '/news/post/<post>')] #[UrlFormat('news-post', '/news/post/<post>')]
#[UrlFormat('news-post-comments', '/news/post/<post>', fragment: 'comments')] #[UrlFormat('news-post-comments', '/news/post/<post>', fragment: 'comments')]
public function getPost($response, $request, string $postId) { public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $postId): int|string {
try { try {
$postInfo = $this->news->getPost($postId); $postInfo = $this->news->getPost($postId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
@ -163,7 +182,7 @@ class NewsRoutes implements RouteHandler, UrlSource {
#[HttpGet('/news.(?:xml|rss|atom)')] #[HttpGet('/news.(?:xml|rss|atom)')]
#[UrlFormat('news-feed', '/news.xml')] #[UrlFormat('news-feed', '/news.xml')]
#[UrlFormat('news-category-feed', '/news/<category>.xml')] #[UrlFormat('news-category-feed', '/news/<category>.xml')]
public function getFeed($response, $request, ?NewsCategoryInfo $categoryInfo = null) { public function getFeed(HttpResponseBuilder $response, HttpRequest $request, ?NewsCategoryInfo $categoryInfo = null): string {
$response->setContentType('application/rss+xml; charset=utf-8'); $response->setContentType('application/rss+xml; charset=utf-8');
$hasCategory = $categoryInfo !== null; $hasCategory = $categoryInfo !== null;

View file

@ -61,11 +61,13 @@ final class Pagination {
return $this; return $this;
} }
/** @param ?array<string, mixed> $source */
public function readPage(string $name = self::DEFAULT_PARAM, int $default = self::START_PAGE, ?array $source = null): self { public function readPage(string $name = self::DEFAULT_PARAM, int $default = self::START_PAGE, ?array $source = null): self {
$this->setPage(self::param($name, $default, $source)); $this->setPage(self::param($name, $default, $source));
return $this; return $this;
} }
/** @param ?array<string, mixed> $source */
public static function param(string $name = self::DEFAULT_PARAM, int $default = self::START_PAGE, ?array $source = null): int { public static function param(string $name = self::DEFAULT_PARAM, int $default = self::START_PAGE, ?array $source = null): int {
$source ??= $_GET; $source ??= $_GET;

View file

@ -4,8 +4,10 @@ namespace Misuzu\Parsers\BBCode;
use Misuzu\Parsers\ParserInterface; use Misuzu\Parsers\ParserInterface;
class BBCodeParser implements ParserInterface { class BBCodeParser implements ParserInterface {
private $tags = []; /** @var BBCodeTag[] */
private array $tags = [];
/** @param BBCodeTag[] $tags */
public function __construct(array $tags = []) { public function __construct(array $tags = []) {
if(empty($tags)) { if(empty($tags)) {
$tags = [ $tags = [

View file

@ -12,6 +12,10 @@ class MarkdownParser extends Parsedown implements ParserInterface {
return $this->line($line); return $this->line($line);
} }
/**
* @param mixed[] $excerpt
* @return void|mixed[]
*/
protected function inlineImage($excerpt) { protected function inlineImage($excerpt) {
if(!isset($excerpt['text'][1]) || $excerpt['text'][1] !== '[') if(!isset($excerpt['text'][1]) || $excerpt['text'][1] !== '[')
return; return;

View file

@ -20,6 +20,7 @@ final class Parser {
self::MARKDOWN => 'Markdown', self::MARKDOWN => 'Markdown',
]; ];
/** @var array<int, ParserInterface> */
private static $instances = []; private static $instances = [];
public static function isValid(int $parser): bool { public static function isValid(int $parser): bool {

View file

@ -384,6 +384,15 @@ final class Perm {
? self::LABELS[$category][$permission] : ''; ? self::LABELS[$category][$permission] : '';
} }
/**
* @param string[] $sections
* @return object{
* category: string,
* name: string,
* title: string,
* value: int
* }[]
*/
public static function createList(array $sections): array { public static function createList(array $sections): array {
$list = []; $list = [];
@ -423,6 +432,11 @@ final class Perm {
return $list; return $list;
} }
/**
* @param array<string, string> $raw
* @param string[] $categories
* @return array<string, array<string, int>>
*/
public static function convertSubmission(array $raw, array $categories): array { public static function convertSubmission(array $raw, array $categories): array {
$apply = []; $apply = [];

View file

@ -4,6 +4,7 @@ namespace Misuzu\Perms;
interface IPermissionResult { interface IPermissionResult {
public function getCalculated(): int; public function getCalculated(): int;
public function check(int $perm): bool; public function check(int $perm): bool;
/** @param array<string, int> $perms */
public function checkMany(array $perms): object; public function checkMany(array $perms): object;
public function apply(callable $callable): IPermissionResult; public function apply(callable $callable): IPermissionResult;
} }

View file

@ -24,8 +24,13 @@ class Permissions {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
// this method is purely intended for getting the permission data for a single entity /**
// it should not be used to do actual permission checks * this method is purely intended for getting the permission data for a single entity
* it should not be used to do actual permission checks
*
* @param string|null|string[] $categoryNames
* @return PermissionInfo|null|array<string, PermissionInfo>
*/
public function getPermissionInfo( public function getPermissionInfo(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
RoleInfo|string|null $roleInfo = null, RoleInfo|string|null $roleInfo = null,
@ -111,6 +116,7 @@ class Permissions {
$stmt->execute(); $stmt->execute();
} }
/** @param string|null|string[] $categoryNames */
public function removePermissions( public function removePermissions(
array|string|null $categoryNames, array|string|null $categoryNames,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
@ -173,6 +179,7 @@ class Permissions {
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }
/** @param string|string[] $categoryNames */
public function getPermissions( public function getPermissions(
string|array $categoryNames, string|array $categoryNames,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
@ -220,7 +227,11 @@ class Permissions {
return $sets; return $sets;
} }
// precalculates all permissions for fast lookups /**
* precalculates all permissions for fast lookups
*
* @param string[] $userIds
*/
public function precalculatePermissions(ForumCategories $forumCategories, array $userIds = []): void { public function precalculatePermissions(ForumCategories $forumCategories, array $userIds = []): void {
$suppliedUsers = !empty($userIds); $suppliedUsers = !empty($userIds);
$doGuest = !$suppliedUsers; $doGuest = !$suppliedUsers;
@ -297,6 +308,13 @@ class Permissions {
self::precalculatePermissionsLog('Finished permission precalculations!'); self::precalculatePermissionsLog('Finished permission precalculations!');
} }
/**
* precalculates all permissions for fast lookups
*
* @param string[] $userIds
* @param object{info: ForumCategoryInfo, children: object[]} $forumCat
* @param string[] $catIds
*/
private function precalculatePermissionsForForumCategory(DbStatement $insert, array $userIds, object $forumCat, bool $doGuest, array $catIds = []): void { private function precalculatePermissionsForForumCategory(DbStatement $insert, array $userIds, object $forumCat, bool $doGuest, array $catIds = []): void {
$catIds[] = $currentCatId = $forumCat->info->id; $catIds[] = $currentCatId = $forumCat->info->id;
self::precalculatePermissionsLog('Precalcuting permissions for forum category #%s (%s)...', $currentCatId, implode(' <- ', $catIds)); self::precalculatePermissionsLog('Precalcuting permissions for forum category #%s (%s)...', $currentCatId, implode(' <- ', $catIds));
@ -364,6 +382,9 @@ class Permissions {
$this->precalculatePermissionsForForumCategory($insert, $userIds, $forumChild, $doGuest, $catIds); $this->precalculatePermissionsForForumCategory($insert, $userIds, $forumChild, $doGuest, $catIds);
} }
/**
* @param bool|float|int|string|null ...$args
*/
private static function precalculatePermissionsLog(string $fmt, ...$args): void { private static function precalculatePermissionsLog(string $fmt, ...$args): void {
if(!MSZ_CLI) if(!MSZ_CLI)
return; return;

View file

@ -13,6 +13,10 @@ class ProfileFields {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
/**
* @param ?ProfileFieldValueInfo[] $fieldValueInfos
* @return \Iterator<int, ProfileFieldInfo>|ProfileFieldInfo[]
*/
public function getFields(?iterable $fieldValueInfos = null): iterable { public function getFields(?iterable $fieldValueInfos = null): iterable {
$hasFieldValueInfos = $fieldValueInfos !== null; $hasFieldValueInfos = $fieldValueInfos !== null;
if($hasFieldValueInfos) { if($hasFieldValueInfos) {
@ -55,6 +59,11 @@ class ProfileFields {
return ProfileFieldInfo::fromResult($result); return ProfileFieldInfo::fromResult($result);
} }
/**
* @param ?ProfileFieldInfo[] $fieldInfos
* @param ?ProfileFieldValueInfo[] $fieldValueInfos
* @return \Iterator<int, ProfileFieldFormatInfo>|ProfileFieldFormatInfo[]
*/
public function getFieldFormats( public function getFieldFormats(
?iterable $fieldInfos = null, ?iterable $fieldInfos = null,
?iterable $fieldValueInfos = null ?iterable $fieldValueInfos = null
@ -143,6 +152,7 @@ class ProfileFields {
return ProfileFieldFormatInfo::fromResult($result); return ProfileFieldFormatInfo::fromResult($result);
} }
/** @return \Iterator<int, ProfileFieldValueInfo> */
public function getFieldValues(UserInfo|string $userInfo): iterable { public function getFieldValues(UserInfo|string $userInfo): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->id; $userInfo = $userInfo->id;
@ -178,6 +188,10 @@ class ProfileFields {
return ProfileFieldValueInfo::fromResult($result); return ProfileFieldValueInfo::fromResult($result);
} }
/**
* @param ProfileFieldInfo|string|array<string|ProfileFieldInfo> $fieldInfos
* @param string|string[] $values
*/
public function setFieldValues( public function setFieldValues(
UserInfo|string $userInfo, UserInfo|string $userInfo,
ProfileFieldInfo|string|array $fieldInfos, ProfileFieldInfo|string|array $fieldInfos,
@ -236,6 +250,9 @@ class ProfileFields {
$stmt->execute(); $stmt->execute();
} }
/**
* @param ProfileFieldInfo|string|array<string|ProfileFieldInfo> $fieldInfos
*/
public function removeFieldValues( public function removeFieldValues(
UserInfo|string $userInfo, UserInfo|string $userInfo,
ProfileFieldInfo|string|array $fieldInfos ProfileFieldInfo|string|array $fieldInfos
@ -251,7 +268,7 @@ class ProfileFields {
foreach($fieldInfos as $key => $value) { foreach($fieldInfos as $key => $value) {
if($value instanceof ProfileFieldInfo) if($value instanceof ProfileFieldInfo)
$fieldInfos[$key] = $value->id; $fieldInfos[$key] = $value->id;
elseif(is_string($value)) elseif(!is_string($value))
throw new InvalidArgumentException('$fieldInfos array may only contain string IDs or instances of ProfileFieldInfo'); throw new InvalidArgumentException('$fieldInfos array may only contain string IDs or instances of ProfileFieldInfo');
} }

View file

@ -1,6 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Index\Http\HttpRequest;
use Index\Http\Routing\{HttpRouter,Router,RouteHandler}; use Index\Http\Routing\{HttpRouter,Router,RouteHandler};
use Index\Urls\{ArrayUrlRegistry,UrlFormat,UrlRegistry,UrlSource}; use Index\Urls\{ArrayUrlRegistry,UrlFormat,UrlRegistry,UrlSource};
@ -21,7 +22,8 @@ class RoutingContext {
$this->urls->register($handler); $this->urls->register($handler);
} }
public function dispatch(...$args): void { /** @param mixed[] $args */
$this->router->dispatch(...$args); public function dispatch(?HttpRequest $request = null, array $args = []): void {
$this->router->dispatch($request, $args);
} }
} }

View file

@ -4,6 +4,7 @@ namespace Misuzu\Satori;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Config\Config; use Index\Config\Config;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler,RouteHandlerTrait};
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\RoutingContext; use Misuzu\RoutingContext;
@ -21,8 +22,9 @@ final class SatoriRoutes implements RouteHandler {
private ProfileFields $profileFields private ProfileFields $profileFields
) {} ) {}
/** @return void|int */
#[HttpMiddleware('/_satori')] #[HttpMiddleware('/_satori')]
public function verifyRequest($response, $request) { public function verifyRequest(HttpResponseBuilder $response, HttpRequest $request) {
$secretKey = $this->config->getString('secret'); $secretKey = $this->config->getString('secret');
if(!empty($secretKey)) { if(!empty($secretKey)) {
@ -41,8 +43,9 @@ final class SatoriRoutes implements RouteHandler {
} }
} }
/** @return array{error: int}|array{field_value: string} */
#[HttpGet('/_satori/get-profile-field')] #[HttpGet('/_satori/get-profile-field')]
public function getProfileField($response, $request): array { public function getProfileField(HttpResponseBuilder $response, HttpRequest $request): array {
$userId = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT); $userId = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
$fieldId = (string)$request->getParam('field', FILTER_SANITIZE_NUMBER_INT); $fieldId = (string)$request->getParam('field', FILTER_SANITIZE_NUMBER_INT);
@ -57,8 +60,21 @@ final class SatoriRoutes implements RouteHandler {
]; ];
} }
/**
* @return array{
* post_id: int,
* topic_id: int,
* topic_title: string,
* forum_id: int,
* forum_name: string,
* user_id: int,
* username: string,
* user_colour: int,
* is_opening_post: int
* }[]
*/
#[HttpGet('/_satori/get-recent-forum-posts')] #[HttpGet('/_satori/get-recent-forum-posts')]
public function getRecentForumPosts($response, $request): array { public function getRecentForumPosts(HttpResponseBuilder $response, HttpRequest $request): array {
$categoryIds = $this->config->getArray('forum.categories'); $categoryIds = $this->config->getArray('forum.categories');
if(empty($categoryIds)) if(empty($categoryIds))
return []; return [];
@ -99,8 +115,14 @@ final class SatoriRoutes implements RouteHandler {
return $posts; return $posts;
} }
/**
* @return array{
* user_id: int,
* username: string
* }[]
*/
#[HttpGet('/_satori/get-recent-registrations')] #[HttpGet('/_satori/get-recent-registrations')]
public function getRecentRegistrations($response, $request) { public function getRecentRegistrations(HttpResponseBuilder $response, HttpRequest $request): array {
$batchSize = $this->config->getInteger('users.batch', 10); $batchSize = $this->config->getInteger('users.batch', 10);
$backlogDays = $this->config->getInteger('users.backlog', 7); $backlogDays = $this->config->getInteger('users.backlog', 7);
$startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); $startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT);

View file

@ -10,6 +10,7 @@ use Misuzu\Perms\Permissions;
use Misuzu\Users\{Bans,UsersContext,UserInfo}; use Misuzu\Users\{Bans,UsersContext,UserInfo};
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Config\Config; use Index\Config\Config;
use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerTrait};
use Index\Urls\UrlRegistry; use Index\Urls\UrlRegistry;
@ -32,9 +33,10 @@ final class SharpChatRoutes implements RouteHandler {
$this->hashKey = $this->config->getString('hashKey', 'woomy'); $this->hashKey = $this->config->getString('hashKey', 'woomy');
} }
/** @return int|array{Text: string[], Image: string, Hierarchy: int}[] */
#[HttpOptions('/_sockchat/emotes')] #[HttpOptions('/_sockchat/emotes')]
#[HttpGet('/_sockchat/emotes')] #[HttpGet('/_sockchat/emotes')]
public function getEmotes($response, $request): array|int { public function getEmotes(HttpResponseBuilder $response, HttpRequest $request): array|int {
$response->setHeader('Access-Control-Allow-Origin', '*'); $response->setHeader('Access-Control-Allow-Origin', '*');
$response->setHeader('Access-Control-Allow-Methods', 'GET'); $response->setHeader('Access-Control-Allow-Methods', 'GET');
$response->setHeader('Access-Control-Allow-Headers', 'Cache-Control'); $response->setHeader('Access-Control-Allow-Headers', 'Cache-Control');
@ -64,7 +66,7 @@ final class SharpChatRoutes implements RouteHandler {
} }
#[HttpGet('/_sockchat/login')] #[HttpGet('/_sockchat/login')]
public function getLogin($response, $request) { public function getLogin(HttpResponseBuilder $response, HttpRequest $request): void {
if(!$this->authInfo->isLoggedIn) { if(!$this->authInfo->isLoggedIn) {
$response->redirect($this->urls->format('auth-login')); $response->redirect($this->urls->format('auth-login'));
return; return;
@ -84,9 +86,10 @@ final class SharpChatRoutes implements RouteHandler {
return in_array($targetId, $whitelist, true); return in_array($targetId, $whitelist, true);
} }
/** @return int|array{ok: false, err: string}|array{ok: true, usr: int, tkn: string} */
#[HttpOptions('/_sockchat/token')] #[HttpOptions('/_sockchat/token')]
#[HttpGet('/_sockchat/token')] #[HttpGet('/_sockchat/token')]
public function getToken($response, $request) { public function getToken(HttpResponseBuilder $response, HttpRequest $request): int|array {
$host = $request->hasHeader('Host') ? $request->getHeaderFirstLine('Host') : ''; $host = $request->hasHeader('Host') ? $request->getHeaderFirstLine('Host') : '';
$origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : ''; $origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : '';
$originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? ''); $originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');
@ -139,24 +142,25 @@ final class SharpChatRoutes implements RouteHandler {
]; ];
} }
/** @return int|void */
#[HttpPost('/_sockchat/bump')] #[HttpPost('/_sockchat/bump')]
public function postBump($response, $request) { public function postBump(HttpResponseBuilder $response, HttpRequest $request) {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
if($request->isFormContent()) { $content = $request->getContent();
$content = $request->getContent(); if(!($content instanceof FormHttpContent))
return 400;
$bumpList = $content->getParam('u', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); $bumpList = $content->getParam('u', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
if(!is_array($bumpList)) if(!is_array($bumpList))
return 400; return 400;
$userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT); $userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT);
$signature = "bump#{$userTime}"; $signature = "bump#{$userTime}";
foreach($bumpList as $userId => $ipAddr) foreach($bumpList as $userId => $ipAddr)
$signature .= "#{$userId}:{$ipAddr}"; $signature .= "#{$userId}:{$ipAddr}";
} else return 400;
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
$realHash = hash_hmac('sha256', $signature, $this->hashKey); $realHash = hash_hmac('sha256', $signature, $this->hashKey);
@ -169,19 +173,31 @@ final class SharpChatRoutes implements RouteHandler {
$this->usersCtx->users->recordUserActivity($userId, remoteAddr: $ipAddr); $this->usersCtx->users->recordUserActivity($userId, remoteAddr: $ipAddr);
} }
/**
* @return int|array{success: false, reason: string}|array{
* success: true,
* user_id: int,
* username: string,
* colour_raw: int,
* rank: int,
* hierarchy: int,
* perms: int,
* super: bool
* }
*/
#[HttpPost('/_sockchat/verify')] #[HttpPost('/_sockchat/verify')]
public function postVerify($response, $request) { public function postVerify(HttpResponseBuilder $response, HttpRequest $request): int|array {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
if($request->isFormContent()) { $content = $request->getContent();
$content = $request->getContent(); if(!($content instanceof FormHttpContent))
$authMethod = (string)$content->getParam('method');
$authToken = (string)$content->getParam('token');
$ipAddress = (string)$content->getParam('ipaddr');
} else
return ['success' => false, 'reason' => 'request']; return ['success' => false, 'reason' => 'request'];
$authMethod = (string)$content->getParam('method');
$authToken = (string)$content->getParam('token');
$ipAddress = (string)$content->getParam('ipaddr');
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
if(strlen($userHash) !== 64) if(strlen($userHash) !== 64)
return ['success' => false, 'reason' => 'length']; return ['success' => false, 'reason' => 'length'];
@ -293,8 +309,19 @@ final class SharpChatRoutes implements RouteHandler {
]; ];
} }
/**
* @return int|array{
* is_ban: true,
* user_id: string,
* user_name: string,
* user_colour: int,
* ip_addr: string,
* is_perma: bool,
* expires: string
* }[]
*/
#[HttpGet('/_sockchat/bans/list')] #[HttpGet('/_sockchat/bans/list')]
public function getBanList($response, $request) { public function getBanList(HttpResponseBuilder $response, HttpRequest $request): int|array {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
@ -328,8 +355,17 @@ final class SharpChatRoutes implements RouteHandler {
return $list; return $list;
} }
/**
* @return int|array{is_ban: false}|array{
* is_ban: true,
* user_id: string,
* ip_addr: string,
* is_perma: bool,
* expires: string
* }
*/
#[HttpGet('/_sockchat/bans/check')] #[HttpGet('/_sockchat/bans/check')]
public function getBanCheck($response, $request) { public function getBanCheck(HttpResponseBuilder $response, HttpRequest $request): int|array {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
@ -365,12 +401,15 @@ final class SharpChatRoutes implements RouteHandler {
} }
#[HttpPost('/_sockchat/bans/create')] #[HttpPost('/_sockchat/bans/create')]
public function postBanCreate($response, $request): int { public function postBanCreate(HttpResponseBuilder $response, HttpRequest $request): int {
if(!$request->hasHeader('X-SharpChat-Signature') || !$request->isFormContent()) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400;
$content = $request->getContent();
if(!($content instanceof FormHttpContent))
return 400; return 400;
$userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
$content = $request->getContent();
$userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT); $userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT);
$userId = (string)$content->getParam('ui', FILTER_SANITIZE_NUMBER_INT); $userId = (string)$content->getParam('ui', FILTER_SANITIZE_NUMBER_INT);
$userAddr = (string)$content->getParam('ua'); $userAddr = (string)$content->getParam('ua');
@ -439,7 +478,7 @@ final class SharpChatRoutes implements RouteHandler {
} }
#[HttpDelete('/_sockchat/bans/revoke')] #[HttpDelete('/_sockchat/bans/revoke')]
public function deleteBanRevoke($response, $request): int { public function deleteBanRevoke(HttpResponseBuilder $response, HttpRequest $request): int {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;

View file

@ -35,6 +35,7 @@ class TOTPGenerator {
return str_pad((string)$otp, self::DIGITS, '0', STR_PAD_LEFT); return str_pad((string)$otp, self::DIGITS, '0', STR_PAD_LEFT);
} }
/** @return string[] */
public function generateRange(int $range = 1, ?int $timecode = null): array { public function generateRange(int $range = 1, ?int $timecode = null): array {
if($range < 1) if($range < 1)
throw new InvalidArgumentException('$range must be greater than 0.'); throw new InvalidArgumentException('$range must be greater than 0.');

View file

@ -8,6 +8,8 @@ final class Template {
private const FILE_EXT = '.twig'; private const FILE_EXT = '.twig';
private static TplEnvironment $env; private static TplEnvironment $env;
/** @var array<string, mixed> */
private static array $vars = []; private static array $vars = [];
public static function init(TplEnvironment $env): void { public static function init(TplEnvironment $env): void {
@ -18,6 +20,7 @@ final class Template {
self::$env->addFunction($name, $body); self::$env->addFunction($name, $body);
} }
/** @param array<string, mixed> $vars */
public static function renderRaw(string $file, array $vars = []): string { public static function renderRaw(string $file, array $vars = []): string {
if(!defined('MSZ_TPL_RENDER')) if(!defined('MSZ_TPL_RENDER'))
define('MSZ_TPL_RENDER', microtime(true)); define('MSZ_TPL_RENDER', microtime(true));
@ -28,10 +31,15 @@ final class Template {
return self::$env->render($file, array_merge(self::$vars, $vars)); return self::$env->render($file, array_merge(self::$vars, $vars));
} }
/** @param array<string, mixed> $vars */
public static function render(string $file, array $vars = []): void { public static function render(string $file, array $vars = []): void {
echo self::renderRaw($file, $vars); echo self::renderRaw($file, $vars);
} }
/**
* @param string|array<string|int, mixed> $arrayOrKey
* @param mixed $value
*/
public static function set($arrayOrKey, $value = null): void { public static function set($arrayOrKey, $value = null): void {
if(is_string($arrayOrKey)) if(is_string($arrayOrKey))
self::$vars[$arrayOrKey] = $value; self::$vars[$arrayOrKey] = $value;

View file

@ -61,6 +61,16 @@ final class TemplatingExtension extends AbstractExtension {
return $dateTime->diffForHumans(); return $dateTime->diffForHumans();
} }
/**
* @return array{
* title: string,
* url: string,
* menu?: array{
* title: string,
* url: string
* }[]
* }[]
*/
public function getHeaderMenu(): array { public function getHeaderMenu(): array {
$menu = []; $menu = [];
@ -120,6 +130,13 @@ final class TemplatingExtension extends AbstractExtension {
return $menu; return $menu;
} }
/**
* @return array{
* title: string,
* url: string,
* icon: string
* }[]
*/
public function getUserMenu(bool $inBroomCloset, string $manageUrl = ''): array { public function getUserMenu(bool $inBroomCloset, string $manageUrl = ''): array {
$menu = []; $menu = [];
@ -188,6 +205,7 @@ final class TemplatingExtension extends AbstractExtension {
return $menu; return $menu;
} }
/** @return array<string, array<string, string>> */
public function getManageMenu(): array { public function getManageMenu(): array {
$globalPerms = $this->ctx->authInfo->getPerms('global'); $globalPerms = $this->ctx->authInfo->getPerms('global');
if(!$this->ctx->authInfo->isLoggedIn || !$globalPerms->check(Perm::G_IS_JANITOR)) if(!$this->ctx->authInfo->isLoggedIn || !$globalPerms->check(Perm::G_IS_JANITOR))

View file

@ -3,6 +3,7 @@ namespace Misuzu\Users\Assets;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
use Misuzu\Perm; use Misuzu\Perm;
@ -18,7 +19,7 @@ class AssetsRoutes implements RouteHandler, UrlSource {
private UsersContext $usersCtx private UsersContext $usersCtx
) {} ) {}
private function canViewAsset($request, UserInfo $assetUser): bool { private function canViewAsset(HttpRequest $request, UserInfo $assetUser): bool {
if($this->usersCtx->bans->countActiveBans($assetUser)) if($this->usersCtx->bans->countActiveBans($assetUser))
// allow staff viewing profile to still see banned user assets // allow staff viewing profile to still see banned user assets
// should change the Referer check with some query param only applied when needed // should change the Referer check with some query param only applied when needed
@ -28,10 +29,11 @@ class AssetsRoutes implements RouteHandler, UrlSource {
return true; return true;
} }
/** @return void */
#[HttpGet('/assets/avatar')] #[HttpGet('/assets/avatar')]
#[HttpGet('/assets/avatar/([0-9]+)(?:\.[a-z]+)?')] #[HttpGet('/assets/avatar/([0-9]+)(?:\.[a-z]+)?')]
#[UrlFormat('user-avatar', '/assets/avatar/<user>', ['res' => '<res>'])] #[UrlFormat('user-avatar', '/assets/avatar/<user>', ['res' => '<res>'])]
public function getAvatar($response, $request, string $userId = '') { public function getAvatar(HttpResponseBuilder $response, HttpRequest $request, string $userId = '') {
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC); $assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
try { try {
@ -50,10 +52,11 @@ class AssetsRoutes implements RouteHandler, UrlSource {
$this->serveAsset($response, $request, $assetInfo); $this->serveAsset($response, $request, $assetInfo);
} }
/** @return int|void */
#[HttpGet('/assets/profile-background')] #[HttpGet('/assets/profile-background')]
#[HttpGet('/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')] #[HttpGet('/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')]
#[UrlFormat('user-background', '/assets/profile-background/<user>')] #[UrlFormat('user-background', '/assets/profile-background/<user>')]
public function getProfileBackground($response, $request, string $userId = '') { public function getProfileBackground(HttpResponseBuilder $response, HttpRequest $request, string $userId = '') {
try { try {
$userInfo = $this->usersCtx->getUserInfo($userId); $userInfo = $this->usersCtx->getUserInfo($userId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
@ -74,13 +77,16 @@ class AssetsRoutes implements RouteHandler, UrlSource {
$this->serveAsset($response, $request, $assetInfo); $this->serveAsset($response, $request, $assetInfo);
} }
/** @return int|void */
#[HttpGet('/user-assets.php')] #[HttpGet('/user-assets.php')]
public function getUserAssets($response, $request) { public function getUserAssets(HttpResponseBuilder $response, HttpRequest $request) {
$userId = (string)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT); $userId = (string)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT);
$mode = (string)$request->getParam('m'); $mode = (string)$request->getParam('m');
if($mode === 'avatar') if($mode === 'avatar') {
return $this->getAvatar($response, $request, $userId); $this->getAvatar($response, $request, $userId);
return;
}
if($mode === 'background') if($mode === 'background')
return $this->getProfileBackground($response, $request, $userId); return $this->getProfileBackground($response, $request, $userId);
@ -90,7 +96,7 @@ class AssetsRoutes implements RouteHandler, UrlSource {
return 404; return 404;
} }
private function serveAsset($response, $request, UserImageAssetInterface $assetInfo): void { private function serveAsset(HttpResponseBuilder $response, HttpRequest $request, UserImageAssetInterface $assetInfo): void {
$contentType = $assetInfo->getMimeType(); $contentType = $assetInfo->getMimeType();
$publicPath = $assetInfo->getPublicPath(); $publicPath = $assetInfo->getPublicPath();
$fileName = $assetInfo->getFileName(); $fileName = $assetInfo->getFileName();

View file

@ -2,9 +2,9 @@
namespace Misuzu\Users\Assets; namespace Misuzu\Users\Assets;
class StaticUserImageAsset implements UserImageAssetInterface { class StaticUserImageAsset implements UserImageAssetInterface {
private $path = ''; private string $path = '';
private $filename = ''; private string $filename = '';
private $relativePath = ''; private string $relativePath = '';
public function __construct(string $path, string $absolutePart = '') { public function __construct(string $path, string $absolutePart = '') {
$this->path = $path; $this->path = $path;

View file

@ -37,6 +37,7 @@ class UserBackgroundAsset extends UserImageAsset {
self::ATTRIB_SLIDE => 'slide', self::ATTRIB_SLIDE => 'slide',
]; ];
/** @return array<int, string> */
public static function getAttachmentStringOptions(): array { public static function getAttachmentStringOptions(): array {
return [ return [
self::ATTACH_COVER => 'Cover', self::ATTACH_COVER => 'Cover',
@ -121,6 +122,7 @@ class UserBackgroundAsset extends UserImageAsset {
return $this; return $this;
} }
/** @return string[] */
public function getClassNames(string $format = '%s'): array { public function getClassNames(string $format = '%s'): array {
$names = []; $names = [];
$attachment = $this->getAttachment(); $attachment = $this->getAttachment();

View file

@ -32,6 +32,7 @@ abstract class UserImageAsset implements UserImageAssetInterface {
public abstract function getMaxHeight(): int; public abstract function getMaxHeight(): int;
public abstract function getMaxBytes(): int; public abstract function getMaxBytes(): int;
/** @return int[] */
public function getAllowedTypes(): array { public function getAllowedTypes(): array {
return [self::TYPE_PNG, self::TYPE_JPG, self::TYPE_GIF]; return [self::TYPE_PNG, self::TYPE_JPG, self::TYPE_GIF];
} }
@ -39,6 +40,7 @@ abstract class UserImageAsset implements UserImageAssetInterface {
return in_array($type, $this->getAllowedTypes()); return in_array($type, $this->getAllowedTypes());
} }
/** @return mixed[] */
private function getImageSize(): array { private function getImageSize(): array {
return $this->isPresent() && ($imageSize = getimagesize($this->getPath())) ? $imageSize : []; return $this->isPresent() && ($imageSize = getimagesize($this->getPath())) ? $imageSize : [];
} }

View file

@ -57,6 +57,7 @@ class Bans {
return $count; return $count;
} }
/** @return \Iterator<int, BanInfo> */
public function getBans( public function getBans(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?bool $activeOnly = null, ?bool $activeOnly = null,
@ -177,6 +178,7 @@ class Bans {
return $this->getBan((string)$this->dbConn->getLastInsertId()); return $this->getBan((string)$this->dbConn->getLastInsertId());
} }
/** @param BanInfo|string|array<BanInfo|string> $banInfos */
public function deleteBans(BanInfo|string|array $banInfos): void { public function deleteBans(BanInfo|string|array $banInfos): void {
if(!is_array($banInfos)) if(!is_array($banInfos))
$banInfos = [$banInfos]; $banInfos = [$banInfos];

View file

@ -53,6 +53,7 @@ class ModNotes {
return $count; return $count;
} }
/** @return \Iterator<int, ModNoteInfo> */
public function getNotes( public function getNotes(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
UserInfo|string|null $authorInfo = null, UserInfo|string|null $authorInfo = null,
@ -127,6 +128,7 @@ class ModNotes {
return $this->getNote((string)$this->dbConn->getLastInsertId()); return $this->getNote((string)$this->dbConn->getLastInsertId());
} }
/** @param ModNoteInfo|string|array<ModNoteInfo|string> $noteInfos */
public function deleteNotes(ModNoteInfo|string|array $noteInfos): void { public function deleteNotes(ModNoteInfo|string|array $noteInfos): void {
if(!is_array($noteInfos)) if(!is_array($noteInfos))
$noteInfos = [$noteInfos]; $noteInfos = [$noteInfos];

View file

@ -10,11 +10,11 @@ use Misuzu\Pagination;
class Roles { class Roles {
public const DEFAULT_ROLE = '1'; public const DEFAULT_ROLE = '1';
private DbConnection $dbConn;
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) { public function __construct(
$this->dbConn = $dbConn; private DbConnection $dbConn
) {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
@ -52,6 +52,7 @@ class Roles {
return $count; return $count;
} }
/** @return \Iterator<int, RoleInfo> */
public function getRoles( public function getRoles(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?bool $hidden = null, ?bool $hidden = null,
@ -138,6 +139,7 @@ class Roles {
return $this->getRole((string)$this->dbConn->getLastInsertId()); return $this->getRole((string)$this->dbConn->getLastInsertId());
} }
/** @param RoleInfo|string|array<RoleInfo|string> $roleInfos */
public function deleteRoles(RoleInfo|string|array $roleInfos): void { public function deleteRoles(RoleInfo|string|array $roleInfos): void {
if(!is_array($roleInfos)) if(!is_array($roleInfos))
$roleInfos = [$roleInfos]; $roleInfos = [$roleInfos];

View file

@ -12,11 +12,11 @@ use Misuzu\Tools;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
class Users { class Users {
private DbConnection $dbConn;
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) { public function __construct(
$this->dbConn = $dbConn; private DbConnection $dbConn
) {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
@ -98,6 +98,10 @@ class Users {
'random' => ['RAND()', null], 'random' => ['RAND()', null],
]; ];
/**
* @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery
* @return \Iterator<int, UserInfo>|UserInfo[]
*/
public function getUsers( public function getUsers(
RoleInfo|string|null $roleInfo = null, RoleInfo|string|null $roleInfo = null,
UserInfo|string|null $after = null, UserInfo|string|null $after = null,
@ -456,6 +460,10 @@ class Users {
return in_array($roleInfo, $this->hasRoles($userInfo, $roleInfo)); return in_array($roleInfo, $this->hasRoles($userInfo, $roleInfo));
} }
/**
* @param RoleInfo|string|array<RoleInfo|string> $roleInfos
* @return string[]
*/
public function hasRoles( public function hasRoles(
UserInfo|string $userInfo, UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos RoleInfo|string|array $roleInfos
@ -494,6 +502,7 @@ class Users {
return $roleIds; return $roleIds;
} }
/** @param RoleInfo|string|array<RoleInfo|string> $roleInfos */
public function addRoles( public function addRoles(
UserInfo|string $userInfo, UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos RoleInfo|string|array $roleInfos
@ -524,6 +533,7 @@ class Users {
$stmt->execute(); $stmt->execute();
} }
/** @param RoleInfo|string|array<RoleInfo|string> $roleInfos */
public function removeRoles( public function removeRoles(
UserInfo|string $userInfo, UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos RoleInfo|string|array $roleInfos

View file

@ -11,10 +11,16 @@ class UsersContext {
public private(set) Warnings $warnings; public private(set) Warnings $warnings;
public private(set) ModNotes $modNotes; public private(set) ModNotes $modNotes;
/** @var array<string, UserInfo> */
private array $userInfos = []; private array $userInfos = [];
/** @var array<string, Colour> */
private array $userColours = []; private array $userColours = [];
/** @var array<string, int> */
private array $userRanks = []; private array $userRanks = [];
/** @var array<string, ?BanInfo> */
private array $activeBans = []; private array $activeBans = [];
public function __construct(DbConnection $dbConn) { public function __construct(DbConnection $dbConn) {

View file

@ -18,6 +18,26 @@ final class UsersRpcHandler implements RpcHandler {
private UsersContext $usersCtx private UsersContext $usersCtx
) {} ) {}
/**
* @return array{error: string}|array{
* id: string,
* name: string,
* email?: string,
* colour_raw: int,
* colour_css: string,
* rank: int,
* country_code: string,
* roles?: string[],
* is_super?: bool,
* title?: string,
* created_at: string,
* last_active_at?: string,
* profile_url: string,
* avatar_url: string,
* avatar_urls?: array{res: int, url: string}[],
* is_deleted?: bool
* }
*/
#[RpcQuery('misuzu:users:getUser')] #[RpcQuery('misuzu:users:getUser')]
public function queryGetUser(string $userId, bool $includeEMailAddress = false): array { public function queryGetUser(string $userId, bool $includeEMailAddress = false): array {
try { try {

View file

@ -23,6 +23,7 @@ class WarningInfo {
); );
} }
/** @var string[] */
public array $bodyLines { public array $bodyLines {
get => explode("\n", $this->body); get => explode("\n", $this->body);
} }

View file

@ -12,11 +12,11 @@ use Misuzu\Pagination;
class Warnings { class Warnings {
public const VISIBLE_BACKLOG = 90 * 24 * 60 * 60; public const VISIBLE_BACKLOG = 90 * 24 * 60 * 60;
private DbConnection $dbConn;
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) { public function __construct(
$this->dbConn = $dbConn; private DbConnection $dbConn
) {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
@ -55,6 +55,7 @@ class Warnings {
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }
/** @return \Iterator<int, WarningInfo> */
public function getWarningsWithDefaultBacklog( public function getWarningsWithDefaultBacklog(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
@ -66,6 +67,7 @@ class Warnings {
); );
} }
/** @return \Iterator<int, WarningInfo> */
public function getWarnings( public function getWarnings(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?int $backlog = null, ?int $backlog = null,
@ -140,6 +142,7 @@ class Warnings {
return $this->getWarning((string)$this->dbConn->getLastInsertId()); return $this->getWarning((string)$this->dbConn->getLastInsertId());
} }
/** @param WarningInfo|string|array<WarningInfo|string> $warnInfos */
public function deleteWarnings(WarningInfo|string|array $warnInfos): void { public function deleteWarnings(WarningInfo|string|array $warnInfos): void {
if(!is_array($warnInfos)) if(!is_array($warnInfos))
$warnInfos = [$warnInfos]; $warnInfos = [$warnInfos];

View file

@ -53,6 +53,7 @@ final class Zalgo {
|| in_array($char, self::CHARS_MIDDLE); || in_array($char, self::CHARS_MIDDLE);
} }
/** @param string[] $array */
public static function getString(array $array, int $length): string { public static function getString(array $array, int $length): string {
$string = ''; $string = '';
$rngMax = count($array) - 1; $rngMax = count($array) - 1;
@ -110,13 +111,13 @@ final class Zalgo {
} }
if($going_up) if($going_up)
$str .= self::getString(self::CHARS_UP, $num_up); $str .= self::getString(self::CHARS_UP, (int)$num_up);
if($going_mid) if($going_mid)
$str .= self::getString(self::CHARS_MIDDLE, $num_mid); $str .= self::getString(self::CHARS_MIDDLE, (int)$num_mid);
if($going_down) if($going_down)
$str .= self::getString(self::CHARS_DOWN, $num_down); $str .= self::getString(self::CHARS_DOWN, (int)$num_down);
} }
return $str; return $str;