diff --git a/phpstan.neon b/phpstan.neon index f3296db..2a5a8cb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ parameters: - level: 5 + level: 6 + treatPhpDocTypesAsCertain: false paths: - database - src diff --git a/public-legacy/_github-callback.php b/public-legacy/_github-callback.php index 2e93ee9..dda9ee9 100644 --- a/public-legacy/_github-callback.php +++ b/public-legacy/_github-callback.php @@ -57,6 +57,9 @@ if(empty($config['tokens']['token'])) $isGitea = isset($_SERVER['HTTP_X_GITEA_DELIVERY']) && isset($_SERVER['HTTP_X_GITEA_EVENT']); $rawData = file_get_contents('php://input'); +if(!is_string($rawData)) + die('no input data'); + $sigParts = $isGitea ? ['sha256', $_SERVER['HTTP_X_GITEA_SIGNATURE']] : explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '', 2); @@ -94,6 +97,11 @@ if($data->repository->full_name !== $repoName) if($_SERVER['HTTP_X_GITHUB_EVENT'] !== 'push') 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); if($commitCount < 1) die('no commits received'); diff --git a/public-legacy/manage/changelog/change.php b/public-legacy/manage/changelog/change.php index c38883d..8d662ab 100644 --- a/public-legacy/manage/changelog/change.php +++ b/public-legacy/manage/changelog/change.php @@ -44,7 +44,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { } // 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')); $summary = trim((string)filter_input(INPUT_POST, 'cl_summary')); $body = trim((string)filter_input(INPUT_POST, 'cl_body')); diff --git a/public-legacy/manage/changelog/tag.php b/public-legacy/manage/changelog/tag.php index 5b209c0..5c1fa5b 100644 --- a/public-legacy/manage/changelog/tag.php +++ b/public-legacy/manage/changelog/tag.php @@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { 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')); $description = trim((string)filter_input(INPUT_POST, 'ct_desc')); $archive = !empty($_POST['ct_archive']); diff --git a/public-legacy/manage/news/category.php b/public-legacy/manage/news/category.php index dd023b1..dde4ebb 100644 --- a/public-legacy/manage/news/category.php +++ b/public-legacy/manage/news/category.php @@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { 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')); $description = trim((string)filter_input(INPUT_POST, 'nc_desc')); $hidden = !empty($_POST['nc_hidden']); diff --git a/public-legacy/manage/news/post.php b/public-legacy/manage/news/post.php index 2aa7f59..903f7f7 100644 --- a/public-legacy/manage/news/post.php +++ b/public-legacy/manage/news/post.php @@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { 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')); $category = (string)filter_input(INPUT_POST, 'np_category', FILTER_SANITIZE_NUMBER_INT); $featured = !empty($_POST['np_featured']); diff --git a/public-legacy/manage/users/ban.php b/public-legacy/manage/users/ban.php index b2296ee..0304783 100644 --- a/public-legacy/manage/users/ban.php +++ b/public-legacy/manage/users/ban.php @@ -35,7 +35,7 @@ try { $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); $expiresCustom = (string)filter_input(INPUT_POST, 'ub_expires_custom'); $publicReason = trim((string)filter_input(INPUT_POST, 'ub_reason_pub')); diff --git a/public-legacy/manage/users/warning.php b/public-legacy/manage/users/warning.php index cbb24df..55cba84 100644 --- a/public-legacy/manage/users/warning.php +++ b/public-legacy/manage/users/warning.php @@ -33,7 +33,7 @@ try { $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')); Template::set('warn_value_body', $body); diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php index ca38409..c6a5856 100644 --- a/public-legacy/settings/data.php +++ b/public-legacy/settings/data.php @@ -13,7 +13,14 @@ if(!$msz->authInfo->isLoggedIn) $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; $userId = $userInfo->id; diff --git a/src/AuditLog/AuditLog.php b/src/AuditLog/AuditLog.php index 6c23fd2..7ed4ce7 100644 --- a/src/AuditLog/AuditLog.php +++ b/src/AuditLog/AuditLog.php @@ -52,6 +52,7 @@ class AuditLog { return $count; } + /** @return \Iterator */ public function getLogs( UserInfo|string|null $userInfo = null, ?string $remoteAddr = null, @@ -95,6 +96,7 @@ class AuditLog { return $stmt->getResult()->getIterator(AuditLogInfo::fromResult(...)); } + /** @param mixed[] $params */ public function createLog( UserInfo|string|null $userInfo, string $action, diff --git a/src/AuditLog/AuditLogInfo.php b/src/AuditLog/AuditLogInfo.php index 5b85f7e..014f5cd 100644 --- a/src/AuditLog/AuditLogInfo.php +++ b/src/AuditLog/AuditLogInfo.php @@ -6,6 +6,7 @@ use Carbon\CarbonImmutable; use Index\Db\DbResult; class AuditLogInfo { + /** @param scalar[] $params */ public function __construct( public private(set) ?string $userId, public private(set) string $action, diff --git a/src/Auth/AuthInfo.php b/src/Auth/AuthInfo.php index 1da4fd9..e0c429e 100644 --- a/src/Auth/AuthInfo.php +++ b/src/Auth/AuthInfo.php @@ -4,7 +4,7 @@ namespace Misuzu\Auth; use Misuzu\Auth\SessionInfo; use Misuzu\Forum\ForumCategoryInfo; use Misuzu\Perms\IPermissionResult; -use Misuzu\Perms\Permissions; +use Misuzu\Perms\{Permissions,PermissionResult}; use Misuzu\Users\UserInfo; class AuthInfo { @@ -12,6 +12,8 @@ class AuthInfo { public private(set) ?UserInfo $userInfo; public private(set) ?SessionInfo $sessionInfo; public private(set) ?UserInfo $realUserInfo; + + /** @var array */ public private(set) array $perms; public function __construct( diff --git a/src/Auth/AuthRpcHandler.php b/src/Auth/AuthRpcHandler.php index 03da5e1..5500c80 100644 --- a/src/Auth/AuthRpcHandler.php +++ b/src/Auth/AuthRpcHandler.php @@ -23,6 +23,7 @@ final class AuthRpcHandler implements RpcHandler { 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')] public function procAttemptMisuzuAuth(string $remoteAddr, string $token): array { $tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token); diff --git a/src/Auth/AuthTokenBuilder.php b/src/Auth/AuthTokenBuilder.php index 71334e7..3a7f113 100644 --- a/src/Auth/AuthTokenBuilder.php +++ b/src/Auth/AuthTokenBuilder.php @@ -5,15 +5,12 @@ use Misuzu\Auth\SessionInfo; use Misuzu\Users\UserInfo; class AuthTokenBuilder { - private array $props; + /** @var array */ + public private(set) array $props; private bool $edited = false; public function __construct(?AuthTokenInfo $baseTokenInfo = null) { - $this->props = $baseTokenInfo === null ? [] : $baseTokenInfo->getProperties(); - } - - public function getProperties(): array { - return $this->props; + $this->props = $baseTokenInfo === null ? [] : $baseTokenInfo->props; } public function setEdited(): void { diff --git a/src/Auth/AuthTokenInfo.php b/src/Auth/AuthTokenInfo.php index 18f8aec..86cecb2 100644 --- a/src/Auth/AuthTokenInfo.php +++ b/src/Auth/AuthTokenInfo.php @@ -9,15 +9,12 @@ class AuthTokenInfo { public const SESSION_TOKEN_HEX = 't'; // Session token that should be hex encoded public const IMPERSONATED_USER_ID = 'i'; // Impersonated user ID + /** @param array $props */ public function __construct( 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 { return new AuthTokenBuilder($this); } diff --git a/src/Auth/AuthTokenPacker.php b/src/Auth/AuthTokenPacker.php index 2815814..7c9519f 100644 --- a/src/Auth/AuthTokenPacker.php +++ b/src/Auth/AuthTokenPacker.php @@ -10,7 +10,7 @@ class AuthTokenPacker { public function __construct(private string $secretKey) {} public function pack(AuthTokenBuilder|AuthTokenInfo $tokenInfo): string { - $props = $tokenInfo->getProperties(); + $props = $tokenInfo->props; $timestamp = $tokenInfo instanceof AuthTokenInfo ? $tokenInfo->timestamp : time(); $data = ''; diff --git a/src/Auth/LoginAttempts.php b/src/Auth/LoginAttempts.php index 57808c0..5325556 100644 --- a/src/Auth/LoginAttempts.php +++ b/src/Auth/LoginAttempts.php @@ -70,6 +70,7 @@ class LoginAttempts { ); } + /** @return \Iterator */ public function getAttempts( ?bool $success = null, UserInfo|string|null $userInfo = null, diff --git a/src/Auth/Sessions.php b/src/Auth/Sessions.php index a7b6a4b..40db162 100644 --- a/src/Auth/Sessions.php +++ b/src/Auth/Sessions.php @@ -10,11 +10,11 @@ use Misuzu\Pagination; use Misuzu\Users\UserInfo; class Sessions { - private DbConnection $dbConn; private DbStatementCache $cache; - public function __construct(DbConnection $dbConn) { - $this->dbConn = $dbConn; + public function __construct( + private DbConnection $dbConn + ) { $this->cache = new DbStatementCache($dbConn); } @@ -52,6 +52,7 @@ class Sessions { return $count; } + /** @return \Iterator */ public function getSessions( UserInfo|string|null $userInfo = null, ?Pagination $pagination = null @@ -143,6 +144,11 @@ class Sessions { return $this->getSession(sessionId: (string)$this->dbConn->getLastInsertId()); } + /** + * @param SessionInfo|string|array|null $sessionInfos + * @param string|array|null $sessionTokens + * @param UserInfo|string|array|null $userInfos + */ public function deleteSessions( SessionInfo|string|array|null $sessionInfos = null, string|array|null $sessionTokens = null, diff --git a/src/Changelog/Changelog.php b/src/Changelog/Changelog.php index 11f1d51..97baf63 100644 --- a/src/Changelog/Changelog.php +++ b/src/Changelog/Changelog.php @@ -12,13 +12,14 @@ class Changelog { // not a strict list but useful to have public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert']; - private DbConnection $dbConn; private DbStatementCache $cache; + /** @var array */ private array $tags = []; - public function __construct(DbConnection $dbConn) { - $this->dbConn = $dbConn; + public function __construct( + private DbConnection $dbConn + ) { $this->cache = new DbStatementCache($dbConn); } @@ -61,6 +62,7 @@ class Changelog { }; } + /** @param ?array<\Stringable|string> $tags */ public function countChanges( UserInfo|string|null $userInfo = null, DateTimeInterface|int|null $dateTime = null, @@ -113,6 +115,10 @@ class Changelog { return $count; } + /** + * @param ?array<\Stringable|string> $tags + * @return \Iterator + */ public function getChanges( UserInfo|string|null $userInfo = null, DateTimeInterface|int|null $dateTime = null, @@ -267,6 +273,7 @@ class Changelog { $stmt->execute(); } + /** @return ChangeTagInfo[] */ public function getTags( ChangeInfo|string|null $changeInfo = null ): array { diff --git a/src/Changelog/ChangelogRoutes.php b/src/Changelog/ChangelogRoutes.php index 7639420..97cf824 100644 --- a/src/Changelog/ChangelogRoutes.php +++ b/src/Changelog/ChangelogRoutes.php @@ -3,6 +3,7 @@ namespace Misuzu\Changelog; use ErrorException; use RuntimeException; +use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Syndication\FeedBuilder; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; @@ -30,7 +31,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource { #[HttpGet('/changelog')] #[UrlFormat('changelog-index', '/changelog', ['date' => '', 'user' => '', 'tags' => '', 'p' => ''])] - public function getIndex($response, $request) { + public function getIndex(HttpResponseBuilder $response, HttpRequest $request): int|string { $filterDate = (string)$request->getParam('date'); $filterUser = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT); $filterTags = (string)$request->getParam('tags'); @@ -100,7 +101,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource { #[HttpGet('/changelog/change/([0-9]+)')] #[UrlFormat('changelog-change', '/changelog/change/')] #[UrlFormat('changelog-change-comments', '/changelog/change/', fragment: 'comments')] - public function getChange($response, $request, string $changeId) { + public function getChange(HttpResponseBuilder $response, HttpRequest $request, string $changeId): int|string { try { $changeInfo = $this->changelog->getChange($changeId); } catch(RuntimeException $ex) { @@ -121,7 +122,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource { #[HttpGet('/changelog.(xml|rss|atom)')] #[UrlFormat('changelog-feed', '/changelog.xml')] - public function getFeed($response) { + public function getFeed(HttpResponseBuilder $response): string { $response->setContentType('application/rss+xml; charset=utf-8'); $siteName = $this->siteInfo->name; diff --git a/src/ClientInfo.php b/src/ClientInfo.php index b63ab33..67cb649 100644 --- a/src/ClientInfo.php +++ b/src/ClientInfo.php @@ -13,6 +13,11 @@ class ClientInfo implements Stringable, JsonSerializable { 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( private array|bool|null $botInfo, private ?array $clientInfo, @@ -26,6 +31,7 @@ class ClientInfo implements Stringable, JsonSerializable { return self::SERIALIZE_VERSION; } + /** @return bool|null|array{name: string} */ #[JsonProperty('bot')] public function getBotInfo(): bool|array|null { if($this->botInfo === true || is_array($this->botInfo)) @@ -34,11 +40,13 @@ class ClientInfo implements Stringable, JsonSerializable { return null; } + /** @return ?array{name: string, version?: string} */ #[JsonProperty('client')] public function getClientInfo(): ?array { return $this->clientInfo; } + /** @return ?array{name: string, version?: string, platform?: string} */ #[JsonProperty('os')] public function getOsInfo(): ?array { return $this->osInfo; @@ -114,6 +122,7 @@ class ClientInfo implements Stringable, JsonSerializable { ); } + /** @param mixed[]|string $serverVarsOrUserAgent */ public static function parse(array|string $serverVarsOrUserAgent): self { static $dd = null; $dd ??= new DeviceDetector(); diff --git a/src/Comments/Comments.php b/src/Comments/Comments.php index 08aaa80..f55d0ea 100644 --- a/src/Comments/Comments.php +++ b/src/Comments/Comments.php @@ -41,6 +41,7 @@ class Comments { return $count; } + /** @return \Iterator */ public function getCategories( UserInfo|string|null $owner = null, ?Pagination $pagination = null @@ -260,6 +261,7 @@ class Comments { return $count; } + /** @return \Iterator */ public function getPosts( CommentsCategoryInfo|string|null $categoryInfo = null, CommentsPostInfo|string|null $parentInfo = null, diff --git a/src/Counters/Counters.php b/src/Counters/Counters.php index fb3d65a..6fb005f 100644 --- a/src/Counters/Counters.php +++ b/src/Counters/Counters.php @@ -20,6 +20,7 @@ class Counters { 'updated' => 'counter_updated', ]; + /** @return \Iterator */ public function getCounters( ?string $orderBy = null, ?Pagination $pagination = null @@ -48,6 +49,10 @@ class Counters { return $stmt->getResult()->getIterator(CounterInfo::fromResult(...)); } + /** + * @param string|string[] $names + * @return array|int + */ public function get(array|string $names): array|int { if(is_string($names)) { $returnFirst = true; @@ -72,6 +77,7 @@ class Counters { return $returnFirst ? $values[array_key_first($values)] : $values; } + /** @param string|array $nameOrValues */ public function set(string|array $nameOrValues, ?int $value = null): void { if(empty($nameOrValues)) throw new InvalidArgumentException('$nameOrValues may not be empty.'); @@ -97,6 +103,7 @@ class Counters { $stmt->execute(); } + /** @param string|string[] $names */ public function reset(string|array $names): void { if(empty($names)) throw new InvalidArgumentException('$names may not be empty.'); diff --git a/src/Emoticons/Emotes.php b/src/Emoticons/Emotes.php index 4156d74..ac7b3fc 100644 --- a/src/Emoticons/Emotes.php +++ b/src/Emoticons/Emotes.php @@ -33,11 +33,15 @@ class Emotes { return EmoteInfo::fromResult($result); } + /** @return string[] */ public static function emoteOrderOptions(): array { return array_keys(self::EMOTE_ORDER); } - // TODO: pagination + /** + * @todo pagination + * @return \Iterator + */ public function getEmotes( ?int $minRank = null, ?string $orderBy = null, @@ -149,6 +153,7 @@ class Emotes { $stmt->execute(); } + /** @return \Iterator */ public function getEmoteStrings(EmoteInfo|string $infoOrId): iterable { if($infoOrId instanceof EmoteInfo) $infoOrId = $infoOrId->id; diff --git a/src/Emoticons/EmotesRpcHandler.php b/src/Emoticons/EmotesRpcHandler.php index 5391183..5f0576a 100644 --- a/src/Emoticons/EmotesRpcHandler.php +++ b/src/Emoticons/EmotesRpcHandler.php @@ -11,6 +11,7 @@ final class EmotesRpcHandler implements RpcHandler { private Emotes $emotes ) {} + /** @return array */ #[RpcQuery('misuzu:emotes:all')] public function queryAll(bool $includeId = false, bool $includeOrder = false): array { return XArray::select( diff --git a/src/Forum/ForumCategories.php b/src/Forum/ForumCategories.php index 7948c00..6fe94e9 100644 --- a/src/Forum/ForumCategories.php +++ b/src/Forum/ForumCategories.php @@ -16,6 +16,10 @@ class ForumCategories { $this->cache = new DbStatementCache($dbConn); } + /** + * @param \Iterator|ForumCategoryInfo[] $catInfos + * @return object{info: ForumCategoryInfo, colour: Colour, children: mixed[], childIds: string[]}[] + */ public static function convertCategoryListToTree( iterable $catInfos, ForumCategoryInfo|string|null $parentInfo = null, @@ -96,6 +100,7 @@ class ForumCategories { return $result->next() ? $result->getInteger(0) : 0; } + /** @return \Iterator|object{info: ForumCategoryInfo, colour: Colour, children: mixed[], childIds: string[]}[] */ public function getCategories( ForumCategoryInfo|string|null|false $parentInfo = false, string|int|null $type = null, @@ -254,6 +259,7 @@ class ForumCategories { $stmt->execute(); } + /** @return \Iterator */ public function getCategoryAncestry( ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo ): iterable { @@ -275,6 +281,7 @@ class ForumCategories { return $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...)); } + /** @return \Iterator|object{info: ForumCategoryInfo, colour: Colour, children: mixed[], childIds: string[]}[] */ public function getCategoryChildren( ForumCategoryInfo|string $parentInfo, bool $includeSelf = false, @@ -316,6 +323,7 @@ class ForumCategories { return $cats; } + /** @param ForumCategoryInfo|string|array $categoryInfos */ public function checkCategoryUnread( ForumCategoryInfo|string|array $categoryInfos, UserInfo|string|null $userInfo @@ -388,6 +396,10 @@ class ForumCategories { return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none(); } + /** + * @param array $exceptCategoryInfos + * @param array $exceptTopicInfos + */ public function getMostActiveCategoryInfo( UserInfo|string $userInfo, array $exceptCategoryInfos = [], diff --git a/src/Forum/ForumContext.php b/src/Forum/ForumContext.php index 6e7d180..343a0fd 100644 --- a/src/Forum/ForumContext.php +++ b/src/Forum/ForumContext.php @@ -11,7 +11,10 @@ class ForumContext { public private(set) ForumTopicRedirects $topicRedirects; public private(set) ForumPosts $posts; + /** @var array */ private array $totalUserTopics = []; + + /** @var array */ private array $totalUserPosts = []; public function __construct(DbConnection $dbConn) { diff --git a/src/Forum/ForumPosts.php b/src/Forum/ForumPosts.php index 09e5010..77dc7c1 100644 --- a/src/Forum/ForumPosts.php +++ b/src/Forum/ForumPosts.php @@ -71,6 +71,11 @@ class ForumPosts { return $result->next() ? $result->getInteger(0) : 0; } + /** + * @param ForumCategoryInfo|string|null|array $categoryInfo + * @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery + * @return \Iterator|ForumPostInfo[] + */ public function getPosts( ForumCategoryInfo|string|array|null $categoryInfo = null, ForumTopicInfo|string|null $topicInfo = null, @@ -189,6 +194,7 @@ class ForumPosts { return $stmt->getResult()->getIterator(ForumPostInfo::fromResult(...)); } + /** @param ForumCategoryInfo|string|null|array $categoryInfos */ public function getPost( ?string $postId = null, ForumTopicInfo|string|null $topicInfo = null, @@ -390,6 +396,11 @@ class ForumPosts { return CarbonImmutable::createFromTimestampUTC($this->getUserLastPostCreatedTime($userInfo)); } + /** + * @param array $exceptCategoryInfos + * @param array $exceptTopicInfos + * @return array + */ public function generatePostRankings( int $year = 0, int $month = 0, diff --git a/src/Forum/ForumTopicRedirects.php b/src/Forum/ForumTopicRedirects.php index 1e1efea..4760813 100644 --- a/src/Forum/ForumTopicRedirects.php +++ b/src/Forum/ForumTopicRedirects.php @@ -34,6 +34,7 @@ class ForumTopicRedirects { return $result->next() ? $result->getInteger(0) : 0; } + /** @return \Iterator */ public function getTopicRedirects( UserInfo|string|null $userInfo = null, ?Pagination $pagination = null diff --git a/src/Forum/ForumTopics.php b/src/Forum/ForumTopics.php index cd300aa..2aaf90e 100644 --- a/src/Forum/ForumTopics.php +++ b/src/Forum/ForumTopics.php @@ -17,6 +17,7 @@ class ForumTopics { $this->cache = new DbStatementCache($dbConn); } + /** @param ForumCategoryInfo|string|null|array $categoryInfo */ public function countTopics( ForumCategoryInfo|string|array|null $categoryInfo = null, UserInfo|string|null $userInfo = null, @@ -82,6 +83,11 @@ class ForumTopics { return $result->next() ? $result->getInteger(0) : 0; } + /** + * @param ForumCategoryInfo|string|null|array $categoryInfo + * @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery + * @return \Iterator|ForumTopicInfo[] + */ public function getTopics( ForumCategoryInfo|string|array|null $categoryInfo = null, UserInfo|string|null $userInfo = null, @@ -418,6 +424,11 @@ class ForumTopics { return $result->getInteger(0) < $topicInfo->bumpedTime; } + /** + * @param array $exceptCategoryInfos + * @param array $exceptTopicInfos + * @return object{success: false}|object{success: true, topicId: string, categoryId: string, postCount: int} + */ public function getMostActiveTopicInfo( UserInfo|string $userInfo, array $exceptCategoryInfos = [], diff --git a/src/Hanyuu/HanyuuRpcHandler.php b/src/Hanyuu/HanyuuRpcHandler.php index b8609db..7942bc5 100644 --- a/src/Hanyuu/HanyuuRpcHandler.php +++ b/src/Hanyuu/HanyuuRpcHandler.php @@ -13,6 +13,7 @@ use Index\Urls\UrlRegistry; final class HanyuuRpcHandler implements RpcHandler { use RpcHandlerCommon; + /** @param callable(): string $getBaseUrl */ public function __construct( private $getBaseUrl, private Config $impersonateConfig, @@ -21,6 +22,10 @@ final class HanyuuRpcHandler implements RpcHandler { private AuthContext $authCtx ) {} + /** + * @param array $attrs + * @return array{name: string, attrs: array} + */ private static function createPayload(string $name, array $attrs = []): array { $payload = ['name' => $name, 'attrs' => []]; foreach($attrs as $name => $value) { @@ -32,6 +37,7 @@ final class HanyuuRpcHandler implements RpcHandler { return $payload; } + /** @return array{name: string, attrs: array{code: string, text?: string}} */ private static function createErrorPayload(string $code, ?string $text = null): array { $attrs = ['code' => $code]; if($text !== null && $text !== '') @@ -51,8 +57,9 @@ final class HanyuuRpcHandler implements RpcHandler { ); } + /** @return array{name: string, attrs: array} */ #[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') return self::createErrorPayload('auth:check:method', 'Requested auth method is not supported.'); diff --git a/src/Home/HomeRoutes.php b/src/Home/HomeRoutes.php index 44ac460..ffe124e 100644 --- a/src/Home/HomeRoutes.php +++ b/src/Home/HomeRoutes.php @@ -4,7 +4,9 @@ namespace Misuzu\Home; use RuntimeException; use Index\XDateTime; use Index\Config\Config; +use Index\Colour\Colour; use Index\Db\{DbConnection,DbTools}; +use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait}; use Misuzu\{Pagination,SiteInfo,Template}; @@ -12,8 +14,8 @@ use Misuzu\Auth\AuthInfo; use Misuzu\Changelog\Changelog; use Misuzu\Comments\Comments; use Misuzu\Counters\Counters; -use Misuzu\News\News; -use Misuzu\Users\UsersContext; +use Misuzu\News\{News,NewsCategoryInfo,NewsPostInfo}; +use Misuzu\Users\{UsersContext,UserInfo}; class HomeRoutes implements RouteHandler, UrlSource { use RouteHandlerTrait, UrlSourceTrait; @@ -30,6 +32,16 @@ class HomeRoutes implements RouteHandler, UrlSource { 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 { return $this->counters->get([ 'users:active', @@ -41,6 +53,7 @@ class HomeRoutes implements RouteHandler, UrlSource { ]); } + /** @return \Iterator */ private function getOnlineUsers(): iterable { return $this->usersCtx->users->getUsers( lastActiveInMinutes: 5, @@ -49,8 +62,18 @@ class HomeRoutes implements RouteHandler, UrlSource { ); } + /** @var array */ 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 { $postInfos = $this->news->getPosts( onlyFeatured: true, @@ -86,6 +109,17 @@ class HomeRoutes implements RouteHandler, UrlSource { 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 { $args = 0; $stmt = $this->dbConn->prepare( @@ -118,6 +152,18 @@ class HomeRoutes implements RouteHandler, UrlSource { 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 { $args = 0; $stmt = $this->dbConn->prepare( @@ -154,13 +200,13 @@ class HomeRoutes implements RouteHandler, UrlSource { #[HttpGet('/')] #[UrlFormat('index', '/')] - public function getIndex(...$args) { + public function getIndex(HttpResponseBuilder $response, HttpRequest $request): string { return $this->authInfo->isLoggedIn - ? $this->getHome(...$args) - : $this->getLanding(...$args); + ? $this->getHome() + : $this->getLanding($response, $request); } - public function getHome() { + public function getHome(): string { $stats = $this->getStats(); $onlineUserInfos = iterator_to_array($this->getOnlineUsers()); $featuredNews = $this->getFeaturedNewsPosts(5, true); @@ -200,7 +246,7 @@ class HomeRoutes implements RouteHandler, UrlSource { } #[HttpGet('/_landing')] - public function getLanding($response, $request) { + public function getLanding(HttpResponseBuilder $response, HttpRequest $request): string { $config = $this->config->getValues([ ['social.embed_linked:b'], ['landing.forum_categories:a'], diff --git a/src/Imaging/GdImage.php b/src/Imaging/GdImage.php index 90b7314..6b50452 100644 --- a/src/Imaging/GdImage.php +++ b/src/Imaging/GdImage.php @@ -4,7 +4,8 @@ namespace Misuzu\Imaging; use InvalidArgumentException; final class GdImage extends Image { - private $gd; + private \GdImage $gd; + private int $type; private const CONSTRUCTORS = [ @@ -25,11 +26,10 @@ final class GdImage extends Image { IMAGETYPE_WEBP => 'imagewebp', ]; - public function __construct($pathOrWidth, int $height = -1) { - parent::__construct($pathOrWidth); - + public function __construct(int|string $pathOrWidth, int $height = -1) { + $handle = false; if(is_int($pathOrWidth)) { - $this->gd = imagecreatetruecolor($pathOrWidth, $height < 1 ? $pathOrWidth : $height); + $handle = imagecreatetruecolor($pathOrWidth, $height < 1 ? $pathOrWidth : $height); $this->type = IMAGETYPE_PNG; } elseif(is_string($pathOrWidth)) { $imageInfo = getimagesize($pathOrWidth); @@ -38,17 +38,18 @@ final class GdImage extends Image { $this->type = $imageInfo[2]; 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.'); + + $this->gd = $handle; } public function __destruct() { - if(isset($this->gd)) - $this->destroy(); + $this->destroy(); } public function getWidth(): int { @@ -103,9 +104,7 @@ final class GdImage extends Image { } public function destroy(): void { - if(imagedestroy($this->gd)) { - $this->gd = null; + if(imagedestroy($this->gd)) $this->type = 0; - } } } diff --git a/src/Imaging/Image.php b/src/Imaging/Image.php index 70abaea..4bc5037 100644 --- a/src/Imaging/Image.php +++ b/src/Imaging/Image.php @@ -5,12 +5,7 @@ use InvalidArgumentException; use UnexpectedValueException; abstract class Image { - public function __construct($pathOrWidth) { - 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 { + public static function create(int|string $pathOrWidth, int $height = -1): Image { if(extension_loaded('imagick')) return new ImagickImage($pathOrWidth, $height); if(extension_loaded('gd')) diff --git a/src/Imaging/ImagickImage.php b/src/Imaging/ImagickImage.php index 4fd5950..eb805a0 100644 --- a/src/Imaging/ImagickImage.php +++ b/src/Imaging/ImagickImage.php @@ -7,9 +7,7 @@ use InvalidArgumentException; final class ImagickImage extends Image { private ?Imagick $imagick = null; - public function __construct($pathOrWidth, int $height = -1) { - parent::__construct($pathOrWidth); - + public function __construct(int|string $pathOrWidth, int $height = -1) { if(is_int($pathOrWidth)) { $this->imagick = new Imagick(); $this->imagick->newImage($pathOrWidth, $height < 1 ? $pathOrWidth : $height, 'none'); diff --git a/src/Info/InfoRoutes.php b/src/Info/InfoRoutes.php index c957487..5dbf784 100644 --- a/src/Info/InfoRoutes.php +++ b/src/Info/InfoRoutes.php @@ -1,6 +1,7 @@ ')] #[UrlFormat('info-doc', '/info/')] - public function getDocsPage($response, $request, string $name) { + public function getDocsPage(HttpResponseBuilder $response, HttpRequest $request, string $name): string { return $this->serveMarkdownDocument( 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_]+)')] #[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)) return 404; @@ -99,7 +100,7 @@ class InfoRoutes implements RouteHandler, UrlSource { 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)) return 404; diff --git a/src/LegacyRoutes.php b/src/LegacyRoutes.php index fc80267..6caf549 100644 --- a/src/LegacyRoutes.php +++ b/src/LegacyRoutes.php @@ -1,6 +1,7 @@ <?php namespace Misuzu; +use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource}; @@ -126,27 +127,27 @@ class LegacyRoutes implements RouteHandler, UrlSource { } #[HttpGet('/index.php')] - public function getIndexPHP($response): void { + public function getIndexPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('index'), true); } #[HttpGet('/info.php')] - public function getInfoPHP($response): void { + public function getInfoPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('info'), true); } #[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); } #[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); } #[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)); if($postId > 0) @@ -161,14 +162,14 @@ class LegacyRoutes implements RouteHandler, UrlSource { } #[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', [ 'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT), ]), true); } #[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', [ 'category' => $request->getParam('c', 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')] - public function getNewsPostPHP($response, $request): void { + public function getNewsPostPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('news-post', [ 'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), ]), true); @@ -187,7 +188,7 @@ class LegacyRoutes implements RouteHandler, UrlSource { #[HttpGet('/news/feed.php')] #[HttpGet('/news/feed.php/rss')] #[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); $response->redirect($this->urls->format( $catId > 0 ? 'news-category-feed' : 'news-feed', @@ -196,7 +197,7 @@ class LegacyRoutes implements RouteHandler, UrlSource { } #[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); if($changeId) { $response->redirect($this->urls->format('changelog-change', ['change' => $changeId]), true); @@ -210,7 +211,7 @@ class LegacyRoutes implements RouteHandler, UrlSource { } #[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')) { 'logout' => 'auth-logout', 'reset' => 'auth-reset', @@ -222,43 +223,43 @@ class LegacyRoutes implements RouteHandler, UrlSource { #[HttpGet('/auth')] #[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); } #[HttpGet('/settings.php')] - public function getSettingsPHP($response): void { + public function getSettingsPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('settings-index'), true); } #[HttpGet('/settings')] - public function getSettingsIndex($response): void { + public function getSettingsIndex(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('settings-account')); } #[HttpGet('/settings/index.php')] - public function getSettingsIndexPHP($response): void { + public function getSettingsIndexPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('settings-account'), true); } #[HttpGet('/manage')] #[UrlFormat('manage-index', '/manage')] - public function getManageIndex($response): void { + public function getManageIndex(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('manage-general-overview')); } #[HttpGet('/manage/index.php')] - public function getManageIndexPHP($response): void { + public function getManageIndexPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('manage-general-overview'), true); } #[HttpGet('/manage/news')] - public function getManageNewsIndex($response): void { + public function getManageNewsIndex(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('manage-news-categories')); } #[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); } } diff --git a/src/Mailer.php b/src/Mailer.php index 2c11a18..f4f8900 100644 --- a/src/Mailer.php +++ b/src/Mailer.php @@ -6,15 +6,17 @@ use Index\Config\Config; use Symfony\Component\Mime\Email as SymfonyMessage; use Symfony\Component\Mime\Address as SymfonyAddress; use Symfony\Component\Mailer\Transport as SymfonyTransport; +use Symfony\Component\Mailer\Transport\TransportInterface as SymfonyTransportInterface; final class Mailer { private const TEMPLATE_PATH = MSZ_ROOT . '/config/emails/%s.txt'; private static Config $config; - private static $transport = null; + private static ?SymfonyTransportInterface $transport; public static function init(Config $config): void { self::$config = $config; + self::$transport = null; } private static function createDsn(): string { @@ -47,15 +49,15 @@ final class Mailer { return $dsn; } - public static function getTransport() { - if(self::$transport === null) - self::$transport = SymfonyTransport::fromDsn(self::createDsn()); + public static function getTransport(): SymfonyTransportInterface { + self::$transport ??= SymfonyTransport::fromDsn(self::createDsn()); return self::$transport; } + /** @param array<int|string, mixed> $to */ public static function sendMessage(array $to, string $subject, string $contents, bool $bcc = false): bool { foreach($to as $email => $name) { - $to = new SymfonyAddress($email, $name); + $to = new SymfonyAddress((string)$email, (string)$name); break; } @@ -83,6 +85,10 @@ final class Mailer { return true; } + /** + * @param array<string, scalar> $vars + * @return array{subject: string, message: string} + */ public static function template(string $name, array $vars = []): array { $path = sprintf(self::TEMPLATE_PATH, $name); diff --git a/src/Messages/MessagesDatabase.php b/src/Messages/MessagesDatabase.php index 074103d..994b023 100644 --- a/src/Messages/MessagesDatabase.php +++ b/src/Messages/MessagesDatabase.php @@ -82,6 +82,7 @@ class MessagesDatabase { return $result->next() ? $result->getInteger(0) : 0; } + /** @return \Iterator<int, MessageInfo> */ public function getMessages( UserInfo|string|null $ownerInfo = null, UserInfo|string|null $authorInfo = null, @@ -277,6 +278,7 @@ class MessagesDatabase { $stmt->execute(); } + /** @param MessageInfo|string|null|array<MessageInfo|string> $messageInfos */ public function deleteMessages( UserInfo|string|null $ownerInfo, MessageInfo|array|string|null $messageInfos @@ -315,6 +317,7 @@ class MessagesDatabase { $stmt->execute(); } + /** @param MessageInfo|string|null|array<MessageInfo|string> $messageInfos */ public function restoreMessages( UserInfo|string|null $ownerInfo, MessageInfo|array|string|null $messageInfos @@ -353,6 +356,7 @@ class MessagesDatabase { $stmt->execute(); } + /** @param MessageInfo|string|null|array<MessageInfo|string> $messageInfos */ public function nukeMessages( UserInfo|string|null $ownerInfo, MessageInfo|array|string|null $messageInfos diff --git a/src/Messages/MessagesRoutes.php b/src/Messages/MessagesRoutes.php index 7a55bb6..e90630b 100644 --- a/src/Messages/MessagesRoutes.php +++ b/src/Messages/MessagesRoutes.php @@ -6,6 +6,8 @@ use InvalidArgumentException; use RuntimeException; use Index\XString; 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\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; use Misuzu\{CSRF,Pagination,Perm,Template}; @@ -35,8 +37,9 @@ class MessagesRoutes implements RouteHandler, UrlSource { private bool $canSendMessages; + /** @return void|int|array{error: array{name: string, text: string}} */ #[HttpMiddleware('/messages')] - public function checkAccess($response, $request) { + public function checkAccess(HttpResponseBuilder $response, HttpRequest $request) { // should probably be a permission or something too if(!$this->authInfo->isLoggedIn) return 401; @@ -52,8 +55,11 @@ class MessagesRoutes implements RouteHandler, UrlSource { $this->canSendMessages = $globalPerms->check(Perm::G_MESSAGES_SEND) && !$this->usersCtx->hasActiveBan($this->authInfo->userInfo); - if($request->getMethod() === 'POST' && $request->isFormContent()) { + if($request->getMethod() === 'POST') { $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) + return 400; + if(!$content->hasParam('_csrfp') || !CSRF::validate((string)$content->getParam('_csrfp'))) return [ '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 { $message = new stdClass; @@ -80,7 +95,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { #[HttpGet('/messages')] #[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'); if($folderName === '') $folderName = 'inbox'; @@ -131,9 +146,10 @@ class MessagesRoutes implements RouteHandler, UrlSource { ]); } + /** @return array{unread: int} */ #[HttpGet('/messages/stats')] #[UrlFormat('messages-stats', '/messages/stats')] - public function getStats() { + public function getStats(): array { $selfInfo = $this->authInfo->userInfo; $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')] #[UrlFormat('messages-recipient', '/messages/recipient')] - public function postRecipient($response, $request) { - if(!$request->isFormContent()) - return 400; + public function postRecipient(HttpResponseBuilder $response, HttpRequest $request): int|array { if(!$this->canSendMessages) return 403; $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) + return 400; + $name = trim((string)$content->getParam('name')); // flappy hacks @@ -190,7 +215,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { #[HttpGet('/messages/compose')] #[UrlFormat('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])] - public function getEditor($response, $request) { + public function getEditor(HttpResponseBuilder $response, HttpRequest $request): int|string { if(!$this->canSendMessages) return 403; @@ -201,7 +226,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { #[HttpGet('/messages/([A-Za-z0-9]+)')] #[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) 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 { $globalPerms = $this->perms->getPermissions('global', $userInfo); if(!$globalPerms->check(Perm::G_MESSAGES_VIEW)) @@ -270,6 +296,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { return null; } + /** @return ?array{error: array{name: string, text: string, args?: scalar[]}} */ private function checkMessageFields(string $title, string $body, int $parser): ?array { if(!Parser::isValid($parser)) return [ @@ -329,15 +356,17 @@ class MessagesRoutes implements RouteHandler, UrlSource { return null; } + /** @return int|array{error: array{name: string, text: string}}|array{id: string, url: string} */ #[HttpPost('/messages/create')] #[UrlFormat('messages-create', '/messages/create')] - public function postCreate($response, $request) { - if(!$request->isFormContent()) - return 400; + public function postCreate(HttpResponseBuilder $response, HttpRequest $request): int|array { if(!$this->canSendMessages) return 403; $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) + return 400; + $recipient = (string)$content->getParam('recipient'); $replyTo = (string)$content->getParam('reply'); $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]+)')] #[UrlFormat('messages-update', '/messages/<message>')] - public function postUpdate($response, $request, string $messageId) { - if(!$request->isFormContent()) - return 400; + public function postUpdate(HttpResponseBuilder $response, HttpRequest $request, string $messageId): int|array { if(!$this->canSendMessages) return 403; $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) + return 400; + $title = (string)$content->getParam('title'); $body = (string)$content->getParam('body'); $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')] #[UrlFormat('messages-mark', '/messages/mark')] - public function postMark($response, $request) { - if(!$request->isFormContent()) + public function postMark(HttpResponseBuilder $response, HttpRequest $request): int|array { + $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) return 400; - $content = $request->getContent(); $type = (string)$content->getParam('type'); $messages = explode(',', (string)$content->getParam('messages')); @@ -554,13 +586,13 @@ class MessagesRoutes implements RouteHandler, UrlSource { return []; } + /** @return int|array{error: array{name: string, text: string}}|scalar[] */ #[HttpPost('/messages/delete')] #[UrlFormat('messages-delete', '/messages/delete')] - public function postDelete($response, $request) { - if(!$request->isFormContent()) - return 400; - + public function postDelete(HttpResponseBuilder $response, HttpRequest $request): int|array { $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) + return 400; $messages = (string)$content->getParam('messages'); if($messages === '') @@ -581,13 +613,13 @@ class MessagesRoutes implements RouteHandler, UrlSource { return []; } + /** @return int|array{error: array{name: string, text: string}}|scalar[] */ #[HttpPost('/messages/restore')] #[UrlFormat('messages-restore', '/messages/restore')] - public function postRestore($response, $request) { - if(!$request->isFormContent()) - return 400; - + public function postRestore(HttpResponseBuilder $response, HttpRequest $request) { $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) + return 400; $messages = (string)$content->getParam('messages'); if($messages === '') @@ -608,13 +640,13 @@ class MessagesRoutes implements RouteHandler, UrlSource { return []; } + /** @return int|array{error: array{name: string, text: string}}|scalar[] */ #[HttpPost('/messages/nuke')] #[UrlFormat('messages-nuke', '/messages/nuke')] - public function postNuke($response, $request) { - if(!$request->isFormContent()) - return 400; - + public function postNuke(HttpResponseBuilder $response, HttpRequest $request) { $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) + return 400; $messages = (string)$content->getParam('messages'); if($messages === '') diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index bb8e496..16bf6cf 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -88,6 +88,7 @@ class MisuzuContext { return new FsDbMigrationRepo(MSZ_MIGRATIONS); } + /** @param mixed[] $params */ public function createAuditLog(string $action, array $params = [], UserInfo|string|null $userInfo = null): void { if($userInfo === null && $this->authInfo->isLoggedIn) $userInfo = $this->authInfo->userInfo; diff --git a/src/News/News.php b/src/News/News.php index c5dfd8d..8e9b4fc 100644 --- a/src/News/News.php +++ b/src/News/News.php @@ -36,6 +36,7 @@ class News { return $count; } + /** @return \Iterator<int, NewsCategoryInfo> */ public function getCategories( ?bool $hidden = null, ?Pagination $pagination = null @@ -209,6 +210,7 @@ class News { return $count; } + /** @return \Iterator<int, NewsPostInfo> */ public function getPosts( NewsCategoryInfo|string|null $categoryInfo = null, ?string $searchQuery = null, diff --git a/src/News/NewsRoutes.php b/src/News/NewsRoutes.php index 2b2e950..2f09ce8 100644 --- a/src/News/NewsRoutes.php +++ b/src/News/NewsRoutes.php @@ -2,6 +2,8 @@ namespace Misuzu\News; use RuntimeException; +use Index\Colour\Colour; +use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Syndication\FeedBuilder; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; @@ -9,7 +11,7 @@ use Misuzu\{Pagination,SiteInfo,Template}; use Misuzu\Auth\AuthInfo; use Misuzu\Comments\{Comments,CommentsCategory,CommentsEx}; use Misuzu\Parsers\Parser; -use Misuzu\Users\UsersContext; +use Misuzu\Users\{UsersContext,UserInfo}; class NewsRoutes implements RouteHandler, UrlSource { use RouteHandlerTrait, UrlSourceTrait; @@ -23,8 +25,18 @@ class NewsRoutes implements RouteHandler, UrlSource { private Comments $comments ) {} + /** @var array<string, NewsCategoryInfo> */ 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 { $posts = []; $postInfos = $this->news->getPosts( @@ -58,6 +70,13 @@ class NewsRoutes implements RouteHandler, UrlSource { return $posts; } + /** + * @return array{ + * post: NewsPostInfo, + * category: NewsCategoryInfo, + * user: UserInfo + * }[] + */ private function getNewsPostsForFeed(?NewsCategoryInfo $categoryInfo = null): array { $posts = []; $postInfos = $this->news->getPosts( @@ -82,7 +101,7 @@ class NewsRoutes implements RouteHandler, UrlSource { #[HttpGet('/news')] #[UrlFormat('news-index', '/news', ['p' => '<page>'])] - public function getIndex() { + public function getIndex(): int|string { $categories = $this->news->getCategories(hidden: false); $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))?')] #[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 { $categoryInfo = $this->news->getCategory(categoryId: $categoryId); } catch(RuntimeException $ex) { @@ -126,7 +145,7 @@ class NewsRoutes implements RouteHandler, UrlSource { #[HttpGet('/news/post/([0-9]+)')] #[UrlFormat('news-post', '/news/post/<post>')] #[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 { $postInfo = $this->news->getPost($postId); } catch(RuntimeException $ex) { @@ -163,7 +182,7 @@ class NewsRoutes implements RouteHandler, UrlSource { #[HttpGet('/news.(?:xml|rss|atom)')] #[UrlFormat('news-feed', '/news.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'); $hasCategory = $categoryInfo !== null; diff --git a/src/Pagination.php b/src/Pagination.php index e499be6..7a1115d 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -61,11 +61,13 @@ final class Pagination { return $this; } + /** @param ?array<string, mixed> $source */ public function readPage(string $name = self::DEFAULT_PARAM, int $default = self::START_PAGE, ?array $source = null): self { $this->setPage(self::param($name, $default, $source)); 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 { $source ??= $_GET; diff --git a/src/Parsers/BBCode/BBCodeParser.php b/src/Parsers/BBCode/BBCodeParser.php index 447fb77..8c7b460 100644 --- a/src/Parsers/BBCode/BBCodeParser.php +++ b/src/Parsers/BBCode/BBCodeParser.php @@ -4,8 +4,10 @@ namespace Misuzu\Parsers\BBCode; use Misuzu\Parsers\ParserInterface; class BBCodeParser implements ParserInterface { - private $tags = []; + /** @var BBCodeTag[] */ + private array $tags = []; + /** @param BBCodeTag[] $tags */ public function __construct(array $tags = []) { if(empty($tags)) { $tags = [ diff --git a/src/Parsers/MarkdownParser.php b/src/Parsers/MarkdownParser.php index 66db348..4ca4bff 100644 --- a/src/Parsers/MarkdownParser.php +++ b/src/Parsers/MarkdownParser.php @@ -12,6 +12,10 @@ class MarkdownParser extends Parsedown implements ParserInterface { return $this->line($line); } + /** + * @param mixed[] $excerpt + * @return void|mixed[] + */ protected function inlineImage($excerpt) { if(!isset($excerpt['text'][1]) || $excerpt['text'][1] !== '[') return; diff --git a/src/Parsers/Parser.php b/src/Parsers/Parser.php index f49cffb..0258685 100644 --- a/src/Parsers/Parser.php +++ b/src/Parsers/Parser.php @@ -20,6 +20,7 @@ final class Parser { self::MARKDOWN => 'Markdown', ]; + /** @var array<int, ParserInterface> */ private static $instances = []; public static function isValid(int $parser): bool { diff --git a/src/Perm.php b/src/Perm.php index 955a6ee..a5fb52c 100644 --- a/src/Perm.php +++ b/src/Perm.php @@ -384,6 +384,15 @@ final class Perm { ? self::LABELS[$category][$permission] : ''; } + /** + * @param string[] $sections + * @return object{ + * category: string, + * name: string, + * title: string, + * value: int + * }[] + */ public static function createList(array $sections): array { $list = []; @@ -423,6 +432,11 @@ final class Perm { 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 { $apply = []; diff --git a/src/Perms/IPermissionResult.php b/src/Perms/IPermissionResult.php index e6701b5..ae2e7d4 100644 --- a/src/Perms/IPermissionResult.php +++ b/src/Perms/IPermissionResult.php @@ -4,6 +4,7 @@ namespace Misuzu\Perms; interface IPermissionResult { public function getCalculated(): int; public function check(int $perm): bool; + /** @param array<string, int> $perms */ public function checkMany(array $perms): object; public function apply(callable $callable): IPermissionResult; } diff --git a/src/Perms/Permissions.php b/src/Perms/Permissions.php index b88eb2f..147471b 100644 --- a/src/Perms/Permissions.php +++ b/src/Perms/Permissions.php @@ -24,8 +24,13 @@ class Permissions { $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( UserInfo|string|null $userInfo = null, RoleInfo|string|null $roleInfo = null, @@ -111,6 +116,7 @@ class Permissions { $stmt->execute(); } + /** @param string|null|string[] $categoryNames */ public function removePermissions( array|string|null $categoryNames, UserInfo|string|null $userInfo = null, @@ -173,6 +179,7 @@ class Permissions { return $result->next() ? $result->getInteger(0) : 0; } + /** @param string|string[] $categoryNames */ public function getPermissions( string|array $categoryNames, UserInfo|string|null $userInfo = null, @@ -220,7 +227,11 @@ class Permissions { 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 { $suppliedUsers = !empty($userIds); $doGuest = !$suppliedUsers; @@ -297,6 +308,13 @@ class Permissions { 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 { $catIds[] = $currentCatId = $forumCat->info->id; 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); } + /** + * @param bool|float|int|string|null ...$args + */ private static function precalculatePermissionsLog(string $fmt, ...$args): void { if(!MSZ_CLI) return; diff --git a/src/Profile/ProfileFields.php b/src/Profile/ProfileFields.php index 6fdf8de..f2a79fb 100644 --- a/src/Profile/ProfileFields.php +++ b/src/Profile/ProfileFields.php @@ -13,6 +13,10 @@ class ProfileFields { $this->cache = new DbStatementCache($dbConn); } + /** + * @param ?ProfileFieldValueInfo[] $fieldValueInfos + * @return \Iterator<int, ProfileFieldInfo>|ProfileFieldInfo[] + */ public function getFields(?iterable $fieldValueInfos = null): iterable { $hasFieldValueInfos = $fieldValueInfos !== null; if($hasFieldValueInfos) { @@ -55,6 +59,11 @@ class ProfileFields { return ProfileFieldInfo::fromResult($result); } + /** + * @param ?ProfileFieldInfo[] $fieldInfos + * @param ?ProfileFieldValueInfo[] $fieldValueInfos + * @return \Iterator<int, ProfileFieldFormatInfo>|ProfileFieldFormatInfo[] + */ public function getFieldFormats( ?iterable $fieldInfos = null, ?iterable $fieldValueInfos = null @@ -143,6 +152,7 @@ class ProfileFields { return ProfileFieldFormatInfo::fromResult($result); } + /** @return \Iterator<int, ProfileFieldValueInfo> */ public function getFieldValues(UserInfo|string $userInfo): iterable { if($userInfo instanceof UserInfo) $userInfo = $userInfo->id; @@ -178,6 +188,10 @@ class ProfileFields { return ProfileFieldValueInfo::fromResult($result); } + /** + * @param ProfileFieldInfo|string|array<string|ProfileFieldInfo> $fieldInfos + * @param string|string[] $values + */ public function setFieldValues( UserInfo|string $userInfo, ProfileFieldInfo|string|array $fieldInfos, @@ -236,6 +250,9 @@ class ProfileFields { $stmt->execute(); } + /** + * @param ProfileFieldInfo|string|array<string|ProfileFieldInfo> $fieldInfos + */ public function removeFieldValues( UserInfo|string $userInfo, ProfileFieldInfo|string|array $fieldInfos @@ -251,7 +268,7 @@ class ProfileFields { foreach($fieldInfos as $key => $value) { if($value instanceof ProfileFieldInfo) $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'); } diff --git a/src/RoutingContext.php b/src/RoutingContext.php index 2f9049e..dab9052 100644 --- a/src/RoutingContext.php +++ b/src/RoutingContext.php @@ -1,6 +1,7 @@ <?php namespace Misuzu; +use Index\Http\HttpRequest; use Index\Http\Routing\{HttpRouter,Router,RouteHandler}; use Index\Urls\{ArrayUrlRegistry,UrlFormat,UrlRegistry,UrlSource}; @@ -21,7 +22,8 @@ class RoutingContext { $this->urls->register($handler); } - public function dispatch(...$args): void { - $this->router->dispatch(...$args); + /** @param mixed[] $args */ + public function dispatch(?HttpRequest $request = null, array $args = []): void { + $this->router->dispatch($request, $args); } } diff --git a/src/Satori/SatoriRoutes.php b/src/Satori/SatoriRoutes.php index f649faf..5f9e8f5 100644 --- a/src/Satori/SatoriRoutes.php +++ b/src/Satori/SatoriRoutes.php @@ -4,6 +4,7 @@ namespace Misuzu\Satori; use RuntimeException; use Index\Colour\Colour; use Index\Config\Config; +use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler,RouteHandlerTrait}; use Misuzu\Pagination; use Misuzu\RoutingContext; @@ -21,8 +22,9 @@ final class SatoriRoutes implements RouteHandler { private ProfileFields $profileFields ) {} + /** @return void|int */ #[HttpMiddleware('/_satori')] - public function verifyRequest($response, $request) { + public function verifyRequest(HttpResponseBuilder $response, HttpRequest $request) { $secretKey = $this->config->getString('secret'); 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')] - public function getProfileField($response, $request): array { + public function getProfileField(HttpResponseBuilder $response, HttpRequest $request): array { $userId = (string)$request->getParam('user', 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')] - public function getRecentForumPosts($response, $request): array { + public function getRecentForumPosts(HttpResponseBuilder $response, HttpRequest $request): array { $categoryIds = $this->config->getArray('forum.categories'); if(empty($categoryIds)) return []; @@ -99,8 +115,14 @@ final class SatoriRoutes implements RouteHandler { return $posts; } + /** + * @return array{ + * user_id: int, + * username: string + * }[] + */ #[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); $backlogDays = $this->config->getInteger('users.backlog', 7); $startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); diff --git a/src/SharpChat/SharpChatRoutes.php b/src/SharpChat/SharpChatRoutes.php index b27808f..25bd44c 100644 --- a/src/SharpChat/SharpChatRoutes.php +++ b/src/SharpChat/SharpChatRoutes.php @@ -10,6 +10,7 @@ use Misuzu\Perms\Permissions; use Misuzu\Users\{Bans,UsersContext,UserInfo}; use Index\Colour\Colour; use Index\Config\Config; +use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerTrait}; use Index\Urls\UrlRegistry; @@ -32,9 +33,10 @@ final class SharpChatRoutes implements RouteHandler { $this->hashKey = $this->config->getString('hashKey', 'woomy'); } + /** @return int|array{Text: string[], Image: string, Hierarchy: int}[] */ #[HttpOptions('/_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-Methods', 'GET'); $response->setHeader('Access-Control-Allow-Headers', 'Cache-Control'); @@ -64,7 +66,7 @@ final class SharpChatRoutes implements RouteHandler { } #[HttpGet('/_sockchat/login')] - public function getLogin($response, $request) { + public function getLogin(HttpResponseBuilder $response, HttpRequest $request): void { if(!$this->authInfo->isLoggedIn) { $response->redirect($this->urls->format('auth-login')); return; @@ -84,9 +86,10 @@ final class SharpChatRoutes implements RouteHandler { return in_array($targetId, $whitelist, true); } + /** @return int|array{ok: false, err: string}|array{ok: true, usr: int, tkn: string} */ #[HttpOptions('/_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') : ''; $origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : ''; $originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? ''); @@ -139,24 +142,25 @@ final class SharpChatRoutes implements RouteHandler { ]; } + /** @return int|void */ #[HttpPost('/_sockchat/bump')] - public function postBump($response, $request) { + public function postBump(HttpResponseBuilder $response, HttpRequest $request) { if(!$request->hasHeader('X-SharpChat-Signature')) 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); - if(!is_array($bumpList)) - return 400; + $bumpList = $content->getParam('u', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); + if(!is_array($bumpList)) + return 400; - $userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT); - $signature = "bump#{$userTime}"; + $userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT); + $signature = "bump#{$userTime}"; - foreach($bumpList as $userId => $ipAddr) - $signature .= "#{$userId}:{$ipAddr}"; - } else return 400; + foreach($bumpList as $userId => $ipAddr) + $signature .= "#{$userId}:{$ipAddr}"; $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); $realHash = hash_hmac('sha256', $signature, $this->hashKey); @@ -169,19 +173,31 @@ final class SharpChatRoutes implements RouteHandler { $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')] - public function postVerify($response, $request) { + public function postVerify(HttpResponseBuilder $response, HttpRequest $request): int|array { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; - if($request->isFormContent()) { - $content = $request->getContent(); - $authMethod = (string)$content->getParam('method'); - $authToken = (string)$content->getParam('token'); - $ipAddress = (string)$content->getParam('ipaddr'); - } else + $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) 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'); if(strlen($userHash) !== 64) 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')] - public function getBanList($response, $request) { + public function getBanList(HttpResponseBuilder $response, HttpRequest $request): int|array { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; @@ -328,8 +355,17 @@ final class SharpChatRoutes implements RouteHandler { 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')] - public function getBanCheck($response, $request) { + public function getBanCheck(HttpResponseBuilder $response, HttpRequest $request): int|array { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; @@ -365,12 +401,15 @@ final class SharpChatRoutes implements RouteHandler { } #[HttpPost('/_sockchat/bans/create')] - public function postBanCreate($response, $request): int { - if(!$request->hasHeader('X-SharpChat-Signature') || !$request->isFormContent()) + public function postBanCreate(HttpResponseBuilder $response, HttpRequest $request): int { + if(!$request->hasHeader('X-SharpChat-Signature')) + return 400; + + $content = $request->getContent(); + if(!($content instanceof FormHttpContent)) return 400; $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); - $content = $request->getContent(); $userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT); $userId = (string)$content->getParam('ui', FILTER_SANITIZE_NUMBER_INT); $userAddr = (string)$content->getParam('ua'); @@ -439,7 +478,7 @@ final class SharpChatRoutes implements RouteHandler { } #[HttpDelete('/_sockchat/bans/revoke')] - public function deleteBanRevoke($response, $request): int { + public function deleteBanRevoke(HttpResponseBuilder $response, HttpRequest $request): int { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; diff --git a/src/TOTPGenerator.php b/src/TOTPGenerator.php index 2e2f8fd..a5459fb 100644 --- a/src/TOTPGenerator.php +++ b/src/TOTPGenerator.php @@ -35,6 +35,7 @@ class TOTPGenerator { return str_pad((string)$otp, self::DIGITS, '0', STR_PAD_LEFT); } + /** @return string[] */ public function generateRange(int $range = 1, ?int $timecode = null): array { if($range < 1) throw new InvalidArgumentException('$range must be greater than 0.'); diff --git a/src/Template.php b/src/Template.php index 0b26d3b..7ab430c 100644 --- a/src/Template.php +++ b/src/Template.php @@ -8,6 +8,8 @@ final class Template { private const FILE_EXT = '.twig'; private static TplEnvironment $env; + + /** @var array<string, mixed> */ private static array $vars = []; public static function init(TplEnvironment $env): void { @@ -18,6 +20,7 @@ final class Template { self::$env->addFunction($name, $body); } + /** @param array<string, mixed> $vars */ public static function renderRaw(string $file, array $vars = []): string { if(!defined('MSZ_TPL_RENDER')) define('MSZ_TPL_RENDER', microtime(true)); @@ -28,10 +31,15 @@ final class Template { return self::$env->render($file, array_merge(self::$vars, $vars)); } + /** @param array<string, mixed> $vars */ public static function render(string $file, array $vars = []): void { echo self::renderRaw($file, $vars); } + /** + * @param string|array<string|int, mixed> $arrayOrKey + * @param mixed $value + */ public static function set($arrayOrKey, $value = null): void { if(is_string($arrayOrKey)) self::$vars[$arrayOrKey] = $value; diff --git a/src/TemplatingExtension.php b/src/TemplatingExtension.php index d15fd8f..793f3e3 100644 --- a/src/TemplatingExtension.php +++ b/src/TemplatingExtension.php @@ -61,6 +61,16 @@ final class TemplatingExtension extends AbstractExtension { return $dateTime->diffForHumans(); } + /** + * @return array{ + * title: string, + * url: string, + * menu?: array{ + * title: string, + * url: string + * }[] + * }[] + */ public function getHeaderMenu(): array { $menu = []; @@ -120,6 +130,13 @@ final class TemplatingExtension extends AbstractExtension { return $menu; } + /** + * @return array{ + * title: string, + * url: string, + * icon: string + * }[] + */ public function getUserMenu(bool $inBroomCloset, string $manageUrl = ''): array { $menu = []; @@ -188,6 +205,7 @@ final class TemplatingExtension extends AbstractExtension { return $menu; } + /** @return array<string, array<string, string>> */ public function getManageMenu(): array { $globalPerms = $this->ctx->authInfo->getPerms('global'); if(!$this->ctx->authInfo->isLoggedIn || !$globalPerms->check(Perm::G_IS_JANITOR)) diff --git a/src/Users/Assets/AssetsRoutes.php b/src/Users/Assets/AssetsRoutes.php index b9944fd..4a9e221 100644 --- a/src/Users/Assets/AssetsRoutes.php +++ b/src/Users/Assets/AssetsRoutes.php @@ -3,6 +3,7 @@ namespace Misuzu\Users\Assets; use InvalidArgumentException; use RuntimeException; +use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait}; use Misuzu\Perm; @@ -18,7 +19,7 @@ class AssetsRoutes implements RouteHandler, UrlSource { private UsersContext $usersCtx ) {} - private function canViewAsset($request, UserInfo $assetUser): bool { + private function canViewAsset(HttpRequest $request, UserInfo $assetUser): bool { if($this->usersCtx->bans->countActiveBans($assetUser)) // allow staff viewing profile to still see banned user assets // 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 void */ #[HttpGet('/assets/avatar')] #[HttpGet('/assets/avatar/([0-9]+)(?:\.[a-z]+)?')] #[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); try { @@ -50,10 +52,11 @@ class AssetsRoutes implements RouteHandler, UrlSource { $this->serveAsset($response, $request, $assetInfo); } + /** @return int|void */ #[HttpGet('/assets/profile-background')] #[HttpGet('/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')] #[UrlFormat('user-background', '/assets/profile-background/<user>')] - public function getProfileBackground($response, $request, string $userId = '') { + public function getProfileBackground(HttpResponseBuilder $response, HttpRequest $request, string $userId = '') { try { $userInfo = $this->usersCtx->getUserInfo($userId); } catch(RuntimeException $ex) { @@ -74,13 +77,16 @@ class AssetsRoutes implements RouteHandler, UrlSource { $this->serveAsset($response, $request, $assetInfo); } + /** @return int|void */ #[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); $mode = (string)$request->getParam('m'); - if($mode === 'avatar') - return $this->getAvatar($response, $request, $userId); + if($mode === 'avatar') { + $this->getAvatar($response, $request, $userId); + return; + } if($mode === 'background') return $this->getProfileBackground($response, $request, $userId); @@ -90,7 +96,7 @@ class AssetsRoutes implements RouteHandler, UrlSource { return 404; } - private function serveAsset($response, $request, UserImageAssetInterface $assetInfo): void { + private function serveAsset(HttpResponseBuilder $response, HttpRequest $request, UserImageAssetInterface $assetInfo): void { $contentType = $assetInfo->getMimeType(); $publicPath = $assetInfo->getPublicPath(); $fileName = $assetInfo->getFileName(); diff --git a/src/Users/Assets/StaticUserImageAsset.php b/src/Users/Assets/StaticUserImageAsset.php index fecb65e..ff3ed67 100644 --- a/src/Users/Assets/StaticUserImageAsset.php +++ b/src/Users/Assets/StaticUserImageAsset.php @@ -2,9 +2,9 @@ namespace Misuzu\Users\Assets; class StaticUserImageAsset implements UserImageAssetInterface { - private $path = ''; - private $filename = ''; - private $relativePath = ''; + private string $path = ''; + private string $filename = ''; + private string $relativePath = ''; public function __construct(string $path, string $absolutePart = '') { $this->path = $path; diff --git a/src/Users/Assets/UserBackgroundAsset.php b/src/Users/Assets/UserBackgroundAsset.php index 774a0b9..29bd45a 100644 --- a/src/Users/Assets/UserBackgroundAsset.php +++ b/src/Users/Assets/UserBackgroundAsset.php @@ -37,6 +37,7 @@ class UserBackgroundAsset extends UserImageAsset { self::ATTRIB_SLIDE => 'slide', ]; + /** @return array<int, string> */ public static function getAttachmentStringOptions(): array { return [ self::ATTACH_COVER => 'Cover', @@ -121,6 +122,7 @@ class UserBackgroundAsset extends UserImageAsset { return $this; } + /** @return string[] */ public function getClassNames(string $format = '%s'): array { $names = []; $attachment = $this->getAttachment(); diff --git a/src/Users/Assets/UserImageAsset.php b/src/Users/Assets/UserImageAsset.php index b134583..2717c06 100644 --- a/src/Users/Assets/UserImageAsset.php +++ b/src/Users/Assets/UserImageAsset.php @@ -32,6 +32,7 @@ abstract class UserImageAsset implements UserImageAssetInterface { public abstract function getMaxHeight(): int; public abstract function getMaxBytes(): int; + /** @return int[] */ public function getAllowedTypes(): array { 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 mixed[] */ private function getImageSize(): array { return $this->isPresent() && ($imageSize = getimagesize($this->getPath())) ? $imageSize : []; } diff --git a/src/Users/Bans.php b/src/Users/Bans.php index b209ad0..df272ad 100644 --- a/src/Users/Bans.php +++ b/src/Users/Bans.php @@ -57,6 +57,7 @@ class Bans { return $count; } + /** @return \Iterator<int, BanInfo> */ public function getBans( UserInfo|string|null $userInfo = null, ?bool $activeOnly = null, @@ -177,6 +178,7 @@ class Bans { return $this->getBan((string)$this->dbConn->getLastInsertId()); } + /** @param BanInfo|string|array<BanInfo|string> $banInfos */ public function deleteBans(BanInfo|string|array $banInfos): void { if(!is_array($banInfos)) $banInfos = [$banInfos]; diff --git a/src/Users/ModNotes.php b/src/Users/ModNotes.php index 686577e..64b1760 100644 --- a/src/Users/ModNotes.php +++ b/src/Users/ModNotes.php @@ -53,6 +53,7 @@ class ModNotes { return $count; } + /** @return \Iterator<int, ModNoteInfo> */ public function getNotes( UserInfo|string|null $userInfo = null, UserInfo|string|null $authorInfo = null, @@ -127,6 +128,7 @@ class ModNotes { return $this->getNote((string)$this->dbConn->getLastInsertId()); } + /** @param ModNoteInfo|string|array<ModNoteInfo|string> $noteInfos */ public function deleteNotes(ModNoteInfo|string|array $noteInfos): void { if(!is_array($noteInfos)) $noteInfos = [$noteInfos]; diff --git a/src/Users/Roles.php b/src/Users/Roles.php index f6109a6..f55066a 100644 --- a/src/Users/Roles.php +++ b/src/Users/Roles.php @@ -10,11 +10,11 @@ use Misuzu\Pagination; class Roles { public const DEFAULT_ROLE = '1'; - private DbConnection $dbConn; private DbStatementCache $cache; - public function __construct(DbConnection $dbConn) { - $this->dbConn = $dbConn; + public function __construct( + private DbConnection $dbConn + ) { $this->cache = new DbStatementCache($dbConn); } @@ -52,6 +52,7 @@ class Roles { return $count; } + /** @return \Iterator<int, RoleInfo> */ public function getRoles( UserInfo|string|null $userInfo = null, ?bool $hidden = null, @@ -138,6 +139,7 @@ class Roles { return $this->getRole((string)$this->dbConn->getLastInsertId()); } + /** @param RoleInfo|string|array<RoleInfo|string> $roleInfos */ public function deleteRoles(RoleInfo|string|array $roleInfos): void { if(!is_array($roleInfos)) $roleInfos = [$roleInfos]; diff --git a/src/Users/Users.php b/src/Users/Users.php index d5cda54..cac0c01 100644 --- a/src/Users/Users.php +++ b/src/Users/Users.php @@ -12,11 +12,11 @@ use Misuzu\Tools; use Misuzu\Parsers\Parser; class Users { - private DbConnection $dbConn; private DbStatementCache $cache; - public function __construct(DbConnection $dbConn) { - $this->dbConn = $dbConn; + public function __construct( + private DbConnection $dbConn + ) { $this->cache = new DbStatementCache($dbConn); } @@ -98,6 +98,10 @@ class Users { 'random' => ['RAND()', null], ]; + /** + * @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery + * @return \Iterator<int, UserInfo>|UserInfo[] + */ public function getUsers( RoleInfo|string|null $roleInfo = null, UserInfo|string|null $after = null, @@ -456,6 +460,10 @@ class Users { return in_array($roleInfo, $this->hasRoles($userInfo, $roleInfo)); } + /** + * @param RoleInfo|string|array<RoleInfo|string> $roleInfos + * @return string[] + */ public function hasRoles( UserInfo|string $userInfo, RoleInfo|string|array $roleInfos @@ -494,6 +502,7 @@ class Users { return $roleIds; } + /** @param RoleInfo|string|array<RoleInfo|string> $roleInfos */ public function addRoles( UserInfo|string $userInfo, RoleInfo|string|array $roleInfos @@ -524,6 +533,7 @@ class Users { $stmt->execute(); } + /** @param RoleInfo|string|array<RoleInfo|string> $roleInfos */ public function removeRoles( UserInfo|string $userInfo, RoleInfo|string|array $roleInfos diff --git a/src/Users/UsersContext.php b/src/Users/UsersContext.php index 8f2a2e6..1c848b7 100644 --- a/src/Users/UsersContext.php +++ b/src/Users/UsersContext.php @@ -11,10 +11,16 @@ class UsersContext { public private(set) Warnings $warnings; public private(set) ModNotes $modNotes; + /** @var array<string, UserInfo> */ private array $userInfos = []; + + /** @var array<string, Colour> */ private array $userColours = []; + + /** @var array<string, int> */ private array $userRanks = []; + /** @var array<string, ?BanInfo> */ private array $activeBans = []; public function __construct(DbConnection $dbConn) { diff --git a/src/Users/UsersRpcHandler.php b/src/Users/UsersRpcHandler.php index abc0279..6798b33 100644 --- a/src/Users/UsersRpcHandler.php +++ b/src/Users/UsersRpcHandler.php @@ -18,6 +18,26 @@ final class UsersRpcHandler implements RpcHandler { 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')] public function queryGetUser(string $userId, bool $includeEMailAddress = false): array { try { diff --git a/src/Users/WarningInfo.php b/src/Users/WarningInfo.php index b748da8..1f3c1f8 100644 --- a/src/Users/WarningInfo.php +++ b/src/Users/WarningInfo.php @@ -23,6 +23,7 @@ class WarningInfo { ); } + /** @var string[] */ public array $bodyLines { get => explode("\n", $this->body); } diff --git a/src/Users/Warnings.php b/src/Users/Warnings.php index 4dd9314..36046bf 100644 --- a/src/Users/Warnings.php +++ b/src/Users/Warnings.php @@ -12,11 +12,11 @@ use Misuzu\Pagination; class Warnings { public const VISIBLE_BACKLOG = 90 * 24 * 60 * 60; - private DbConnection $dbConn; private DbStatementCache $cache; - public function __construct(DbConnection $dbConn) { - $this->dbConn = $dbConn; + public function __construct( + private DbConnection $dbConn + ) { $this->cache = new DbStatementCache($dbConn); } @@ -55,6 +55,7 @@ class Warnings { return $result->next() ? $result->getInteger(0) : 0; } + /** @return \Iterator<int, WarningInfo> */ public function getWarningsWithDefaultBacklog( UserInfo|string|null $userInfo = null, ?Pagination $pagination = null @@ -66,6 +67,7 @@ class Warnings { ); } + /** @return \Iterator<int, WarningInfo> */ public function getWarnings( UserInfo|string|null $userInfo = null, ?int $backlog = null, @@ -140,6 +142,7 @@ class Warnings { return $this->getWarning((string)$this->dbConn->getLastInsertId()); } + /** @param WarningInfo|string|array<WarningInfo|string> $warnInfos */ public function deleteWarnings(WarningInfo|string|array $warnInfos): void { if(!is_array($warnInfos)) $warnInfos = [$warnInfos]; diff --git a/src/Zalgo.php b/src/Zalgo.php index 4a31680..e93dafb 100644 --- a/src/Zalgo.php +++ b/src/Zalgo.php @@ -53,6 +53,7 @@ final class Zalgo { || in_array($char, self::CHARS_MIDDLE); } + /** @param string[] $array */ public static function getString(array $array, int $length): string { $string = ''; $rngMax = count($array) - 1; @@ -110,13 +111,13 @@ final class Zalgo { } if($going_up) - $str .= self::getString(self::CHARS_UP, $num_up); + $str .= self::getString(self::CHARS_UP, (int)$num_up); if($going_mid) - $str .= self::getString(self::CHARS_MIDDLE, $num_mid); + $str .= self::getString(self::CHARS_MIDDLE, (int)$num_mid); if($going_down) - $str .= self::getString(self::CHARS_DOWN, $num_down); + $str .= self::getString(self::CHARS_DOWN, (int)$num_down); } return $str;