Compare commits

..

22 commits

Author SHA1 Message Date
flash 44a4bb6e6f Prevent access to private messages when impersonating a user. 2024-06-02 19:57:58 +00:00
flash ec00cfa176 Base64 encode PM titles and bodies in the database.
To prevent personal discomfort with having to do database messages and seeing people's personal conversations.
I haven't run into it yet, but I'd rather avoid it altogether.
2024-06-02 19:54:33 +00:00
flash 1d295df8da Added broom closet PM stats. 2024-06-02 19:43:57 +00:00
flash 6a88ed8b11 Updated libraries. 2024-05-30 22:02:09 +00:00
flash 36bcf1ab1d Built Playpen icon updating into Misuzu.
Was previously handled by a stinky script.
2024-05-30 22:00:41 +00:00
flash 5d3e1d4960 Fixed wrong HTTP verb. 2024-03-30 15:22:11 +00:00
flash 9bb943bacf Fixed various oversights. 2024-03-30 03:19:08 +00:00
flash 107d16cf46 Updated Misuzu to new HTTP router. 2024-03-30 03:14:03 +00:00
flash 0afc5186a7 Fixed error when trying to access a topic with no posts associated. 2024-02-24 22:03:32 +00:00
flash 0300bae994 hurr 2024-02-21 00:31:25 +00:00
flash cb0c64f8ed Stinky fix for impersonation in chat auth. 2024-02-20 23:56:43 +00:00
flash 89ef9d9ad1 Fixed bans no longer working. 2024-02-15 22:55:24 +00:00
flash c02d922dc6 Fixed Forum Activity section always showing up. 2024-02-13 21:22:56 +00:00
flash 80cd6222c4 Fixed profile fields not showing up anymore. 2024-02-11 02:22:22 +00:00
flash 344a3c9160 Missed one! 2024-02-09 16:07:43 +00:00
flash df5dbdf3ad Fixed forum/topic breadcrumbs. 2024-02-08 15:20:44 +00:00
flash c0caceed7b Fixed use of wrong BanInfo constructor. 2024-02-08 15:18:57 +00:00
flash be54ce2c22 Fixed oversights on landing page. 2024-02-08 00:06:23 +00:00
flash 070dc5e782 Added lazy database object creation. 2024-02-07 00:04:45 +00:00
flash b89621cb1a Added PMs to data export. 2024-02-05 22:56:51 +00:00
flash 760cca0e5d whoops 2024-02-02 21:53:36 +00:00
flash fe77f1616c Updated to new EEPROM script. 2024-02-02 21:42:40 +00:00
79 changed files with 1526 additions and 1421 deletions

View file

@ -1,4 +1,4 @@
Copyright (c) 2017-2023, flashwave <me@flash.moe> Copyright (c) 2017-2024, flashwave <me@flash.moe>
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View file

@ -19,7 +19,7 @@ const MszEEPROM = (() => {
const scriptElem = $e({ const scriptElem = $e({
tag: 'script', tag: 'script',
attrs: { attrs: {
src: `${peepPath}/eeprom.js`, src: `${peepPath}/scripts/eepromv1a.js`,
charset: 'utf-8', charset: 'utf-8',
type: 'text/javascript', type: 'text/javascript',
onerror: () => reject(), onerror: () => reject(),

View file

@ -22,10 +22,10 @@ const MszForumEditor = function(form) {
MszEEPROM.init() MszEEPROM.init()
.catch(() => console.error('Failed to initialise EEPROM')) .catch(() => console.error('Failed to initialise EEPROM'))
.then(() => { .then(() => {
const eepromClient = new EEPROM(peepApp, `${peepPath}/uploads`, ''); const eepromClient = new EEPROM(peepApp, peepPath);
const eepromHistory = <div class="eeprom-widget-history-items"/>; const eepromHistory = <div class="eeprom-widget-history-items"/>;
const eepromHandleFileUpload = file => { const eepromHandleFileUpload = async file => {
const uploadElemNameValue = <div class="eeprom-widget-file-name-value" title={file.name}>{file.name}</div>; const uploadElemNameValue = <div class="eeprom-widget-file-name-value" title={file.name}>{file.name}</div>;
const uploadElemName = <a class="eeprom-widget-file-name" target="_blank">{uploadElemNameValue}</a>; const uploadElemName = <a class="eeprom-widget-file-name" target="_blank">{uploadElemNameValue}</a>;
const uploadElemProgressText = <div class="eeprom-widget-file-progress">Please wait...</div>; const uploadElemProgressText = <div class="eeprom-widget-file-progress">Please wait...</div>;
@ -46,62 +46,21 @@ const MszForumEditor = function(form) {
eepromHistory.appendChild(uploadElem); eepromHistory.appendChild(uploadElem);
const explodeUploadElem = () => $r(uploadElem); const explodeUploadElem = () => $r(uploadElem);
const uploadTask = eepromClient.createUpload(file); const uploadTask = eepromClient.create(file);
uploadTask.onProgress = function(progressInfo) { uploadTask.onProgress(prog => {
const progressValue = `${progressInfo.progress}%`; uploadElemProgressBarValue.style.width = `${Math.ceil(prog.progress * 100)}%`;
uploadElemProgressBarValue.style.width = progressValue; uploadElemProgressText.textContent = `${prog.progress.toLocaleString(undefined, { style: 'percent' })} (${prog.total - prog.loaded} bytes remaining)`;
uploadElemProgressText.textContent = `${progressValue} (${progressInfo.total - progressInfo.loaded} bytes remaining)`; });
};
uploadTask.onFailure = function(errorInfo) { try {
if(!errorInfo.userAborted) { const fileInfo = await uploadTask.start();
let errorText = 'Was unable to upload file.';
switch(errorInfo.error) {
case EEPROM.ERR_INVALID:
errorText = 'Upload request was invalid.';
break;
case EEPROM.ERR_AUTH:
errorText = 'Upload authentication failed, refresh and try again.';
break;
case EEPROM.ERR_ACCESS:
errorText = "You're not allowed to upload files.";
break;
case EEPROM.ERR_GONE:
errorText = 'Upload client has a configuration error or the server is gone.';
break;
case EEPROM.ERR_DMCA:
errorText = 'This file has been uploaded before and was removed for copyright reasons, you cannot upload this file.';
break;
case EEPROM.ERR_SERVER:
errorText = 'Upload server returned a critical error, try again later.';
break;
case EEPROM.ERR_SIZE:
if(errorInfo.maxSize < 1)
errorText = 'Selected file is too large.';
else {
const types = ['bytes', 'KB', 'MB', 'GB', 'TB'],
typeIndex = parseInt(Math.floor(Math.log(errorInfo.maxSize) / Math.log(1024))),
number = Math.round(errorInfo.maxSize / Math.pow(1024, _i), 2);
errorText = `Upload may not be larger than ${number} ${types[typeIndex]}.`;
}
break;
}
uploadElem.classList.add('eeprom-widget-file-fail');
uploadElemProgressText.textContent = errorText;
MszShowMessageBox(errorText, 'Upload Error');
}
};
uploadTask.onComplete = function(fileInfo) {
uploadElem.classList.add('eeprom-widget-file-done'); uploadElem.classList.add('eeprom-widget-file-done');
uploadElemName.href = fileInfo.url; uploadElemName.href = fileInfo.url;
uploadElemProgressText.textContent = ''; uploadElemProgressText.textContent = '';
const insertTheLinkIntoTheBoxEx2 = function() { const insertTheLinkIntoTheBoxEx2 = () => {
const parserMode = parseInt(parserElem.value); const parserMode = parseInt(parserElem.value);
let insertText = location.protocol + fileInfo.url; let insertText = location.protocol + fileInfo.url;
@ -124,14 +83,27 @@ const MszForumEditor = function(form) {
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => insertTheLinkIntoTheBoxEx2()}>Insert</a>); uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => insertTheLinkIntoTheBoxEx2()}>Insert</a>);
uploadElemProgressText.appendChild($t(' ')); uploadElemProgressText.appendChild($t(' '));
uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => { uploadElemProgressText.appendChild(<a href="javascript:void(0)" onclick={() => {
eepromClient.deleteUpload(fileInfo).start(); eepromClient.delete(fileInfo)
explodeUploadElem(); .then(() => explodeUploadElem())
.catch(ex => {
console.error(ex);
MszShowMessageBox(ex, 'Upload Error');
});
}}>Delete</a>); }}>Delete</a>);
insertTheLinkIntoTheBoxEx2(); insertTheLinkIntoTheBoxEx2();
}; } catch(ex) {
let errorText = 'Upload aborted.';
uploadTask.start(); if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
}
uploadElem.classList.add('eeprom-widget-file-fail');
uploadElemProgressText.textContent = errorText;
await MszShowMessageBox(errorText, 'Upload Error');
}
}; };
const eepromFormInput = <input type="file" multiple={true} class="eeprom-widget-form-input" const eepromFormInput = <input type="file" multiple={true} class="eeprom-widget-form-input"

View file

@ -67,16 +67,12 @@ const MszMessagesReply = function(element) {
MszEEPROM.init() MszEEPROM.init()
.catch(() => console.error('Failed to initialise EEPROM')) .catch(() => console.error('Failed to initialise EEPROM'))
.then(() => { .then(() => {
const eepromClient = new EEPROM(peepApp, `${peepPath}/uploads`, ''); const eepromClient = new EEPROM(peepApp, peepPath);
const eepromHandleFileUpload = file => { const eepromHandleFileUpload = async file => {
const uploadTask = eepromClient.createUpload(file); const uploadTask = eepromClient.create(file);
uploadTask.onFailure = errorInfo => { try {
if(!errorInfo.userAborted) const fileInfo = await uploadTask.start();
MszShowMessageBox('Was unable to upload file.', 'Upload Error');
};
uploadTask.onComplete = fileInfo => {
const parserMode = parseInt(parserSelect.value); const parserMode = parseInt(parserSelect.value);
let insertText = location.protocol + fileInfo.url; let insertText = location.protocol + fileInfo.url;
@ -94,9 +90,16 @@ const MszMessagesReply = function(element) {
$insertTags(bodyElem, insertText, ''); $insertTags(bodyElem, insertText, '');
bodyElem.value = bodyElem.value.trim(); bodyElem.value = bodyElem.value.trim();
}; } catch(ex) {
let errorText = 'Upload aborted.';
uploadTask.start(); if(!ex.aborted) {
console.error(ex);
errorText = ex.toString();
}
await MszShowMessageBox(errorText, 'Upload Error');
}
}; };
bodyElem.addEventListener('paste', ev => { bodyElem.addEventListener('paste', ev => {

View file

@ -12,8 +12,11 @@ const MszShowConfirmBox = async (text, title, target) => {
}; };
const MszShowMessageBox = (text, title, buttons, target) => { const MszShowMessageBox = (text, title, buttons, target) => {
if(typeof text !== 'string') if(typeof text !== 'string') {
throw 'text must be a string'; if(text !== undefined && text !== null && typeof text.toString === 'function')
text = text.toString();
else throw 'text must be a string';
}
if(!(target instanceof Element)) if(!(target instanceof Element))
target = document.body; target = document.body;

369
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class BaseSixtyFourEncodePmsInDb_20240602_194809 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$conn->execute('UPDATE msz_messages SET msg_title = TO_BASE64(msg_title), msg_body = TO_BASE64(msg_body)');
$conn->execute('
ALTER TABLE `msz_messages`
CHANGE COLUMN `msg_title` `msg_title` TINYBLOB NOT NULL AFTER `msg_reply_to`,
CHANGE COLUMN `msg_body` `msg_body` BLOB NOT NULL AFTER `msg_title`;
');
}
}

593
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
{ {
"dependencies": { "dependencies": {
"@swc/core": "^1.3.69", "@swc/core": "^1.5.24",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.19",
"cssnano": "^6.0.1", "cssnano": "^6.1.2",
"postcss": "^8.4.26" "postcss": "^8.4.38"
} }
} }

View file

@ -168,7 +168,7 @@ $perms = $perms->checkMany([
]); ]);
Template::render('forum.forum', [ Template::render('forum.forum', [
'forum_breadcrumbs' => $forumCategories->getCategoryAncestry($categoryInfo), 'forum_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo), 'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
'forum_info' => $categoryInfo, 'forum_info' => $categoryInfo,
'forum_children' => $children, 'forum_children' => $children,

View file

@ -296,7 +296,7 @@ try {
} }
Template::render('forum.posting', [ Template::render('forum.posting', [
'posting_breadcrumbs' => $forumCategories->getCategoryAncestry($categoryInfo), 'posting_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo), 'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
'posting_user' => $currentUser, 'posting_user' => $currentUser,
'posting_user_colour' => $usersCtx->getUserColour($currentUser), 'posting_user_colour' => $usersCtx->getUserColour($currentUser),

View file

@ -291,7 +291,11 @@ $postInfos = $forumPosts->getPosts(
if(empty($postInfos)) if(empty($postInfos))
Template::throwError(404); Template::throwError(404);
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo); try {
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
} catch(RuntimeException $ex) {
Template::throwError(404);
}
$posts = []; $posts = [];
@ -326,7 +330,7 @@ $perms = $perms->checkMany([
]); ]);
Template::render('forum.topic', [ Template::render('forum.topic', [
'topic_breadcrumbs' => $forumCategories->getCategoryAncestry($topicInfo), 'topic_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($topicInfo)),
'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo), 'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo),
'topic_info' => $topicInfo, 'topic_info' => $topicInfo,
'category_info' => $categoryInfo, 'category_info' => $categoryInfo,

View file

@ -18,7 +18,7 @@ else
try { try {
$isNew = false; $isNew = false;
$emoteInfo = $emotes->getEmote($emoteId); $emoteInfo = $emotes->getEmote($emoteId);
$emoteStrings = $emotes->getEmoteStrings($emoteInfo); $emoteStrings = iterator_to_array($emotes->getEmoteStrings($emoteInfo));
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
Template::throwError(404); Template::throwError(404);
} }

View file

@ -13,7 +13,7 @@ $pagination = new Pagination($auditLog->countLogs(), 50);
if(!$pagination->hasValidOffset()) if(!$pagination->hasValidOffset())
Template::throwError(404); Template::throwError(404);
$logs = $auditLog->getLogs(pagination: $pagination); $logs = iterator_to_array($auditLog->getLogs(pagination: $pagination));
$userInfos = []; $userInfos = [];
$userColours = []; $userColours = [];

View file

@ -106,7 +106,7 @@ if(CSRF::validateRequest() && $canEdit) {
} }
$existingRoles = []; $existingRoles = [];
foreach($roles->getRoles(userInfo: $userInfo) as $roleInfo) foreach(iterator_to_array($roles->getRoles(userInfo: $userInfo)) as $roleInfo)
$existingRoles[$roleInfo->getId()] = $roleInfo; $existingRoles[$roleInfo->getId()] = $roleInfo;
$removeRoles = []; $removeRoles = [];
@ -226,7 +226,7 @@ if(CSRF::validateRequest() && $canEdit) {
return; return;
} }
$rolesAll = $roles->getRoles(); $rolesAll = iterator_to_array($roles->getRoles());
$userRoleIds = $users->hasRoles($userInfo, $rolesAll); $userRoleIds = $users->hasRoles($userInfo, $rolesAll);
Template::render('manage.users.user', [ Template::render('manage.users.user', [

View file

@ -113,7 +113,7 @@ if($isEditing) {
if(!$perms->edit_profile) { if(!$perms->edit_profile) {
$notices[] = 'You\'re not allowed to edit your profile'; $notices[] = 'You\'re not allowed to edit your profile';
} else { } else {
$profileFieldInfos = $profileFields->getFields(); $profileFieldInfos = iterator_to_array($profileFields->getFields());
$profileFieldsSetInfos = []; $profileFieldsSetInfos = [];
$profileFieldsSetValues = []; $profileFieldsSetValues = [];
$profileFieldsRemove = []; $profileFieldsRemove = [];
@ -299,7 +299,7 @@ $profileStats->forum_post_count = $forumCtx->countTotalUserPosts($userInfo);
$profileStats->comments_count = $msz->getComments()->countPosts(userInfo: $userInfo, deleted: false); $profileStats->comments_count = $msz->getComments()->countPosts(userInfo: $userInfo, deleted: false);
if(!$viewingAsGuest) { if(!$viewingAsGuest) {
Template::set('profile_warnings', $usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo)); Template::set('profile_warnings', iterator_to_array($usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo)));
if((!$isBanned || $canEdit)) { if((!$isBanned || $canEdit)) {
$unranked = $cfg->getValues([ $unranked = $cfg->getValues([
@ -323,9 +323,9 @@ if(!$viewingAsGuest) {
); );
$activeTopicInfo = $activeTopicStats->success ? $forumTopics->getTopic(topicId: $activeTopicStats->topicId) : null; $activeTopicInfo = $activeTopicStats->success ? $forumTopics->getTopic(topicId: $activeTopicStats->topicId) : null;
$profileFieldValues = $profileFields->getFieldValues($userInfo); $profileFieldValues = iterator_to_array($profileFields->getFieldValues($userInfo));
$profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues); $profileFieldInfos = $profileFieldInfos ?? iterator_to_array($profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues));
$profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues); $profileFieldFormats = iterator_to_array($profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues));
$profileFieldRawValues = []; $profileFieldRawValues = [];
$profileFieldLinkValues = []; $profileFieldLinkValues = [];

View file

@ -116,7 +116,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest) if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
$userInfo = $users->getUser($userInfo->getId(), 'id'); $userInfo = $users->getUser($userInfo->getId(), 'id');
$userRoles = $roles->getRoles(userInfo: $userInfo); $userRoles = iterator_to_array($roles->getRoles(userInfo: $userInfo));
Template::render('settings.account', [ Template::render('settings.account', [
'errors' => $errors, 'errors' => $errors,

View file

@ -129,6 +129,7 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
$tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_redirects', ['topic_id:s', 'user_id:s:n', 'topic_redir_url:s', 'topic_redir_created:t']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_redirects', ['topic_id:s', 'user_id:s:n', 'topic_redir_url:s', 'topic_redir_created:t']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_track', ['user_id:s', 'topic_id:s', 'forum_id:s', 'track_last_read:t']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_track', ['user_id:s', 'topic_id:s', 'forum_id:s', 'track_last_read:t']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'login_attempts', ['user_id:s:n', 'attempt_success:b', 'attempt_ip:a', 'attempt_country:s', 'attempt_created:t', 'attempt_user_agent:s']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'login_attempts', ['user_id:s:n', 'attempt_success:b', 'attempt_ip:a', 'attempt_country:s', 'attempt_created:t', 'attempt_user_agent:s']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'messages', ['msg_id:s', 'msg_owner_id:s', 'msg_author_id:s:n', 'msg_recipient_id:s:n', 'msg_reply_to:s:n', 'msg_title:s', 'msg_body:s', 'msg_parser:i', 'msg_created:t', 'msg_sent:t:n', 'msg_read:t:n', 'msg_deleted:t:n'], 'msg_owner_id');
$tmpFiles[] = db_to_zip($archive, $userInfo, 'news_posts', ['post_id:s', 'category_id:s', 'user_id:s:n', 'comment_section_id:s:n', 'post_is_featured:b', 'post_title:s', 'post_text:s', 'post_scheduled:t', 'post_created:t', 'post_updated:t', 'post_deleted:t:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'news_posts', ['post_id:s', 'category_id:s', 'user_id:s:n', 'comment_section_id:s:n', 'post_is_featured:b', 'post_title:s', 'post_text:s', 'post_scheduled:t', 'post_created:t', 'post_updated:t', 'post_deleted:t:n']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms', ['user_id:s:n', 'role_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_allow:i', 'perms_deny:i']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'perms', ['user_id:s:n', 'role_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_allow:i', 'perms_deny:i']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms_calculated', ['user_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_calculated:i']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'perms_calculated', ['user_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_calculated:i']);

View file

@ -15,8 +15,8 @@ $auditLog = $msz->getAuditLog();
$loginHistoryPagination = new Pagination($loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp'); $loginHistoryPagination = new Pagination($loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp');
$accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 10, 'ap'); $accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 10, 'ap');
$loginHistory = $loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination); $loginHistory = iterator_to_array($loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination));
$auditLogs = $auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination); $auditLogs = iterator_to_array($auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination));
Template::render('settings.logs', [ Template::render('settings.logs', [
'login_history_list' => $loginHistory, 'login_history_list' => $loginHistory,

View file

@ -61,7 +61,7 @@ class AuditLog {
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
@ -98,13 +98,8 @@ class AuditLog {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult();
$logs = [];
while($result->next()) return $stmt->getResult()->getIterator(AuditLogInfo::fromResult(...));
$logs[] = new AuditLogInfo($result);
return $logs;
} }
public function createLog( public function createLog(

View file

@ -7,20 +7,24 @@ use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
class AuditLogInfo { class AuditLogInfo {
private ?string $userId; public function __construct(
private string $action; private ?string $userId,
private array $params; private string $action,
private int $created; private array $params,
private string $address; private int $created,
private string $country; private string $address,
private string $country,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): AuditLogInfo {
$this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0); return new AuditLogInfo(
$this->action = $result->getString(1); userId: $result->getStringOrNull(0),
$this->params = json_decode($result->getString(2)); action: $result->getString(1),
$this->created = $result->getInteger(3); params: json_decode($result->getString(2)),
$this->address = $result->isNull(4) ? '::1' : $result->getString(4); // apparently this being NULL is possible? created: $result->getInteger(3),
$this->country = $result->getString(5); address: $result->isNull(4) ? '::1' : $result->getString(4), // apparently this being NULL is possible?
country: $result->getString(5),
);
} }
public function hasUserId(): bool { public function hasUserId(): bool {

View file

@ -7,22 +7,26 @@ use Index\Net\IPAddress;
use Misuzu\ClientInfo; use Misuzu\ClientInfo;
class LoginAttemptInfo { class LoginAttemptInfo {
private ?string $userId; public function __construct(
private bool $success; private ?string $userId,
private string $remoteAddr; private bool $success,
private string $countryCode; private string $remoteAddr,
private int $created; private string $countryCode,
private string $userAgent; private int $created,
private string $clientInfo; private string $userAgent,
private string $clientInfo,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): LoginAttemptInfo {
$this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0); return new LoginAttemptInfo(
$this->success = $result->getInteger(1) !== 0; userId: $result->getStringOrNull(0),
$this->remoteAddr = $result->getString(2); success: $result->getBoolean(1),
$this->countryCode = $result->getString(3); remoteAddr: $result->getString(2),
$this->created = $result->getInteger(4); countryCode: $result->getString(3),
$this->userAgent = $result->getString(5); created: $result->getInteger(4),
$this->clientInfo = $result->getString(6); userAgent: $result->getString(5),
clientInfo: $result->getString(6),
);
} }
public function hasUserId(): bool { public function hasUserId(): bool {

View file

@ -83,7 +83,7 @@ class LoginAttempts {
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null, TimeSpan|int|null $timeRange = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
@ -127,13 +127,7 @@ class LoginAttempts {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(LoginAttemptInfo::fromResult(...));
$attempts = [];
while($result->next())
$attempts[] = new LoginAttemptInfo($result);
return $attempts;
} }
public function recordAttempt( public function recordAttempt(

View file

@ -7,32 +7,36 @@ use Index\Net\IPAddress;
use Misuzu\ClientInfo; use Misuzu\ClientInfo;
class SessionInfo { class SessionInfo {
private string $id; public function __construct(
private string $userId; private string $id,
private string $token; private string $userId,
private string $firstRemoteAddr; private string $token,
private ?string $lastRemoteAddr; private string $firstRemoteAddr,
private string $userAgent; private ?string $lastRemoteAddr,
private string $clientInfo; private string $userAgent,
private string $countryCode; private string $clientInfo,
private int $expires; private string $countryCode,
private bool $bumpExpires; private int $expires,
private int $created; private bool $bumpExpires,
private ?int $lastActive; private int $created,
private ?int $lastActive,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): SessionInfo {
$this->id = (string)$result->getInteger(0); return new SessionInfo(
$this->userId = (string)$result->getInteger(1); id: $result->getString(0),
$this->token = $result->getString(2); userId: $result->getString(1),
$this->firstRemoteAddr = $result->getString(3); token: $result->getString(2),
$this->lastRemoteAddr = $result->isNull(4) ? null : $result->getString(4); firstRemoteAddr: $result->getString(3),
$this->userAgent = $result->getString(5); lastRemoteAddr: $result->getStringOrNull(4),
$this->clientInfo = $result->getString(6); userAgent: $result->getString(5),
$this->countryCode = $result->getString(7); clientInfo: $result->getString(6),
$this->expires = $result->getInteger(8); countryCode: $result->getString(7),
$this->bumpExpires = $result->getInteger(9) !== 0; expires: $result->getInteger(8),
$this->created = $result->getInteger(10); bumpExpires: $result->getBoolean(9),
$this->lastActive = $result->isNull(11) ? null : $result->getInteger(11); created: $result->getInteger(10),
lastActive: $result->getIntegerOrNull(11),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -58,7 +58,7 @@ class Sessions {
public function getSessions( public function getSessions(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
@ -85,13 +85,7 @@ class Sessions {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(SessionInfo::fromResult(...));
$sessions = [];
while($result->next())
$sessions[] = new SessionInfo($result);
return $sessions;
} }
public function getSession( public function getSession(
@ -124,7 +118,7 @@ class Sessions {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Session not found.'); throw new RuntimeException('Session not found.');
return new SessionInfo($result); return SessionInfo::fromResult($result);
} }
public function createSession( public function createSession(

View file

@ -5,20 +5,24 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ChangeInfo { class ChangeInfo {
private string $id; public function __construct(
private ?string $userId; private string $id,
private int $action; private ?string $userId,
private int $created; private int $action,
private string $summary; private int $created,
private string $body; private string $summary,
private string $body,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ChangeInfo {
$this->id = (string)$result->getInteger(0); return new ChangeInfo(
$this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1); id: $result->getString(0),
$this->action = $result->getInteger(2); userId: $result->getStringOrNull(1),
$this->created = $result->getInteger(3); action: $result->getInteger(2),
$this->summary = $result->getString(4); created: $result->getInteger(3),
$this->body = $result->getString(5); summary: $result->getString(4),
body: $result->getString(5),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -121,7 +121,7 @@ class Changelog {
DateTime|int|null $dateTime = null, DateTime|int|null $dateTime = null,
?array $tags = null, ?array $tags = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($dateTime instanceof DateTime) if($dateTime instanceof DateTime)
@ -169,13 +169,7 @@ class Changelog {
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(ChangeInfo::fromResult(...));
$changes = [];
while($result->next())
$changes[] = new ChangeInfo($result);
return $changes;
} }
public function getChange(string $changeId): ChangeInfo { public function getChange(string $changeId): ChangeInfo {
@ -187,7 +181,7 @@ class Changelog {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No tag with that ID exists.'); throw new RuntimeException('No tag with that ID exists.');
return new ChangeInfo($result); return ChangeInfo::fromResult($result);
} }
public function createChange( public function createChange(

View file

@ -3,20 +3,12 @@ namespace Misuzu\Changelog;
use ErrorException; use ErrorException;
use RuntimeException; use RuntimeException;
use Index\Routing\Route; use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Routing\RouteHandler; use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\Pagination;
use Misuzu\SiteInfo;
use Misuzu\Template;
use Misuzu\Auth\AuthInfo; use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\Comments; use Misuzu\Comments\{Comments,CommentsEx};
use Misuzu\Comments\CommentsEx; use Misuzu\Feeds\{Feed,FeedItem,AtomFeedSerializer,RssFeedSerializer};
use Misuzu\Feeds\Feed; use Misuzu\URLs\{URLInfo,URLRegistry};
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\URLs\URLInfo;
use Misuzu\URLs\URLRegistry;
use Misuzu\Users\UsersContext; use Misuzu\Users\UsersContext;
final class ChangelogRoutes extends RouteHandler { final class ChangelogRoutes extends RouteHandler {
@ -34,7 +26,7 @@ final class ChangelogRoutes extends RouteHandler {
return $comments->getCommentsForLayout($categoryName); return $comments->getCommentsForLayout($categoryName);
} }
#[Route('GET', '/changelog')] #[HttpGet('/changelog')]
#[URLInfo('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])] #[URLInfo('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
public function getIndex($response, $request) { public function getIndex($response, $request) {
$filterDate = (string)$request->getParam('date'); $filterDate = (string)$request->getParam('date');
@ -73,15 +65,16 @@ final class ChangelogRoutes extends RouteHandler {
if(!$pagination->hasValidOffset()) if(!$pagination->hasValidOffset())
return 404; return 404;
$changeInfos = $this->changelog->getChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
if(empty($changeInfos))
return 404;
$changes = []; $changes = [];
$changeInfos = $this->changelog->getChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
$commentsCategoryName = null;
foreach($changeInfos as $changeInfo) { foreach($changeInfos as $changeInfo) {
$userInfo = $changeInfo->hasUserId() ? $this->usersCtx->getUserInfo($changeInfo->getUserId()) : null; $userInfo = $changeInfo->hasUserId() ? $this->usersCtx->getUserInfo($changeInfo->getUserId()) : null;
if($commentsCategoryName === null)
$commentsCategoryName = $changeInfo->getCommentsCategoryName();
$changes[] = [ $changes[] = [
'change' => $changeInfo, 'change' => $changeInfo,
'user' => $userInfo, 'user' => $userInfo,
@ -89,17 +82,20 @@ final class ChangelogRoutes extends RouteHandler {
]; ];
} }
if(empty($changes))
return 404;
return Template::renderRaw('changelog.index', [ return Template::renderRaw('changelog.index', [
'changelog_infos' => $changes, 'changelog_infos' => $changes,
'changelog_date' => $filterDate, 'changelog_date' => $filterDate,
'changelog_user' => $filterUser, 'changelog_user' => $filterUser,
'changelog_tags' => $filterTags, 'changelog_tags' => $filterTags,
'changelog_pagination' => $pagination, 'changelog_pagination' => $pagination,
'comments_info' => empty($filterDate) ? null : $this->getCommentsInfo($changeInfos[0]->getCommentsCategoryName()), 'comments_info' => empty($filterDate) && $commentsCategoryName !== null ? null : $this->getCommentsInfo($commentsCategoryName),
]); ]);
} }
#[Route('GET', '/changelog/change/:id')] #[HttpGet('/changelog/change/([0-9]+)')]
#[URLInfo('changelog-change', '/changelog/change/<change>')] #[URLInfo('changelog-change', '/changelog/change/<change>')]
#[URLInfo('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')] #[URLInfo('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
public function getChange($response, $request, string $changeId) { public function getChange($response, $request, string $changeId) {
@ -148,14 +144,14 @@ final class ChangelogRoutes extends RouteHandler {
return $feed; return $feed;
} }
#[Route('GET', '/changelog.rss')] #[HttpGet('/changelog.rss')]
#[URLInfo('changelog-feed-rss', '/changelog.rss')] #[URLInfo('changelog-feed-rss', '/changelog.rss')]
public function getFeedRSS($response) { public function getFeedRSS($response) {
$response->setContentType('application/rss+xml; charset=utf-8'); $response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed($this->createFeed('rss')); return (new RssFeedSerializer)->serializeFeed($this->createFeed('rss'));
} }
#[Route('GET', '/changelog.atom')] #[HttpGet('/changelog.atom')]
#[URLInfo('changelog-feed-atom', '/changelog.atom')] #[URLInfo('changelog-feed-atom', '/changelog.atom')]
public function getFeedAtom($response) { public function getFeedAtom($response) {
$response->setContentType('application/atom+xml; charset=utf-8'); $response->setContentType('application/atom+xml; charset=utf-8');

View file

@ -46,7 +46,7 @@ class Comments {
public function getCategories( public function getCategories(
UserInfo|string|null $owner = null, UserInfo|string|null $owner = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($owner instanceof UserInfo) if($owner instanceof UserInfo)
$owner = $owner->getId(); $owner = $owner->getId();
@ -72,13 +72,7 @@ class Comments {
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(CommentsCategoryInfo::fromResult(...));
$categories = [];
while($result->next())
$categories[] = new CommentsCategoryInfo($result);
return $categories;
} }
public function getCategory( public function getCategory(
@ -124,7 +118,7 @@ class Comments {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Comments category not found.'); throw new RuntimeException('Comments category not found.');
return new CommentsCategoryInfo($result); return CommentsCategoryInfo::fromResult($result);
} }
public function checkCategoryNameExists(string $name): bool { public function checkCategoryNameExists(string $name): bool {
@ -276,7 +270,7 @@ class Comments {
?bool $deleted = null, ?bool $deleted = null,
bool $includeRepliesCount = false, bool $includeRepliesCount = false,
bool $includeVotesCount = false bool $includeVotesCount = false
): array { ): iterable {
if($categoryInfo instanceof CommentsCategoryInfo) if($categoryInfo instanceof CommentsCategoryInfo)
$categoryInfo = $categoryInfo->getId(); $categoryInfo = $categoryInfo->getId();
if($parentInfo instanceof CommentsPostInfo) if($parentInfo instanceof CommentsPostInfo)
@ -330,13 +324,7 @@ class Comments {
$stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); $stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->execute(); $stmt->execute();
$posts = []; return $stmt->getResult()->getIterator(fn($result) => CommentsPostInfo::fromResult($result, $includeRepliesCount, $includeVotesCount));
$result = $stmt->getResult();
while($result->next())
$posts[] = new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount);
return $posts;
} }
public function getPost( public function getPost(
@ -362,7 +350,7 @@ class Comments {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No comment with that ID exists.'); throw new RuntimeException('No comment with that ID exists.');
return new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount); return CommentsPostInfo::fromResult($result, $includeRepliesCount, $includeVotesCount);
} }
public function createPost( public function createPost(

View file

@ -6,20 +6,24 @@ use Index\Data\IDbResult;
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;
class CommentsCategoryInfo { class CommentsCategoryInfo {
private string $id; public function __construct(
private string $name; private string $id,
private ?string $ownerId; private string $name,
private int $created; private ?string $ownerId,
private ?int $locked; private int $created,
private int $comments; private ?int $locked,
private int $comments,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): CommentsCategoryInfo {
$this->id = (string)$result->getInteger(0); return new CommentsCategoryInfo(
$this->name = $result->getString(1); id: $result->getString(0),
$this->ownerId = $result->isNull(2) ? null : (string)$result->getInteger(2); name: $result->getString(1),
$this->created = $result->getInteger(3); ownerId: $result->getStringOrNull(2),
$this->locked = $result->isNull(4) ? null : $result->getInteger(4); created: $result->getInteger(3),
$this->comments = $result->getInteger(5); locked: $result->getIntegerOrNull(4),
comments: $result->getInteger(5),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -5,47 +5,53 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class CommentsPostInfo { class CommentsPostInfo {
private string $id;
private string $categoryId;
private ?string $userId;
private ?string $replyingTo;
private string $body;
private int $created;
private ?int $pinned;
private ?int $updated;
private ?int $deleted;
private int $replies;
private int $votesTotal;
private int $votesPositive;
private int $votesNegative;
public function __construct( public function __construct(
private string $id,
private string $categoryId,
private ?string $userId,
private ?string $replyingTo,
private string $body,
private int $created,
private ?int $pinned,
private ?int $updated,
private ?int $deleted,
private int $replies,
private int $votesTotal,
private int $votesPositive,
private int $votesNegative,
) {}
public static function fromResult(
IDbResult $result, IDbResult $result,
bool $includeRepliesCount = false, bool $includeRepliesCount = false,
bool $includeVotesCount = false bool $includeVotesCount = false
) { ): CommentsPostInfo {
$args = 0; $args = [];
$this->id = (string)$result->getInteger($args); $count = 0;
$this->categoryId = (string)$result->getInteger(++$args);
$this->userId = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
$this->replyingTo = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
$this->body = $result->getString(++$args);
$this->created = $result->getInteger(++$args);
$this->pinned = $result->isNull(++$args) ? null : $result->getInteger($args);
$this->updated = $result->isNull(++$args) ? null : $result->getInteger($args);
$this->deleted = $result->isNull(++$args) ? null : $result->getInteger($args);
$this->replies = $includeRepliesCount ? $result->getInteger(++$args) : 0; $args[] = $result->getString($count); // id
$args[] = $result->getString(++$count); // categoryId
$args[] = $result->getStringOrNull(++$count); // userId
$args[] = $result->getStringOrNull(++$count); // replyingTo
$args[] = $result->getString(++$count); // body
$args[] = $result->getInteger(++$count); // created
$args[] = $result->getIntegerOrNull(++$count); // pinned
$args[] = $result->getIntegerOrNull(++$count); // updated
$args[] = $result->getIntegerOrNull(++$count); // deleted
$args[] = $includeRepliesCount ? $result->getInteger(++$count) : 0;
if($includeVotesCount) { if($includeVotesCount) {
$this->votesTotal = $result->getInteger(++$args); $args[] = $result->getInteger(++$count); // votesTotal
$this->votesPositive = $result->getInteger(++$args); $args[] = $result->getInteger(++$count); // votesPositive
$this->votesNegative = $result->getInteger(++$args); $args[] = $result->getInteger(++$count); // votesNegative
} else { } else {
$this->votesTotal = 0; $args[] = 0;
$this->votesPositive = 0; $args[] = 0;
$this->votesNegative = 0; $args[] = 0;
} }
return new CommentsPostInfo(...$args);
} }
public function getId(): string { public function getId(): string {

View file

@ -5,14 +5,18 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class CounterInfo { class CounterInfo {
private string $name; public function __construct(
private int $value; private string $name,
private int $updated; private int $value,
private int $updated,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): CounterInfo {
$this->name = $result->getString(0); return new CounterInfo(
$this->value = $result->getInteger(1); name: $result->getString(0),
$this->updated = $result->getInteger(2); value: $result->getInteger(1),
updated: $result->getInteger(2),
);
} }
public function getName(): string { public function getName(): string {

View file

@ -25,7 +25,7 @@ class Counters {
public function getCounters( public function getCounters(
?string $orderBy = null, ?string $orderBy = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
$hasOrderBy = $orderBy !== null; $hasOrderBy = $orderBy !== null;
$hasPagination = $pagination !== null; $hasPagination = $pagination !== null;
@ -47,13 +47,7 @@ class Counters {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(CounterInfo::fromResult(...));
$counters = [];
while($result->next())
$counters[] = new CounterInfo($result);
return $counters;
} }
public function get(array|string $names): array|int { public function get(array|string $names): array|int {

View file

@ -5,16 +5,20 @@ use Stringable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class EmoteInfo implements Stringable { class EmoteInfo implements Stringable {
private string $id; public function __construct(
private int $order; private string $id,
private int $rank; private int $order,
private string $url; private int $rank,
private string $url,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): EmoteInfo {
$this->id = (string)$result->getInteger(0); return new EmoteInfo(
$this->order = $result->getInteger(1); id: $result->getString(0),
$this->rank = $result->getInteger(2); order: $result->getInteger(1),
$this->url = $result->getString(3); rank: $result->getInteger(2),
url: $result->getString(3),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -5,14 +5,18 @@ use Stringable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class EmoteStringInfo implements Stringable { class EmoteStringInfo implements Stringable {
private string $emoteId; public function __construct(
private int $order; private string $emoteId,
private string $string; private int $order,
private string $string,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): EmoteStringInfo {
$this->emoteId = (string)$result->getInteger(0); return new EmoteStringInfo(
$this->order = $result->getInteger(1); emoteId: $result->getString(0),
$this->string = $result->getString(2); order: $result->getInteger(1),
string: $result->getString(2),
);
} }
public function getEmoteId(): string { public function getEmoteId(): string {

View file

@ -31,7 +31,7 @@ class Emotes {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No emoticon with that ID exists.'); throw new RuntimeException('No emoticon with that ID exists.');
return new EmoteInfo($result); return EmoteInfo::fromResult($result);
} }
public static function emoteOrderOptions(): array { public static function emoteOrderOptions(): array {
@ -43,7 +43,7 @@ class Emotes {
?int $minRank = null, ?int $minRank = null,
?string $orderBy = null, ?string $orderBy = null,
?bool $reverse = null ?bool $reverse = null
): array { ): iterable {
$hasMinRank = $minRank !== null; $hasMinRank = $minRank !== null;
$hasOrderBy = $orderBy !== null; $hasOrderBy = $orderBy !== null;
$hasReverse = $reverse !== null; $hasReverse = $reverse !== null;
@ -65,13 +65,7 @@ class Emotes {
$stmt->addParameter(1, $minRank); $stmt->addParameter(1, $minRank);
$stmt->execute(); $stmt->execute();
$emotes = []; return $stmt->getResult()->getIterator(EmoteInfo::fromResult(...));
$result = $stmt->getResult();
while($result->next())
$emotes[] = new EmoteInfo($result);
return $emotes;
} }
private static function checkEmoteUrlInternal(string $url): string { private static function checkEmoteUrlInternal(string $url): string {
@ -156,7 +150,7 @@ class Emotes {
$stmt->execute(); $stmt->execute();
} }
public function getEmoteStrings(EmoteInfo|string $infoOrId): array { public function getEmoteStrings(EmoteInfo|string $infoOrId): iterable {
if($infoOrId instanceof EmoteInfo) if($infoOrId instanceof EmoteInfo)
$infoOrId = $infoOrId->getId(); $infoOrId = $infoOrId->getId();
@ -164,13 +158,7 @@ class Emotes {
$stmt->addParameter(1, $infoOrId); $stmt->addParameter(1, $infoOrId);
$stmt->execute(); $stmt->execute();
$strings = []; return $stmt->getResult()->getIterator(EmoteStringInfo::fromResult(...));
$result = $stmt->getResult();
while($result->next())
$strings[] = new EmoteStringInfo($result);
return $strings;
} }
private static function checkEmoteStringInternal(string $string): string { private static function checkEmoteStringInternal(string $string): string {

View file

@ -12,19 +12,20 @@ use Misuzu\Pagination;
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;
class ForumCategories { class ForumCategories {
private IDbConnection $dbConn;
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) { public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
public static function convertCategoryListToTree( public static function convertCategoryListToTree(
array $catInfos, iterable $catInfos,
ForumCategoryInfo|string|null $parentInfo = null, ForumCategoryInfo|string|null $parentInfo = null,
?Colour $colour = null ?Colour $colour = null
): array { ): array {
if(!is_array($catInfos))
$catInfos = iterator_to_array($catInfos);
$colour ??= Colour::none(); $colour ??= Colour::none();
$tree = []; $tree = [];
$predicate = $parentInfo $predicate = $parentInfo
@ -103,7 +104,7 @@ class ForumCategories {
?bool $hidden = null, ?bool $hidden = null,
bool $asTree = false, bool $asTree = false,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
$isRootParent = false; $isRootParent = false;
$hasParentInfo = $parentInfo !== false; $hasParentInfo = $parentInfo !== false;
$hasType = $type !== null; $hasType = $type !== null;
@ -155,11 +156,7 @@ class ForumCategories {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
$cats = [];
while($result->next())
$cats[] = new ForumCategoryInfo($result);
if($asTree) if($asTree)
$cats = self::convertCategoryListToTree($cats); $cats = self::convertCategoryListToTree($cats);
@ -216,7 +213,7 @@ class ForumCategories {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Forum category info not found.'); throw new RuntimeException('Forum category info not found.');
return new ForumCategoryInfo($result); return ForumCategoryInfo::fromResult($result);
} }
public function updateCategory( public function updateCategory(
@ -261,7 +258,7 @@ class ForumCategories {
public function getCategoryAncestry( public function getCategoryAncestry(
ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
): array { ): iterable {
if($categoryInfo instanceof ForumCategoryInfo) if($categoryInfo instanceof ForumCategoryInfo)
$categoryInfo = $categoryInfo->getId(); $categoryInfo = $categoryInfo->getId();
elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo) elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo)
@ -277,13 +274,7 @@ class ForumCategories {
$stmt->addParameter(1, $categoryInfo); $stmt->addParameter(1, $categoryInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
$cats = [];
while($result->next())
$cats[] = new ForumCategoryInfo($result);
return $cats;
} }
public function getCategoryChildren( public function getCategoryChildren(
@ -291,7 +282,7 @@ class ForumCategories {
bool $includeSelf = false, bool $includeSelf = false,
?bool $hidden = null, ?bool $hidden = null,
bool $asTree = false bool $asTree = false
): array { ): iterable {
if($parentInfo instanceof ForumCategoryInfo) if($parentInfo instanceof ForumCategoryInfo)
$parentInfo = $parentInfo->getId(); $parentInfo = $parentInfo->getId();
@ -319,11 +310,7 @@ class ForumCategories {
$stmt->addParameter(++$args, $parentInfo); $stmt->addParameter(++$args, $parentInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
$cats = [];
while($result->next())
$cats[] = new ForumCategoryInfo($result);
if($asTree) if($asTree)
$cats = self::convertCategoryListToTree($cats, $parentInfo); $cats = self::convertCategoryListToTree($cats, $parentInfo);

View file

@ -26,38 +26,42 @@ class ForumCategoryInfo {
self::TYPE_DISCUSSION, self::TYPE_DISCUSSION,
]; ];
private string $id; public function __construct(
private int $order; private string $id,
private ?string $parentId; private int $order,
private string $name; private ?string $parentId,
private int $type; private string $name,
private ?string $desc; private int $type,
private ?string $icon; private ?string $desc,
private ?int $colour; private ?string $icon,
private ?string $link; private ?int $colour,
private ?int $clicks; private ?string $link,
private int $created; private ?int $clicks,
private bool $archived; private int $created,
private bool $hidden; private bool $archived,
private int $topicsCount; private bool $hidden,
private int $postsCount; private int $topicsCount,
private int $postsCount,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ForumCategoryInfo {
$this->id = (string)$result->getInteger(0); return new ForumCategoryInfo(
$this->order = $result->getInteger(1); id: $result->getString(0),
$this->parentId = $result->isNull(2) ? null : (string)$result->getInteger(2); order: $result->getInteger(1),
$this->name = $result->getString(3); parentId: $result->getStringOrNull(2),
$this->type = $result->getInteger(4); name: $result->getString(3),
$this->desc = $result->isNull(5) ? null : $result->getString(5); type: $result->getInteger(4),
$this->icon = $result->isNull(6) ? null : $result->getString(6); desc: $result->getStringOrNull(5),
$this->colour = $result->isNull(7) ? null : $result->getInteger(7); icon: $result->getStringOrNull(6),
$this->link = $result->isNull(8) ? null : $result->getString(8); colour: $result->getIntegerOrNull(7),
$this->clicks = $result->isNull(9) ? null : $result->getInteger(9); link: $result->getStringOrNull(8),
$this->created = $result->getInteger(10); clicks: $result->getIntegerOrNull(9),
$this->archived = $result->getInteger(11) !== 0; created: $result->getInteger(10),
$this->hidden = $result->getInteger(12) !== 0; archived: $result->getBoolean(11),
$this->topicsCount = $result->getInteger(13); hidden: $result->getBoolean(12),
$this->postsCount = $result->getInteger(14); topicsCount: $result->getInteger(13),
postsCount: $result->getInteger(14),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -7,30 +7,34 @@ use Index\Net\IPAddress;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
class ForumPostInfo { class ForumPostInfo {
private string $id; public function __construct(
private string $topicId; private string $id,
private string $categoryId; private string $topicId,
private ?string $userId; private string $categoryId,
private string $remoteAddr; private ?string $userId,
private string $body; private string $remoteAddr,
private int $parser; private string $body,
private bool $displaySignature; private int $parser,
private int $created; private bool $displaySignature,
private ?int $edited; private int $created,
private ?int $deleted; private ?int $edited,
private ?int $deleted,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ForumPostInfo {
$this->id = (string)$result->getInteger(0); return new ForumPostInfo(
$this->topicId = (string)$result->getInteger(1); id: $result->getString(0),
$this->categoryId = (string)$result->getInteger(2); topicId: $result->getString(1),
$this->userId = $result->isNull(3) ? null : (string)$result->getInteger(3); categoryId: $result->getString(2),
$this->remoteAddr = $result->getString(4); userId: $result->getStringOrNull(3),
$this->body = $result->getString(5); remoteAddr: $result->getString(4),
$this->parser = $result->getInteger(6); body: $result->getString(5),
$this->displaySignature = $result->getInteger(7) !== 0; parser: $result->getInteger(6),
$this->created = $result->getInteger(8); displaySignature: $result->getBoolean(7),
$this->edited = $result->isNull(9) ? null : $result->getInteger(9); created: $result->getInteger(8),
$this->deleted = $result->isNull(10) ? null : $result->getInteger(10); edited: $result->getIntegerOrNull(9),
deleted: $result->getIntegerOrNull(10),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -84,7 +84,7 @@ class ForumPosts {
?array $searchQuery = null, ?array $searchQuery = null,
?bool $deleted = null, ?bool $deleted = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
// remove this hack when search server // remove this hack when search server
$hasSearchQuery = $searchQuery !== null; $hasSearchQuery = $searchQuery !== null;
$doSearchOrder = false; $doSearchOrder = false;
@ -189,13 +189,7 @@ class ForumPosts {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(ForumPostInfo::fromResult(...));
$posts = [];
while($result->next())
$posts[] = new ForumPostInfo($result);
return $posts;
} }
public function getPost( public function getPost(
@ -267,7 +261,7 @@ class ForumPosts {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Forum post not found.'); throw new RuntimeException('Forum post not found.');
return new ForumPostInfo($result); return ForumPostInfo::fromResult($result);
} }
public function createPost( public function createPost(

View file

@ -17,32 +17,36 @@ class ForumTopicInfo {
'global' => self::TYPE_GLOBAL, 'global' => self::TYPE_GLOBAL,
]; ];
private string $id; public function __construct(
private string $categoryId; private string $id,
private ?string $userId; private string $categoryId,
private int $type; private ?string $userId,
private string $title; private int $type,
private int $postsCount; private string $title,
private int $deletedPostsCount; private int $postsCount,
private int $viewsCount; private int $deletedPostsCount,
private int $created; private int $viewsCount,
private int $bumped; private int $created,
private ?int $deleted; private int $bumped,
private ?int $locked; private ?int $deleted,
private ?int $locked,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ForumTopicInfo {
$this->id = (string)$result->getInteger(0); return new ForumTopicInfo(
$this->categoryId = (string)$result->getInteger(1); id: $result->getString(0),
$this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2); categoryId: $result->getString(1),
$this->type = $result->getInteger(3); userId: $result->getStringOrNull(2),
$this->title = $result->getString(4); type: $result->getInteger(3),
$this->viewsCount = $result->getInteger(5); title: $result->getString(4),
$this->created = $result->getInteger(6); viewsCount: $result->getInteger(5),
$this->bumped = $result->getInteger(7); created: $result->getInteger(6),
$this->deleted = $result->isNull(8) ? null : $result->getInteger(8); bumped: $result->getInteger(7),
$this->locked = $result->isNull(9) ? null : $result->getInteger(9); deleted: $result->getIntegerOrNull(8),
$this->postsCount = $result->getInteger(10); locked: $result->getIntegerOrNull(9),
$this->deletedPostsCount = $result->getInteger(11); postsCount: $result->getInteger(10),
deletedPostsCount: $result->getInteger(11),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -5,16 +5,20 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ForumTopicRedirectInfo { class ForumTopicRedirectInfo {
private string $topicId; public function __construct(
private ?string $userId; private string $topicId,
private string $link; private ?string $userId,
private int $created; private string $link,
private int $created,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ForumTopicRedirectInfo {
$this->topicId = (string)$result->getInteger(0); return new ForumTopicRedirectInfo(
$this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1); topicId: $result->getString(0),
$this->link = $result->getString(2); userId: $result->getStringOrNull(1),
$this->created = $result->getInteger(3); link: $result->getString(2),
created: $result->getInteger(3),
);
} }
public function getTopicId(): string { public function getTopicId(): string {

View file

@ -38,7 +38,7 @@ class ForumTopicRedirects {
public function getTopicRedirects( public function getTopicRedirects(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
@ -61,13 +61,7 @@ class ForumTopicRedirects {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(ForumTopicRedirectInfo::fromResult(...));
$redirs = [];
while($result->next())
$redirs[] = new ForumTopicRedirectInfo($result);
return $redirs;
} }
public function hasTopicRedirect(ForumTopicInfo|string $topicInfo): bool { public function hasTopicRedirect(ForumTopicInfo|string $topicInfo): bool {
@ -97,7 +91,7 @@ class ForumTopicRedirects {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Could not find that forum topic redirect.'); throw new RuntimeException('Could not find that forum topic redirect.');
return new ForumTopicRedirectInfo($result); return ForumTopicRedirectInfo::fromResult($result);
} }
public function createTopicRedirect( public function createTopicRedirect(

View file

@ -91,7 +91,7 @@ class ForumTopics {
?bool $global = null, ?bool $global = null,
?bool $deleted = null, ?bool $deleted = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
// remove this hack when search server // remove this hack when search server
$hasSearchQuery = $searchQuery !== null; $hasSearchQuery = $searchQuery !== null;
$hasAfterTopicId = false; $hasAfterTopicId = false;
@ -194,13 +194,7 @@ class ForumTopics {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(ForumTopicInfo::fromResult(...));
$topics = [];
while($result->next())
$topics[] = new ForumTopicInfo($result);
return $topics;
} }
public function getTopic( public function getTopic(
@ -239,7 +233,7 @@ class ForumTopics {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Forum topic not found.'); throw new RuntimeException('Forum topic not found.');
return new ForumTopicInfo($result); return ForumTopicInfo::fromResult($result);
} }
public function createTopic( public function createTopic(

View file

@ -3,14 +3,10 @@ namespace Misuzu\Home;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\DateTime;
use Index\Data\DbTools; use Index\Data\{DbTools,IDbConnection};
use Index\Data\IDbConnection; use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Routing\Route;
use Index\Routing\RouteHandler;
use Syokuhou\IConfig; use Syokuhou\IConfig;
use Misuzu\Pagination; use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\SiteInfo;
use Misuzu\Template;
use Misuzu\Auth\AuthInfo; use Misuzu\Auth\AuthInfo;
use Misuzu\Changelog\Changelog; use Misuzu\Changelog\Changelog;
use Misuzu\Comments\Comments; use Misuzu\Comments\Comments;
@ -43,7 +39,7 @@ class HomeRoutes extends RouteHandler {
]); ]);
} }
private function getOnlineUsers(): array { private function getOnlineUsers(): iterable {
return $this->usersCtx->getUsers()->getUsers( return $this->usersCtx->getUsers()->getUsers(
lastActiveInMinutes: 5, lastActiveInMinutes: 5,
deleted: false, deleted: false,
@ -53,14 +49,14 @@ class HomeRoutes extends RouteHandler {
private array $newsCategoryInfos = []; private array $newsCategoryInfos = [];
private function getFeaturedNewsPosts(int $amount, bool $decorate): array { private function getFeaturedNewsPosts(int $amount, bool $decorate): iterable {
$postInfos = $this->news->getPosts( $postInfos = $this->news->getPosts(
onlyFeatured: true, onlyFeatured: true,
pagination: new Pagination($amount) pagination: new Pagination($amount)
); );
if(!$decorate) if(!$decorate)
return $postInfos; return iterator_to_array($postInfos);
$posts = []; $posts = [];
@ -154,7 +150,7 @@ class HomeRoutes extends RouteHandler {
return $topics; return $topics;
} }
#[Route('GET', '/')] #[HttpGet('/')]
#[URLInfo('index', '/')] #[URLInfo('index', '/')]
public function getIndex(...$args) { public function getIndex(...$args) {
return $this->authInfo->isLoggedIn() return $this->authInfo->isLoggedIn()
@ -164,9 +160,9 @@ class HomeRoutes extends RouteHandler {
public function getHome() { public function getHome() {
$stats = $this->getStats(); $stats = $this->getStats();
$onlineUserInfos = $this->getOnlineUsers(); $onlineUserInfos = iterator_to_array($this->getOnlineUsers());
$featuredNews = $this->getFeaturedNewsPosts(5, true); $featuredNews = $this->getFeaturedNewsPosts(5, true);
$changelog = $this->changelog->getChanges(pagination: new Pagination(10)); $changelog = iterator_to_array($this->changelog->getChanges(pagination: new Pagination(10)));
$stats['users:online:recent'] = count($onlineUserInfos); $stats['users:online:recent'] = count($onlineUserInfos);
@ -201,7 +197,7 @@ class HomeRoutes extends RouteHandler {
]); ]);
} }
#[Route('GET', '/_landing')] #[HttpGet('/_landing')]
public function getLanding($response, $request) { public function getLanding($response, $request) {
$config = $this->config->getValues([ $config = $this->config->getValues([
['social.embed_linked:b'], ['social.embed_linked:b'],
@ -221,7 +217,7 @@ class HomeRoutes extends RouteHandler {
} else $linkedData = null; } else $linkedData = null;
$stats = $this->getStats(); $stats = $this->getStats();
$onlineUserInfos = $this->getOnlineUsers(); $onlineUserInfos = iterator_to_array($this->getOnlineUsers());
$featuredNews = $this->getFeaturedNewsPosts(3, false); $featuredNews = $this->getFeaturedNewsPosts(3, false);
$popularTopics = $this->getPopularForumTopics($config['landing.forum_categories']); $popularTopics = $this->getPopularForumTopics($config['landing.forum_categories']);
$activeTopics = $this->getActiveForumTopics($config['landing.forum_categories']); $activeTopics = $this->getActiveForumTopics($config['landing.forum_categories']);

View file

@ -1,8 +1,7 @@
<?php <?php
namespace Misuzu\Info; namespace Misuzu\Info;
use Index\Routing\Route; use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Routing\RouteHandler;
use Misuzu\Template; use Misuzu\Template;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\URLs\URLInfo; use Misuzu\URLs\URLInfo;
@ -18,23 +17,16 @@ class InfoRoutes extends RouteHandler {
'index' => 'Index Project » %s', 'index' => 'Index Project » %s',
]; ];
private static function checkName(string $name): bool { #[HttpGet('/info')]
return preg_match('#^([A-Za-z0-9_]+)$#', $name) === 1;
}
#[Route('GET', '/info')]
#[URLInfo('info-index', '/info')] #[URLInfo('info-index', '/info')]
public function getIndex() { public function getIndex() {
return Template::renderRaw('info.index'); return Template::renderRaw('info.index');
} }
#[Route('GET', '/info/:name')] #[HttpGet('/info/([A-Za-z0-9_]+)')]
#[URLInfo('info', '/info/<title>')] #[URLInfo('info', '/info/<title>')]
#[URLInfo('info-doc', '/info/<title>')] #[URLInfo('info-doc', '/info/<title>')]
public function getDocsPage($response, $request, string $name) { public function getDocsPage($response, $request, string $name) {
if(!self::checkName($name))
return 404;
return $this->serveMarkdownDocument( return $this->serveMarkdownDocument(
sprintf('%s/%s.md', self::DOCS_PATH, $name) sprintf('%s/%s.md', self::DOCS_PATH, $name)
); );
@ -78,13 +70,11 @@ class InfoRoutes extends RouteHandler {
return ''; return '';
} }
#[Route('GET', '/info/:project/:name')] #[HttpGet('/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')]
#[URLInfo('info-project-doc', '/info/<project>/<title>')] #[URLInfo('info-project-doc', '/info/<project>/<title>')]
public function getProjectPage($response, $request, string $project, string $name) { public function getProjectPage($response, $request, string $project, string $name) {
if(!array_key_exists($project, self::PROJECT_PATHS)) if(!array_key_exists($project, self::PROJECT_PATHS))
return 404; return 404;
if(!self::checkName($name))
return 404;
$projectPath = self::PROJECT_PATHS[$project]; $projectPath = self::PROJECT_PATHS[$project];
$titleSuffix = array_key_exists($project, self::PROJECT_SUFFIXES) ? self::PROJECT_SUFFIXES[$project] : ''; $titleSuffix = array_key_exists($project, self::PROJECT_SUFFIXES) ? self::PROJECT_SUFFIXES[$project] : '';

View file

@ -1,11 +1,8 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Index\Routing\Route; use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Routing\RouteHandler; use Misuzu\URLs\{IURLSource,URLInfo,URLRegistry};
use Misuzu\URLs\IURLSource;
use Misuzu\URLs\URLInfo;
use Misuzu\URLs\URLRegistry;
class LegacyRoutes extends RouteHandler implements IURLSource { class LegacyRoutes extends RouteHandler implements IURLSource {
public function __construct( public function __construct(
@ -124,27 +121,27 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
$urls->register('manage-role', '/manage/users/role.php', ['r' => '<role>']); $urls->register('manage-role', '/manage/users/role.php', ['r' => '<role>']);
} }
#[Route('GET', '/index.php')] #[HttpGet('/index.php')]
public function getIndexPHP($response): void { public function getIndexPHP($response): void {
$response->redirect($this->urls->format('index'), true); $response->redirect($this->urls->format('index'), true);
} }
#[Route('GET', '/info.php')] #[HttpGet('/info.php')]
public function getInfoPHP($response): void { public function getInfoPHP($response): void {
$response->redirect($this->urls->format('info'), true); $response->redirect($this->urls->format('info'), true);
} }
#[Route('GET', '/info.php/:name')] #[HttpGet('/info.php/([A-Za-z0-9_]+)')]
public function getInfoDocsPHP($response, $request, string $name): void { public function getInfoDocsPHP($response, $request, string $name): void {
$response->redirect($this->urls->format('info', ['title' => $name]), true); $response->redirect($this->urls->format('info', ['title' => $name]), true);
} }
#[Route('GET', '/info.php/:project/:name')] #[HttpGet('/info.php/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')]
public function getInfoProjectPHP($response, $request, string $project, string $name): void { public function getInfoProjectPHP($response, $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);
} }
#[Route('GET', '/news.php')] #[HttpGet('/news.php')]
public function getNewsPHP($response, $request): void { public function getNewsPHP($response, $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));
@ -159,28 +156,28 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
$response->redirect($location, true); $response->redirect($location, true);
} }
#[Route('GET', '/news.php/rss')] #[HttpGet('/news.php/rss')]
public function getNewsRssPHP($response, $request): void { public function getNewsRssPHP($response, $request): void {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT); $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = $this->urls->format($catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss', ['category' => $catId]); $location = $this->urls->format($catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss', ['category' => $catId]);
$response->redirect($location, true); $response->redirect($location, true);
} }
#[Route('GET', '/news.php/atom')] #[HttpGet('/news.php/atom')]
public function getNewsAtomPHP($response, $request): void { public function getNewsAtomPHP($response, $request): void {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT); $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = $this->urls->format($catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom', ['category' => $catId]); $location = $this->urls->format($catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom', ['category' => $catId]);
$response->redirect($location, true); $response->redirect($location, true);
} }
#[Route('GET', '/news/index.php')] #[HttpGet('/news/index.php')]
public function getNewsIndexPHP($response, $request): void { public function getNewsIndexPHP($response, $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);
} }
#[Route('GET', '/news/category.php')] #[HttpGet('/news/category.php')]
public function getNewsCategoryPHP($response, $request): void { public function getNewsCategoryPHP($response, $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),
@ -188,19 +185,19 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
]), true); ]), true);
} }
#[Route('GET', '/news/post.php')] #[HttpGet('/news/post.php')]
public function getNewsPostPHP($response, $request): void { public function getNewsPostPHP($response, $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);
} }
#[Route('GET', '/news/feed.php')] #[HttpGet('/news/feed.php')]
public function getNewsFeedPHP($response, $request): int { public function getNewsFeedPHP($response, $request): int {
return 400; return 400;
} }
#[Route('GET', '/news/feed.php/rss')] #[HttpGet('/news/feed.php/rss')]
public function getNewsFeedRssPHP($response, $request): void { public function getNewsFeedRssPHP($response, $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(
@ -209,7 +206,7 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
), true); ), true);
} }
#[Route('GET', '/news/feed.php/atom')] #[HttpGet('/news/feed.php/atom')]
public function getNewsFeedAtomPHP($response, $request): void { public function getNewsFeedAtomPHP($response, $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(
@ -218,7 +215,7 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
), true); ), true);
} }
#[Route('GET', '/changelog.php')] #[HttpGet('/changelog.php')]
public function getChangelogPHP($response, $request): void { public function getChangelogPHP($response, $request): void {
$changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT); $changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
if($changeId) { if($changeId) {
@ -232,7 +229,7 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
]), true); ]), true);
} }
#[Route('GET', '/auth.php')] #[HttpGet('/auth.php')]
public function getAuthPHP($response, $request): void { public function getAuthPHP($response, $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',
@ -243,44 +240,44 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
}), true); }), true);
} }
#[Route('GET', '/auth')] #[HttpGet('/auth')]
#[Route('GET', '/auth/index.php')] #[HttpGet('/auth/index.php')]
public function getAuthIndexPHP($response, $request): void { public function getAuthIndexPHP($response, $request): void {
$response->redirect($this->urls->format('auth-login'), true); $response->redirect($this->urls->format('auth-login'), true);
} }
#[Route('GET', '/settings.php')] #[HttpGet('/settings.php')]
public function getSettingsPHP($response): void { public function getSettingsPHP($response): void {
$response->redirect($this->urls->format('settings-index'), true); $response->redirect($this->urls->format('settings-index'), true);
} }
#[Route('GET', '/settings')] #[HttpGet('/settings')]
public function getSettingsIndex($response): void { public function getSettingsIndex($response): void {
$response->redirect($this->urls->format('settings-account')); $response->redirect($this->urls->format('settings-account'));
} }
#[Route('GET', '/settings/index.php')] #[HttpGet('/settings/index.php')]
public function getSettingsIndexPHP($response): void { public function getSettingsIndexPHP($response): void {
$response->redirect($this->urls->format('settings-account'), true); $response->redirect($this->urls->format('settings-account'), true);
} }
#[Route('GET', '/manage')] #[HttpGet('/manage')]
#[URLInfo('manage-index', '/manage')] #[URLInfo('manage-index', '/manage')]
public function getManageIndex($response): void { public function getManageIndex($response): void {
$response->redirect($this->urls->format('manage-general-overview')); $response->redirect($this->urls->format('manage-general-overview'));
} }
#[Route('GET', '/manage/index.php')] #[HttpGet('/manage/index.php')]
public function getManageIndexPHP($response): void { public function getManageIndexPHP($response): void {
$response->redirect($this->urls->format('manage-general-overview'), true); $response->redirect($this->urls->format('manage-general-overview'), true);
} }
#[Route('GET', '/manage/news')] #[HttpGet('/manage/news')]
public function getManageNewsIndex($response): void { public function getManageNewsIndex($response): void {
$response->redirect($this->urls->format('manage-news-categories')); $response->redirect($this->urls->format('manage-news-categories'));
} }
#[Route('GET', '/manage/news/index.php')] #[HttpGet('/manage/news/index.php')]
public function getManageNewsIndexPHP($response): void { public function getManageNewsIndexPHP($response): void {
$response->redirect($this->urls->format('manage-news-categories'), true); $response->redirect($this->urls->format('manage-news-categories'), true);
} }

View file

@ -6,32 +6,36 @@ use Index\Data\IDbResult;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
class MessageInfo { class MessageInfo {
private string $messageId; public function __construct(
private string $ownerId; private string $messageId,
private ?string $authorId; private string $ownerId,
private ?string $recipientId; private ?string $authorId,
private ?string $replyTo; private ?string $recipientId,
private string $title; private ?string $replyTo,
private string $body; private string $title,
private int $parser; private string $body,
private int $created; private int $parser,
private ?int $sent; private int $created,
private ?int $read; private ?int $sent,
private ?int $deleted; private ?int $read,
private ?int $deleted,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): MessageInfo {
$this->messageId = $result->getString(0); return new MessageInfo(
$this->ownerId = $result->getString(1); messageId: $result->getString(0),
$this->authorId = $result->getStringOrNull(2); ownerId: $result->getString(1),
$this->recipientId = $result->getStringOrNull(3); authorId: $result->getStringOrNull(2),
$this->replyTo = $result->getStringOrNull(4); recipientId: $result->getStringOrNull(3),
$this->title = $result->getString(5); replyTo: $result->getStringOrNull(4),
$this->body = $result->getString(6); title: $result->getString(5),
$this->parser = $result->getInteger(7); body: $result->getString(6),
$this->created = $result->getInteger(8); parser: $result->getInteger(7),
$this->sent = $result->getIntegerOrNull(9); created: $result->getInteger(8),
$this->read = $result->getIntegerOrNull(10); sent: $result->getIntegerOrNull(9),
$this->deleted = $result->getIntegerOrNull(11); read: $result->getIntegerOrNull(10),
deleted: $result->getIntegerOrNull(11),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -11,7 +11,7 @@ use Misuzu\Users\UserInfo;
class MessagesDatabase { class MessagesDatabase {
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(private IDbConnection $dbConn) { public function __construct(IDbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
@ -92,7 +92,7 @@ class MessagesDatabase {
?bool $read = null, ?bool $read = null,
?bool $deleted = null, ?bool $deleted = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
$hasOwnerInfo = $ownerInfo !== null; $hasOwnerInfo = $ownerInfo !== null;
$hasAuthorInfo = $authorInfo !== null; $hasAuthorInfo = $authorInfo !== null;
$hasRecipientInfo = $recipientInfo !== null; $hasRecipientInfo = $recipientInfo !== null;
@ -104,7 +104,7 @@ class MessagesDatabase {
$hasPagination = $pagination !== null; $hasPagination = $pagination !== null;
$args = 0; $args = 0;
$query = 'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages'; $query = 'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, FROM_BASE64(msg_title), FROM_BASE64(msg_body), msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages';
if($hasOwnerInfo) { if($hasOwnerInfo) {
++$args; ++$args;
$query .= ' WHERE msg_owner_id = ?'; $query .= ' WHERE msg_owner_id = ?';
@ -153,13 +153,7 @@ class MessagesDatabase {
$stmt->execute(); $stmt->execute();
$infos = []; return $stmt->getResult()->getIterator(MessageInfo::fromResult(...));
$result = $stmt->getResult();
while($result->next())
$infos[] = new MessageInfo($result);
return $infos;
} }
public function getMessageInfo( public function getMessageInfo(
@ -168,7 +162,7 @@ class MessagesDatabase {
bool $useReplyTo = false bool $useReplyTo = false
): MessageInfo { ): MessageInfo {
$stmt = $this->cache->get(sprintf( $stmt = $this->cache->get(sprintf(
'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages WHERE msg_id = %s AND msg_owner_id = ?', 'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, FROM_BASE64(msg_title), FROM_BASE64(msg_body), msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages WHERE msg_id = %s AND msg_owner_id = ?',
!$useReplyTo || $messageInfoOrId instanceof MessageInfo ? '?' : '(SELECT msg_reply_to FROM msz_messages WHERE msg_id = ?)' !$useReplyTo || $messageInfoOrId instanceof MessageInfo ? '?' : '(SELECT msg_reply_to FROM msz_messages WHERE msg_id = ?)'
)); ));
@ -183,7 +177,7 @@ class MessagesDatabase {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Message not found.'); throw new RuntimeException('Message not found.');
return new MessageInfo($result); return MessageInfo::fromResult($result);
} }
public function createMessage( public function createMessage(
@ -198,7 +192,7 @@ class MessagesDatabase {
DateTime|int|null $sentAt = null, DateTime|int|null $sentAt = null,
DateTime|int|null $readAt = null DateTime|int|null $readAt = null
): MessageInfo { ): MessageInfo {
$stmt = $this->cache->get('INSERT INTO msz_messages (msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, msg_sent, msg_read) VALUES (?, ?, ?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?))'); $stmt = $this->cache->get('INSERT INTO msz_messages (msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, msg_sent, msg_read) VALUES (?, ?, ?, ?, ?, TO_BASE64(?), TO_BASE64(?), ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?))');
$stmt->addParameter(1, $messageId); $stmt->addParameter(1, $messageId);
$stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo); $stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
$stmt->addParameter(3, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo); $stmt->addParameter(3, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo);
@ -239,12 +233,12 @@ class MessagesDatabase {
} }
if($title !== null) { if($title !== null) {
$setQuery[] = 'msg_title = ?'; $setQuery[] = 'msg_title = TO_BASE64(?)';
$setValues[] = $title; $setValues[] = $title;
} }
if($body !== null) { if($body !== null) {
$setQuery[] = 'msg_body = ?'; $setQuery[] = 'msg_body = TO_BASE64(?)';
$setValues[] = $body; $setValues[] = $body;
} }

View file

@ -5,7 +5,7 @@ use stdClass;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\XString; use Index\XString;
use Index\Routing\{Route,RouteHandler}; use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler};
use Syokuhou\IConfig; use Syokuhou\IConfig;
use Misuzu\{CSRF,Pagination,Perm,Template}; use Misuzu\{CSRF,Pagination,Perm,Template};
use Misuzu\Auth\AuthInfo; use Misuzu\Auth\AuthInfo;
@ -33,12 +33,16 @@ class MessagesRoutes extends RouteHandler {
private bool $canSendMessages; private bool $canSendMessages;
#[Route('/messages')] #[HttpMiddleware('/messages')]
public function checkAccess($response, $request) { public function checkAccess($response, $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;
// do not allow access to PMs when impersonating in production mode
if(!MSZ_DEBUG && $this->authInfo->isImpersonating())
return 403;
$globalPerms = $this->authInfo->getPerms('global'); $globalPerms = $this->authInfo->getPerms('global');
if(!$globalPerms->check(Perm::G_MESSAGES_VIEW)) if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
return 403; return 403;
@ -72,7 +76,7 @@ class MessagesRoutes extends RouteHandler {
return $message; return $message;
} }
#[Route('GET', '/messages')] #[HttpGet('/messages')]
#[URLInfo('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])] #[URLInfo('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])]
public function getIndex($response, $request, string $folderName = '') { public function getIndex($response, $request, string $folderName = '') {
$folderName = (string)$request->getParam('folder'); $folderName = (string)$request->getParam('folder');
@ -125,7 +129,7 @@ class MessagesRoutes extends RouteHandler {
]); ]);
} }
#[Route('GET', '/messages/stats')] #[HttpGet('/messages/stats')]
#[URLInfo('messages-stats', '/messages/stats')] #[URLInfo('messages-stats', '/messages/stats')]
public function getStats() { public function getStats() {
$selfInfo = $this->authInfo->getUserInfo(); $selfInfo = $this->authInfo->getUserInfo();
@ -142,7 +146,7 @@ class MessagesRoutes extends RouteHandler {
]; ];
} }
#[Route('POST', '/messages/recipient')] #[HttpPost('/messages/recipient')]
#[URLInfo('messages-recipient', '/messages/recipient')] #[URLInfo('messages-recipient', '/messages/recipient')]
public function postRecipient($response, $request) { public function postRecipient($response, $request) {
if(!$request->isFormContent()) if(!$request->isFormContent())
@ -182,7 +186,7 @@ class MessagesRoutes extends RouteHandler {
]; ];
} }
#[Route('GET', '/messages/compose')] #[HttpGet('/messages/compose')]
#[URLInfo('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])] #[URLInfo('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])]
public function getEditor($response, $request) { public function getEditor($response, $request) {
if(!$this->canSendMessages) if(!$this->canSendMessages)
@ -193,7 +197,7 @@ class MessagesRoutes extends RouteHandler {
]); ]);
} }
#[Route('GET', '/messages/:message')] #[HttpGet('/messages/([A-Za-z0-9]+)')]
#[URLInfo('messages-view', '/messages/<message>')] #[URLInfo('messages-view', '/messages/<message>')]
public function getView($response, $request, string $messageId) { public function getView($response, $request, string $messageId) {
if(strlen($messageId) !== 8) if(strlen($messageId) !== 8)
@ -323,7 +327,7 @@ class MessagesRoutes extends RouteHandler {
return null; return null;
} }
#[Route('POST', '/messages/create')] #[HttpPost('/messages/create')]
#[URLInfo('messages-create', '/messages/create')] #[URLInfo('messages-create', '/messages/create')]
public function postCreate($response, $request) { public function postCreate($response, $request) {
if(!$request->isFormContent()) if(!$request->isFormContent())
@ -427,7 +431,7 @@ class MessagesRoutes extends RouteHandler {
]; ];
} }
#[Route('POST', '/messages/:message')] #[HttpPost('/messages/([A-Za-z0-9]+)')]
#[URLInfo('messages-update', '/messages/<message>')] #[URLInfo('messages-update', '/messages/<message>')]
public function postUpdate($response, $request, string $messageId) { public function postUpdate($response, $request, string $messageId) {
if(!$request->isFormContent()) if(!$request->isFormContent())
@ -518,7 +522,7 @@ class MessagesRoutes extends RouteHandler {
]; ];
} }
#[Route('POST', '/messages/mark')] #[HttpPost('/messages/mark')]
#[URLInfo('messages-mark', '/messages/mark')] #[URLInfo('messages-mark', '/messages/mark')]
public function postMark($response, $request) { public function postMark($response, $request) {
if(!$request->isFormContent()) if(!$request->isFormContent())
@ -549,16 +553,16 @@ class MessagesRoutes extends RouteHandler {
return []; return [];
} }
#[Route('POST', '/messages/delete')] #[HttpPost('/messages/delete')]
#[URLInfo('messages-delete', '/messages/delete')] #[URLInfo('messages-delete', '/messages/delete')]
public function postDelete($response, $request) { public function postDelete($response, $request) {
if(!$request->isFormContent()) if(!$request->isFormContent())
return 400; return 400;
$content = $request->getContent(); $content = $request->getContent();
$messages = explode(',', (string)$content->getParam('messages'));
if(empty($messages)) $messages = (string)$content->getParam('messages');
if($messages === '')
return [ return [
'error' => [ 'error' => [
'name' => 'msgs:empty', 'name' => 'msgs:empty',
@ -566,6 +570,8 @@ class MessagesRoutes extends RouteHandler {
], ],
]; ];
$messages = explode(',', $messages);
$this->msgsCtx->getDatabase()->deleteMessages( $this->msgsCtx->getDatabase()->deleteMessages(
$this->authInfo->getUserInfo(), $this->authInfo->getUserInfo(),
$messages $messages
@ -574,16 +580,16 @@ class MessagesRoutes extends RouteHandler {
return []; return [];
} }
#[Route('POST', '/messages/restore')] #[HttpPost('/messages/restore')]
#[URLInfo('messages-restore', '/messages/restore')] #[URLInfo('messages-restore', '/messages/restore')]
public function postRestore($response, $request) { public function postRestore($response, $request) {
if(!$request->isFormContent()) if(!$request->isFormContent())
return 400; return 400;
$content = $request->getContent(); $content = $request->getContent();
$messages = explode(',', (string)$content->getParam('messages'));
if(empty($messages)) $messages = (string)$content->getParam('messages');
if($messages === '')
return [ return [
'error' => [ 'error' => [
'name' => 'msgs:empty', 'name' => 'msgs:empty',
@ -591,6 +597,8 @@ class MessagesRoutes extends RouteHandler {
], ],
]; ];
$messages = explode(',', $messages);
$this->msgsCtx->getDatabase()->restoreMessages( $this->msgsCtx->getDatabase()->restoreMessages(
$this->authInfo->getUserInfo(), $this->authInfo->getUserInfo(),
$messages $messages
@ -599,16 +607,16 @@ class MessagesRoutes extends RouteHandler {
return []; return [];
} }
#[Route('POST', '/messages/nuke')] #[HttpPost('/messages/nuke')]
#[URLInfo('messages-nuke', '/messages/nuke')] #[URLInfo('messages-nuke', '/messages/nuke')]
public function postNuke($response, $request) { public function postNuke($response, $request) {
if(!$request->isFormContent()) if(!$request->isFormContent())
return 400; return 400;
$content = $request->getContent(); $content = $request->getContent();
$messages = explode(',', (string)$content->getParam('messages'));
if(empty($messages)) $messages = (string)$content->getParam('messages');
if($messages === '')
return [ return [
'error' => [ 'error' => [
'name' => 'msgs:empty', 'name' => 'msgs:empty',
@ -616,6 +624,8 @@ class MessagesRoutes extends RouteHandler {
], ],
]; ];
$messages = explode(',', $messages);
$this->msgsCtx->getDatabase()->nukeMessages( $this->msgsCtx->getDatabase()->nukeMessages(
$this->authInfo->getUserInfo(), $this->authInfo->getUserInfo(),
$messages $messages

View file

@ -212,7 +212,6 @@ class MisuzuContext {
$routingCtx = new RoutingContext(); $routingCtx = new RoutingContext();
$this->urls = $routingCtx->getURLs(); $this->urls = $routingCtx->getURLs();
$routingCtx->registerDefaultErrorPages();
$routingCtx->register(new \Misuzu\Home\HomeRoutes( $routingCtx->register(new \Misuzu\Home\HomeRoutes(
$this->config, $this->config,
@ -263,6 +262,7 @@ class MisuzuContext {
$routingCtx->register(new \Misuzu\SharpChat\SharpChatRoutes( $routingCtx->register(new \Misuzu\SharpChat\SharpChatRoutes(
$this->config->scopeTo('sockChat'), $this->config->scopeTo('sockChat'),
$this->config->scopeTo('impersonate'),
$this->urls, $this->urls,
$this->usersCtx, $this->usersCtx,
$this->authCtx, $this->authCtx,

View file

@ -41,7 +41,7 @@ class News {
public function getCategories( public function getCategories(
?bool $hidden = null, ?bool $hidden = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
$hasHidden = $hidden !== null; $hasHidden = $hidden !== null;
$hasPagination = $pagination !== null; $hasPagination = $pagination !== null;
@ -61,13 +61,7 @@ class News {
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(NewsCategoryInfo::fromResult(...));
$categories = [];
while($result->next())
$categories[] = new NewsCategoryInfo($result);
return $categories;
} }
public function getCategory( public function getCategory(
@ -107,7 +101,7 @@ class News {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('News category not found.'); throw new RuntimeException('News category not found.');
return new NewsCategoryInfo($result); return NewsCategoryInfo::fromResult($result);
} }
public function createCategory( public function createCategory(
@ -224,7 +218,7 @@ class News {
bool $includeScheduled = false, bool $includeScheduled = false,
bool $includeDeleted = false, bool $includeDeleted = false,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($categoryInfo instanceof NewsCategoryInfo) if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId(); $categoryInfo = $categoryInfo->getId();
@ -271,13 +265,7 @@ class News {
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(NewsPostInfo::fromResult(...));
$posts = [];
while($result->next())
$posts[] = new NewsPostInfo($result);
return $posts;
} }
public function getPost(string $postId): NewsPostInfo { public function getPost(string $postId): NewsPostInfo {
@ -289,7 +277,7 @@ class News {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No news post with that ID exists.'); throw new RuntimeException('No news post with that ID exists.');
return new NewsPostInfo($result); return NewsPostInfo::fromResult($result);
} }
public function createPost( public function createPost(

View file

@ -5,20 +5,24 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class NewsCategoryInfo { class NewsCategoryInfo {
private string $id; public function __construct(
private string $name; private string $id,
private string $description; private string $name,
private bool $hidden; private string $description,
private int $created; private bool $hidden,
private int $posts; private int $created,
private int $posts,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): NewsCategoryInfo {
$this->id = (string)$result->getInteger(0); return new NewsCategoryInfo(
$this->name = $result->getString(1); id: $result->getString(0),
$this->description = $result->getString(2); name: $result->getString(1),
$this->hidden = $result->getInteger(3) !== 0; description: $result->getString(2),
$this->created = $result->getInteger(4); hidden: $result->getBoolean(3),
$this->posts = $result->getInteger(5); created: $result->getInteger(4),
posts: $result->getInteger(5),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -5,30 +5,34 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class NewsPostInfo { class NewsPostInfo {
private string $id; public function __construct(
private string $categoryId; private string $id,
private ?string $userId; private string $categoryId,
private ?string $commentsSectionId; private ?string $userId,
private bool $featured; private ?string $commentsSectionId,
private string $title; private bool $featured,
private string $body; private string $title,
private int $scheduled; private string $body,
private int $created; private int $scheduled,
private int $updated; private int $created,
private ?int $deleted; private int $updated,
private ?int $deleted,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): NewsPostInfo {
$this->id = (string)$result->getInteger(0); return new NewsPostInfo(
$this->categoryId = (string)$result->getInteger(1); id: $result->getString(0),
$this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2); categoryId: $result->getString(1),
$this->commentsSectionId = $result->isNull(3) ? null : (string)$result->getInteger(3); userId: $result->getStringOrNull(2),
$this->featured = $result->getInteger(4) !== 0; commentsSectionId: $result->getStringOrNull(3),
$this->title = $result->getString(5); featured: $result->getBoolean(4),
$this->body = $result->getString(6); title: $result->getString(5),
$this->scheduled = $result->getInteger(7); body: $result->getString(6),
$this->created = $result->getInteger(8); scheduled: $result->getInteger(7),
$this->updated = $result->getInteger(9); created: $result->getInteger(8),
$this->deleted = $result->isNull(10) ? null : $result->getInteger(10); updated: $result->getInteger(9),
deleted: $result->getIntegerOrNull(10),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -3,24 +3,14 @@ namespace Misuzu\News;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\DateTime;
use Index\Data\DbTools; use Index\Data\{DbTools,IDbConnection};
use Index\Data\IDbConnection; use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Routing\Route; use Misuzu\{Pagination,SiteInfo,Template};
use Index\Routing\RouteHandler;
use Misuzu\Pagination;
use Misuzu\SiteInfo;
use Misuzu\Template;
use Misuzu\Auth\AuthInfo; use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\Comments; use Misuzu\Comments\{Comments,CommentsCategory,CommentsEx};
use Misuzu\Comments\CommentsCategory; use Misuzu\Feeds\{Feed,FeedItem,AtomFeedSerializer,RssFeedSerializer};
use Misuzu\Comments\CommentsEx;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\URLs\URLInfo; use Misuzu\URLs\{URLInfo,URLRegistry};
use Misuzu\URLs\URLRegistry;
use Misuzu\Users\UsersContext; use Misuzu\Users\UsersContext;
class NewsRoutes extends RouteHandler { class NewsRoutes extends RouteHandler {
@ -91,7 +81,7 @@ class NewsRoutes extends RouteHandler {
return $posts; return $posts;
} }
#[Route('GET', '/news')] #[HttpGet('/news')]
#[URLInfo('news-index', '/news', ['p' => '<page>'])] #[URLInfo('news-index', '/news', ['p' => '<page>'])]
public function getIndex() { public function getIndex() {
$categories = $this->news->getCategories(hidden: false); $categories = $this->news->getCategories(hidden: false);
@ -109,24 +99,21 @@ class NewsRoutes extends RouteHandler {
]); ]);
} }
#[Route('GET', '/news.rss')] #[HttpGet('/news.rss')]
#[URLInfo('news-feed-rss', '/news.rss')] #[URLInfo('news-feed-rss', '/news.rss')]
public function getFeedRss($response) { public function getFeedRss($response) {
return $this->getFeed($response, 'rss'); return $this->getFeed($response, 'rss');
} }
#[Route('GET', '/news.atom')] #[HttpGet('/news.atom')]
#[URLInfo('news-feed-atom', '/news.atom')] #[URLInfo('news-feed-atom', '/news.atom')]
public function getFeedAtom($response) { public function getFeedAtom($response) {
return $this->getFeed($response, 'atom'); return $this->getFeed($response, 'atom');
} }
#[Route('GET', '/news/:category')] #[HttpGet('/news/([0-9]+)(?:\.(rss|atom))?')]
#[URLInfo('news-category', '/news/<category>', ['p' => '<page>'])] #[URLInfo('news-category', '/news/<category>', ['p' => '<page>'])]
public function getCategory($response, $request, string $fileName) { public function getCategory($response, $request, string $categoryId, string $type = '') {
$categoryId = pathinfo($fileName, PATHINFO_FILENAME);
$type = pathinfo($fileName, PATHINFO_EXTENSION);
try { try {
$categoryInfo = $this->news->getCategory(categoryId: $categoryId); $categoryInfo = $this->news->getCategory(categoryId: $categoryId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
@ -163,7 +150,7 @@ class NewsRoutes extends RouteHandler {
return $this->getFeed($response, 'atom', $categoryInfo); return $this->getFeed($response, 'atom', $categoryInfo);
} }
#[Route('GET', '/news/post/:id')] #[HttpGet('/news/post/([0-9]+)')]
#[URLInfo('news-post', '/news/post/<post>')] #[URLInfo('news-post', '/news/post/<post>')]
#[URLInfo('news-post-comments', '/news/post/<post>', fragment: 'comments')] #[URLInfo('news-post-comments', '/news/post/<post>', fragment: 'comments')]
public function getPost($response, $request, string $postId) { public function getPost($response, $request, string $postId) {

View file

@ -6,24 +6,30 @@ use Index\Data\IDbResult;
class PermissionInfo implements IPermissionResult { class PermissionInfo implements IPermissionResult {
use PermissionResultShared; use PermissionResultShared;
private ?string $userId;
private ?string $roleId;
private ?string $forumCategoryId;
private string $category;
private int $allow;
private int $deny;
private int $calculated; private int $calculated;
public function __construct(IDbResult $result) { public function __construct(
$this->userId = $result->isNull(0) ? null : $result->getString(0); private ?string $userId,
$this->roleId = $result->isNull(1) ? null : $result->getString(1); private ?string $roleId,
$this->forumCategoryId = $result->isNull(2) ? null : $result->getString(2); private ?string $forumCategoryId,
$this->category = $result->getString(3); private string $category,
$this->allow = $result->getInteger(4); private int $allow,
$this->deny = $result->getInteger(5); private int $deny,
) {
$this->calculated = $this->allow & ~$this->deny; $this->calculated = $this->allow & ~$this->deny;
} }
public static function fromResult(IDbResult $result): PermissionInfo {
return new PermissionInfo(
userId: $result->getStringOrNull(0),
roleId: $result->getStringOrNull(1),
forumCategoryId: $result->getStringOrNull(2),
category: $result->getString(3),
allow: $result->getInteger(4),
deny: $result->getInteger(5),
);
}
public function hasUserId(): bool { public function hasUserId(): bool {
return $this->userId !== null; return $this->userId !== null;
} }

View file

@ -76,11 +76,11 @@ class Permissions {
$result = $stmt->getResult(); $result = $stmt->getResult();
if(is_string($categoryNames)) if(is_string($categoryNames))
return $result->next() ? new PermissionInfo($result) : null; return $result->next() ? PermissionInfo::fromResult($result) : null;
$perms = []; $perms = [];
while($result->next()) while($result->next())
$perms[$result->getString(3)] = new PermissionInfo($result); $perms[$result->getString(3)] = PermissionInfo::fromResult($result);
return $perms; return $perms;
} }

View file

@ -4,18 +4,22 @@ namespace Misuzu\Profile;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ProfileFieldFormatInfo { class ProfileFieldFormatInfo {
private string $id; public function __construct(
private string $fieldId; private string $id,
private ?string $regex; private string $fieldId,
private ?string $linkFormat; private ?string $regex,
private string $displayFormat; private ?string $linkFormat,
private string $displayFormat,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ProfileFieldFormatInfo {
$this->id = (string)$result->getInteger(0); return new ProfileFieldFormatInfo(
$this->fieldId = (string)$result->getInteger(1); id: $result->getString(0),
$this->regex = $result->isNull(2) ? null : $result->getString(2); fieldId: $result->getString(1),
$this->linkFormat = $result->isNull(3) ? null : $result->getString(3); regex: $result->getStringOrNull(2),
$this->displayFormat = $result->getString(4); linkFormat: $result->getStringOrNull(3),
displayFormat: $result->getString(4),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -4,18 +4,22 @@ namespace Misuzu\Profile;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ProfileFieldInfo { class ProfileFieldInfo {
private string $id; public function __construct(
private int $order; private string $id,
private string $name; private int $order,
private string $title; private string $name,
private string $regex; private string $title,
private string $regex,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ProfileFieldInfo {
$this->id = (string)$result->getInteger(0); return new ProfileFieldInfo(
$this->order = $result->getInteger(1); id: $result->getString(0),
$this->name = $result->getString(2); order: $result->getInteger(1),
$this->title = $result->getString(3); name: $result->getString(2),
$this->regex = $result->getString(4); title: $result->getString(3),
regex: $result->getString(4),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -4,16 +4,20 @@ namespace Misuzu\Profile;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ProfileFieldValueInfo { class ProfileFieldValueInfo {
private string $fieldId; public function __construct(
private string $userId; private string $fieldId,
private string $formatId; private string $userId,
private string $value; private string $formatId,
private string $value,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ProfileFieldValueInfo {
$this->fieldId = (string)$result->getInteger(0); return new ProfileFieldValueInfo(
$this->userId = (string)$result->getInteger(1); fieldId: $result->getString(0),
$this->formatId = (string)$result->getInteger(2); userId: $result->getString(1),
$this->value = $result->getString(3); formatId: $result->getString(2),
value: $result->getString(3)
);
} }
public function getFieldId(): string { public function getFieldId(): string {

View file

@ -16,10 +16,15 @@ class ProfileFields {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
public function getFields(?array $fieldValueInfos = null): array { public function getFields(?iterable $fieldValueInfos = null): iterable {
$hasFieldValueInfos = $fieldValueInfos !== null; $hasFieldValueInfos = $fieldValueInfos !== null;
if($hasFieldValueInfos && empty($fieldValueInfos)) if($hasFieldValueInfos) {
return []; if(!is_array($fieldValueInfos))
$fieldValueInfos = iterator_to_array($fieldValueInfos);
if(empty($fieldValueInfos))
return [];
}
$query = 'SELECT field_id, field_order, field_key, field_title, field_regex FROM msz_profile_fields'; $query = 'SELECT field_id, field_order, field_key, field_title, field_regex FROM msz_profile_fields';
if($hasFieldValueInfos) if($hasFieldValueInfos)
@ -37,13 +42,8 @@ class ProfileFields {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult();
$fields = [];
while($result->next()) return $stmt->getResult()->getIterator(ProfileFieldInfo::fromResult(...));
$fields[] = new ProfileFieldInfo($result);
return $fields;
} }
public function getField(string $fieldId): ProfileFieldInfo { public function getField(string $fieldId): ProfileFieldInfo {
@ -55,20 +55,30 @@ class ProfileFields {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No field found with the provided field id.'); throw new RuntimeException('No field found with the provided field id.');
return new ProfileFieldInfo($result); return ProfileFieldInfo::fromResult($result);
} }
public function getFieldFormats( public function getFieldFormats(
?array $fieldInfos = null, ?iterable $fieldInfos = null,
?array $fieldValueInfos = null ?iterable $fieldValueInfos = null
): array { ): iterable {
$hasFieldInfos = $fieldInfos !== null; $hasFieldInfos = $fieldInfos !== null;
$hasFieldValueInfos = $fieldValueInfos !== null; $hasFieldValueInfos = $fieldValueInfos !== null;
if($hasFieldInfos && empty($fieldInfos)) if($hasFieldInfos) {
return []; if(!is_array($fieldInfos))
if($hasFieldValueInfos && empty($fieldValueInfos)) $fieldInfos = iterator_to_array($fieldInfos);
return [];
if(empty($fieldInfos))
return [];
}
if($hasFieldValueInfos) {
if(!is_array($fieldValueInfos))
$fieldValueInfos = iterator_to_array($fieldValueInfos);
if(empty($fieldValueInfos))
return [];
}
$args = 0; $args = 0;
$query = 'SELECT format_id, field_id, format_regex, format_link, format_display FROM msz_profile_fields_formats'; $query = 'SELECT format_id, field_id, format_regex, format_link, format_display FROM msz_profile_fields_formats';
@ -101,13 +111,8 @@ class ProfileFields {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult();
$formats = [];
while($result->next()) return $stmt->getResult()->getIterator(ProfileFieldFormatInfo::fromResult(...));
$formats[] = new ProfileFieldFormatInfo($result);
return $formats;
} }
public function getFieldFormat(string $formatId): ProfileFieldFormatInfo { public function getFieldFormat(string $formatId): ProfileFieldFormatInfo {
@ -119,7 +124,7 @@ class ProfileFields {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No format found with the provided format id.'); throw new RuntimeException('No format found with the provided format id.');
return new ProfileFieldFormatInfo($result); return ProfileFieldFormatInfo::fromResult($result);
} }
public function selectFieldFormat( public function selectFieldFormat(
@ -138,10 +143,10 @@ class ProfileFields {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Could not determine an appropriate format for this field (missing default formatting)'); throw new RuntimeException('Could not determine an appropriate format for this field (missing default formatting)');
return new ProfileFieldFormatInfo($result); return ProfileFieldFormatInfo::fromResult($result);
} }
public function getFieldValues(UserInfo|string $userInfo): array { public function getFieldValues(UserInfo|string $userInfo): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
@ -151,13 +156,7 @@ class ProfileFields {
$stmt->addParameter(1, $userInfo); $stmt->addParameter(1, $userInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(ProfileFieldValueInfo::fromResult(...));
$values = [];
while($result->next())
$values[] = new ProfileFieldValueInfo($result);
return $values;
} }
public function getFieldValue( public function getFieldValue(
@ -179,7 +178,7 @@ class ProfileFields {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No value for this field and user combination found.'); throw new RuntimeException('No value for this field and user combination found.');
return new ProfileFieldValueInfo($result); return ProfileFieldValueInfo::fromResult($result);
} }
public function setFieldValues( public function setFieldValues(

View file

@ -1,21 +1,16 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Index\Http\HttpFx; use Index\Http\Routing\{HttpRouter,IRouter,IRouteHandler};
use Index\Http\HttpRequest; use Misuzu\URLs\{IURLSource,URLInfo,URLRegistry};
use Index\Routing\IRouter;
use Index\Routing\IRouteHandler;
use Misuzu\URLs\IURLSource;
use Misuzu\URLs\URLInfo;
use Misuzu\URLs\URLRegistry;
class RoutingContext { class RoutingContext {
private URLRegistry $urls; private URLRegistry $urls;
private HttpFx $router; private HttpRouter $router;
public function __construct() { public function __construct() {
$this->urls = new URLRegistry; $this->urls = new URLRegistry;
$this->router = new HttpFx; $this->router = new HttpRouter(errorHandler: new RoutingErrorHandler);
$this->router->use('/', fn($resp) => $resp->setPoweredBy('Misuzu')); $this->router->use('/', fn($resp) => $resp->setPoweredBy('Misuzu'));
} }
@ -27,25 +22,6 @@ class RoutingContext {
return $this->router; return $this->router;
} }
public function registerDefaultErrorPages(): void {
$this->router->addErrorHandler(400, fn($resp) => $resp->setContent(Template::renderRaw('errors.400')));
$this->router->addErrorHandler(403, fn($resp) => $resp->setContent(Template::renderRaw('errors.403')));
$this->router->addErrorHandler(404, fn($resp) => $resp->setContent(Template::renderRaw('errors.404')));
$this->router->addErrorHandler(500, fn($resp) => $resp->setContent(file_get_contents(MSZ_TEMPLATES . '/500.html')));
$this->router->addErrorHandler(503, fn($resp) => $resp->setContent(file_get_contents(MSZ_TEMPLATES . '/503.html')));
}
public static function registerSimpleErrorPages(IRouter $router, string $path): void {
if($router instanceof HttpFx)
$router->use($path, function() use($router) {
$router->addErrorHandler(400, fn($resp) => $resp->setContent('HTTP 400'));
$router->addErrorHandler(403, fn($resp) => $resp->setContent('HTTP 403'));
$router->addErrorHandler(404, fn($resp) => $resp->setContent('HTTP 404'));
$router->addErrorHandler(500, fn($resp) => $resp->setContent('HTTP 500'));
$router->addErrorHandler(503, fn($resp) => $resp->setContent('HTTP 503'));
});
}
public function register(IRouteHandler|IURLSource $handler): void { public function register(IRouteHandler|IURLSource $handler): void {
if($handler instanceof IRouteHandler) if($handler instanceof IRouteHandler)
$this->router->register($handler); $this->router->register($handler);
@ -54,7 +30,7 @@ class RoutingContext {
URLInfo::handleAttributes($this->urls, $handler); URLInfo::handleAttributes($this->urls, $handler);
} }
public function dispatch(?HttpRequest $request = null): void { public function dispatch(...$args): void {
$this->router->dispatch($request); $this->router->dispatch(...$args);
} }
} }

View file

@ -0,0 +1,29 @@
<?php
namespace Misuzu;
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\ErrorHandling\HtmlErrorHandler;
class RoutingErrorHandler extends HtmlErrorHandler {
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
if(str_starts_with($request->getPath(), '/_')) {
$response->setTypePlain();
$response->setContent(sprintf('HTTP %03d', $code));
return;
}
if($code === 500 || $code === 503) {
$response->setTypeHTML();
$response->setContent(file_get_contents(sprintf('%s/%03d.html', MSZ_TEMPLATES, $code)));
return;
}
if($code === 401 || $code === 403 || $code === 404) {
$response->setTypeHTML();
$response->setContent(Template::renderRaw(sprintf('errors.%03d', $code)));
return;
}
parent::handle($response, $request, $code, $message);
}
}

View file

@ -3,9 +3,7 @@ namespace Misuzu\Satori;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Routing\IRouter; use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler};
use Index\Routing\IRouteHandler;
use Index\Routing\Route;
use Syokuhou\IConfig; use Syokuhou\IConfig;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\RoutingContext; use Misuzu\RoutingContext;
@ -13,7 +11,7 @@ use Misuzu\Forum\ForumContext;
use Misuzu\Profile\ProfileFields; use Misuzu\Profile\ProfileFields;
use Misuzu\Users\UsersContext; use Misuzu\Users\UsersContext;
final class SatoriRoutes implements IRouteHandler { final class SatoriRoutes extends RouteHandler {
public function __construct( public function __construct(
private IConfig $config, private IConfig $config,
private UsersContext $usersCtx, private UsersContext $usersCtx,
@ -21,12 +19,7 @@ final class SatoriRoutes implements IRouteHandler {
private ProfileFields $profileFields private ProfileFields $profileFields
) {} ) {}
public function registerRoutes(IRouter $router): void { #[HttpMiddleware('/_satori')]
RoutingContext::registerSimpleErrorPages($router, '/_satori');
Route::handleAttributes($router, $this);
}
#[Route('/_satori')]
public function verifyRequest($response, $request) { public function verifyRequest($response, $request) {
$secretKey = $this->config->getString('secret'); $secretKey = $this->config->getString('secret');
@ -46,7 +39,7 @@ final class SatoriRoutes implements IRouteHandler {
} }
} }
#[Route('GET', '/_satori/get-profile-field')] #[HttpGet('/_satori/get-profile-field')]
public function getProfileField($response, $request): array { public function getProfileField($response, $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);
@ -62,7 +55,7 @@ final class SatoriRoutes implements IRouteHandler {
]; ];
} }
#[Route('GET', '/_satori/get-recent-forum-posts')] #[HttpGet('/_satori/get-recent-forum-posts')]
public function getRecentForumPosts($response, $request): array { public function getRecentForumPosts($response, $request): array {
$categoryIds = $this->config->getArray('forum.categories'); $categoryIds = $this->config->getArray('forum.categories');
if(empty($categoryIds)) if(empty($categoryIds))
@ -104,7 +97,7 @@ final class SatoriRoutes implements IRouteHandler {
return $posts; return $posts;
} }
#[Route('GET', '/_satori/get-recent-registrations')] #[HttpGet('/_satori/get-recent-registrations')]
public function getRecentRegistrations($response, $request) { public function getRecentRegistrations($response, $request) {
$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);

View file

@ -3,25 +3,21 @@ namespace Misuzu\SharpChat;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Routing\IRouter; use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler};
use Index\Routing\IRouteHandler;
use Index\Routing\Route;
use Syokuhou\IConfig; use Syokuhou\IConfig;
use Misuzu\RoutingContext; use Misuzu\RoutingContext;
use Misuzu\Auth\AuthContext; use Misuzu\Auth\{AuthContext,AuthInfo,Sessions};
use Misuzu\Auth\AuthInfo;
use Misuzu\Auth\Sessions;
use Misuzu\Emoticons\Emotes; use Misuzu\Emoticons\Emotes;
use Misuzu\Perms\Permissions; use Misuzu\Perms\Permissions;
use Misuzu\URLs\URLRegistry; use Misuzu\URLs\URLRegistry;
use Misuzu\Users\Bans; use Misuzu\Users\{Bans,UsersContext,UserInfo};
use Misuzu\Users\UsersContext;
final class SharpChatRoutes implements IRouteHandler { final class SharpChatRoutes extends RouteHandler {
private string $hashKey; private string $hashKey;
public function __construct( public function __construct(
private IConfig $config, private IConfig $config,
private IConfig $impersonateConfig, // this sucks lol
private URLRegistry $urls, private URLRegistry $urls,
private UsersContext $usersCtx, private UsersContext $usersCtx,
private AuthContext $authCtx, private AuthContext $authCtx,
@ -32,13 +28,8 @@ final class SharpChatRoutes implements IRouteHandler {
$this->hashKey = $this->config->getString('hashKey', 'woomy'); $this->hashKey = $this->config->getString('hashKey', 'woomy');
} }
public function registerRoutes(IRouter $router): void { #[HttpOptions('/_sockchat/emotes')]
RoutingContext::registerSimpleErrorPages($router, '/_sockchat'); #[HttpGet('/_sockchat/emotes')]
Route::handleAttributes($router, $this);
}
#[Route('OPTIONS', '/_sockchat/emotes')]
#[Route('GET', '/_sockchat/emotes')]
public function getEmotes($response, $request): array|int { public function getEmotes($response, $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');
@ -66,7 +57,7 @@ final class SharpChatRoutes implements IRouteHandler {
return $out; return $out;
} }
#[Route('GET', '/_sockchat/login')] #[HttpGet('/_sockchat/login')]
public function getLogin($response, $request) { public function getLogin($response, $request) {
if(!$this->authInfo->isLoggedIn()) { if(!$this->authInfo->isLoggedIn()) {
$response->redirect($this->urls->format('auth-login')); $response->redirect($this->urls->format('auth-login'));
@ -79,8 +70,16 @@ final class SharpChatRoutes implements IRouteHandler {
)); ));
} }
#[Route('OPTIONS', '/_sockchat/token')] private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
#[Route('GET', '/_sockchat/token')] if($impersonator->isSuperUser())
return true;
$whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId()));
return in_array($targetId, $whitelist, true);
}
#[HttpOptions('/_sockchat/token')]
#[HttpGet('/_sockchat/token')]
public function getToken($response, $request) { public function getToken($response, $request) {
$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') : '';
@ -121,7 +120,7 @@ final class SharpChatRoutes implements IRouteHandler {
return ['ok' => false, 'err' => 'user']; return ['ok' => false, 'err' => 'user'];
$userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id'); $userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
$userId = $tokenInfo->hasImpersonatedUserId() && $userInfo->isSuperUser() $userId = $tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())
? $tokenInfo->getImpersonatedUserId() ? $tokenInfo->getImpersonatedUserId()
: $userInfo->getId(); : $userInfo->getId();
@ -134,7 +133,7 @@ final class SharpChatRoutes implements IRouteHandler {
]; ];
} }
#[Route('POST', '/_sockchat/bump')] #[HttpPost('/_sockchat/bump')]
public function postBump($response, $request) { public function postBump($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
@ -164,7 +163,7 @@ final class SharpChatRoutes implements IRouteHandler {
$this->usersCtx->getUsers()->recordUserActivity($userId, remoteAddr: $ipAddr); $this->usersCtx->getUsers()->recordUserActivity($userId, remoteAddr: $ipAddr);
} }
#[Route('POST', '/_sockchat/verify')] #[HttpPost('/_sockchat/verify')]
public function postVerify($response, $request) { public function postVerify($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
@ -215,7 +214,7 @@ final class SharpChatRoutes implements IRouteHandler {
$this->authCtx->getSessions()->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $ipAddress); $this->authCtx->getSessions()->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $ipAddress);
$userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id'); $userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
if($tokenInfo->hasImpersonatedUserId() && $userInfo->isSuperUser()) { if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
$userInfoReal = $userInfo; $userInfoReal = $userInfo;
try { try {
@ -245,7 +244,7 @@ final class SharpChatRoutes implements IRouteHandler {
]; ];
} }
#[Route('GET', '/_sockchat/bans/list')] #[HttpGet('/_sockchat/bans/list')]
public function getBanList($response, $request) { public function getBanList($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
@ -280,7 +279,7 @@ final class SharpChatRoutes implements IRouteHandler {
return $list; return $list;
} }
#[Route('GET', '/_sockchat/bans/check')] #[HttpGet('/_sockchat/bans/check')]
public function getBanCheck($response, $request) { public function getBanCheck($response, $request) {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;
@ -318,7 +317,7 @@ final class SharpChatRoutes implements IRouteHandler {
]; ];
} }
#[Route('POST', '/_sockchat/bans/create')] #[HttpPost('/_sockchat/bans/create')]
public function postBanCreate($response, $request): int { public function postBanCreate($response, $request): int {
if(!$request->hasHeader('X-SharpChat-Signature') || !$request->isFormContent()) if(!$request->hasHeader('X-SharpChat-Signature') || !$request->isFormContent())
return 400; return 400;
@ -392,7 +391,7 @@ final class SharpChatRoutes implements IRouteHandler {
return 201; return 201;
} }
#[Route('DELETE', '/_sockchat/bans/revoke')] #[HttpDelete('/_sockchat/bans/revoke')]
public function deleteBanRevoke($response, $request): int { public function deleteBanRevoke($response, $request): int {
if(!$request->hasHeader('X-SharpChat-Signature')) if(!$request->hasHeader('X-SharpChat-Signature'))
return 400; return 400;

View file

@ -1,9 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Index\Routing\IRouter;
use Index\Http\HttpFx;
final class Tools { final class Tools {
public static function isLocalURL( public static function isLocalURL(
string $url, string $url,

View file

@ -3,14 +3,11 @@ namespace Misuzu\Users\Assets;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\Routing\Route; use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Routing\RouteHandler;
use Misuzu\Perm; use Misuzu\Perm;
use Misuzu\Auth\AuthInfo; use Misuzu\Auth\AuthInfo;
use Misuzu\URLs\URLInfo; use Misuzu\URLs\{URLInfo,URLRegistry};
use Misuzu\URLs\URLRegistry; use Misuzu\Users\{UsersContext,UserInfo};
use Misuzu\Users\UsersContext;
use Misuzu\Users\UserInfo;
class AssetsRoutes extends RouteHandler { class AssetsRoutes extends RouteHandler {
public function __construct( public function __construct(
@ -29,11 +26,10 @@ class AssetsRoutes extends RouteHandler {
return true; return true;
} }
#[Route('GET', '/assets/avatar')] #[HttpGet('/assets/avatar')]
#[Route('GET', '/assets/avatar/:filename')] #[HttpGet('/assets/avatar/([0-9]+)(?:\.[a-z]+)?')]
#[URLInfo('user-avatar', '/assets/avatar/<user>', ['res' => '<res>'])] #[URLInfo('user-avatar', '/assets/avatar/<user>', ['res' => '<res>'])]
public function getAvatar($response, $request, string $fileName = '') { public function getAvatar($response, $request, string $userId = '') {
$userId = pathinfo($fileName, PATHINFO_FILENAME);
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC); $assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
try { try {
@ -52,12 +48,10 @@ class AssetsRoutes extends RouteHandler {
$this->serveAsset($response, $request, $assetInfo); $this->serveAsset($response, $request, $assetInfo);
} }
#[Route('GET', '/assets/profile-background')] #[HttpGet('/assets/profile-background')]
#[Route('GET', '/assets/profile-background/:filename')] #[HttpGet('/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')]
#[URLInfo('user-background', '/assets/profile-background/<user>')] #[URLInfo('user-background', '/assets/profile-background/<user>')]
public function getProfileBackground($response, $request, string $fileName = '') { public function getProfileBackground($response, $request, string $userId = '') {
$userId = pathinfo($fileName, PATHINFO_FILENAME);
try { try {
$userInfo = $this->usersCtx->getUserInfo($userId); $userInfo = $this->usersCtx->getUserInfo($userId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
@ -78,7 +72,7 @@ class AssetsRoutes extends RouteHandler {
$this->serveAsset($response, $request, $assetInfo); $this->serveAsset($response, $request, $assetInfo);
} }
#[Route('GET', '/user-assets.php')] #[HttpGet('/user-assets.php')]
public function getUserAssets($response, $request) { public function getUserAssets($response, $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');

View file

@ -5,24 +5,28 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class BanInfo { class BanInfo {
private string $id; public function __construct(
private string $userId; private string $id,
private ?string $modId; private string $userId,
private int $severity; private ?string $modId,
private string $publicReason; private int $severity,
private string $privateReason; private string $publicReason,
private int $created; private string $privateReason,
private ?int $expires; private int $created,
private ?int $expires,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): BanInfo {
$this->id = (string)$result->getInteger(0); return new BanInfo(
$this->userId = (string)$result->getInteger(1); id: $result->getString(0),
$this->modId = $result->isNull(2) ? null : (string)$result->getInteger(2); userId: $result->getString(1),
$this->severity = $result->getInteger(3); modId: $result->getStringOrNull(2),
$this->publicReason = $result->getString(4); severity: $result->getInteger(3),
$this->privateReason = $result->getString(5); publicReason: $result->getString(4),
$this->created = $result->getInteger(6); privateReason: $result->getString(5),
$this->expires = $result->isNull(7) ? null : $result->getInteger(7); created: $result->getInteger(6),
expires: $result->getIntegerOrNull(7),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -64,7 +64,7 @@ class Bans {
?bool $activeOnly = null, ?bool $activeOnly = null,
?bool $activeFirst = null, ?bool $activeFirst = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
@ -101,13 +101,7 @@ class Bans {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(BanInfo::fromResult(...));
$bans = [];
while($result->next())
$bans[] = new BanInfo($result);
return $bans;
} }
public function getBan(string $banId): BanInfo { public function getBan(string $banId): BanInfo {
@ -119,7 +113,7 @@ class Bans {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No ban with ID $banId found.'); throw new RuntimeException('No ban with ID $banId found.');
return new BanInfo($result); return BanInfo::fromResult($result);
} }
public function countActiveBans( public function countActiveBans(
@ -153,7 +147,7 @@ class Bans {
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
return $result->next() ? new BanInfo($result) : null; return $result->next() ? BanInfo::fromResult($result) : null;
} }
public function createBan( public function createBan(

View file

@ -5,20 +5,24 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ModNoteInfo { class ModNoteInfo {
private string $noteId; public function __construct(
private string $userId; private string $noteId,
private ?string $authorId; private string $userId,
private int $created; private ?string $authorId,
private string $title; private int $created,
private string $body; private string $title,
private string $body,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): ModNoteInfo {
$this->noteId = (string)$result->getInteger(0); return new ModNoteInfo(
$this->userId = (string)$result->getInteger(1); noteId: $result->getString(0),
$this->authorId = $result->isNull(2) ? null : (string)$result->getInteger(2); userId: $result->getString(1),
$this->created = $result->getInteger(3); authorId: $result->getStringOrNull(2),
$this->title = $result->getString(4); created: $result->getInteger(3),
$this->body = $result->getString(5); title: $result->getString(4),
body: $result->getString(5)
);
} }
public function getId(): string { public function getId(): string {

View file

@ -59,7 +59,7 @@ class ModNotes {
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
UserInfo|string|null $authorInfo = null, UserInfo|string|null $authorInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($authorInfo instanceof UserInfo) if($authorInfo instanceof UserInfo)
@ -93,13 +93,7 @@ class ModNotes {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(ModNoteInfo::fromResult(...));
$notes = [];
while($result->next())
$notes[] = new ModNoteInfo($result);
return $notes;
} }
public function getNote(string $noteId): ModNoteInfo { public function getNote(string $noteId): ModNoteInfo {
@ -111,7 +105,7 @@ class ModNotes {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('No note with ID $noteId found.'); throw new RuntimeException('No note with ID $noteId found.');
return new ModNoteInfo($result); return ModNoteInfo::fromResult($result);
} }
public function createNote( public function createNote(

View file

@ -7,26 +7,30 @@ use Index\Colour\Colour;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class RoleInfo implements Stringable { class RoleInfo implements Stringable {
private string $id; public function __construct(
private int $rank; private string $id,
private string $name; private int $rank,
private ?string $title; private string $name,
private ?string $description; private ?string $title,
private bool $hidden; private ?string $description,
private bool $leavable; private bool $hidden,
private ?int $colour; private bool $leavable,
private int $created; private ?int $colour,
private int $created,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): RoleInfo {
$this->id = (string)$result->getInteger(0); return new RoleInfo(
$this->rank = $result->getInteger(1); id: $result->getString(0),
$this->name = $result->getString(2); rank: $result->getInteger(1),
$this->title = $result->isNull(3) ? null : $result->getString(3); name: $result->getString(2),
$this->description = $result->isNull(4) ? null : $result->getString(4); title: $result->getStringOrNull(3),
$this->hidden = $result->getInteger(5) !== 0; description: $result->getStringOrNull(4),
$this->leavable = $result->getInteger(6) !== 0; hidden: $result->getBoolean(5),
$this->colour = $result->isNull(7) ? null : $result->getInteger(7); leavable: $result->getBoolean(6),
$this->created = $result->getInteger(8); colour: $result->getIntegerOrNull(7),
created: $result->getInteger(8),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -58,7 +58,7 @@ class Roles {
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?bool $hidden = null, ?bool $hidden = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
@ -87,13 +87,7 @@ class Roles {
} }
$stmt->execute(); $stmt->execute();
$roles = []; return $stmt->getResult()->getIterator(RoleInfo::fromResult(...));
$result = $stmt->getResult();
while($result->next())
$roles[] = new RoleInfo($result);
return $roles;
} }
public function getRole(string $roleId): RoleInfo { public function getRole(string $roleId): RoleInfo {
@ -105,7 +99,7 @@ class Roles {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Could not find role with ID $roleId.'); throw new RuntimeException('Could not find role with ID $roleId.');
return new RoleInfo($result); return RoleInfo::fromResult($result);
} }
public function createRole( public function createRole(

View file

@ -9,50 +9,54 @@ use Index\Net\IPAddress;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
class UserInfo { class UserInfo {
private string $id; public function __construct(
private string $name; private string $id,
private ?string $passwordHash; private string $name,
private string $emailAddr; private ?string $passwordHash,
private string $registerRemoteAddr; private string $emailAddr,
private string $lastRemoteAddr; private string $registerRemoteAddr,
private bool $super; private string $lastRemoteAddr,
private string $countryCode; private bool $super,
private ?int $colour; private string $countryCode,
private int $created; private ?int $colour,
private ?int $lastActive; private int $created,
private ?int $deleted; private ?int $lastActive,
private ?string $displayRoleId; private ?int $deleted,
private ?string $totpKey; private ?string $displayRoleId,
private ?string $aboutContent; private ?string $totpKey,
private int $aboutParser; private ?string $aboutContent,
private ?string $signatureContent; private int $aboutParser,
private int $signatureParser; private ?string $signatureContent,
private ?string $birthdate; private int $signatureParser,
private ?int $backgroundSettings; private ?string $birthdate,
private ?string $title; private ?int $backgroundSettings,
private ?string $title,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): self {
$this->id = (string)$result->getInteger(0); return new UserInfo(
$this->name = $result->getString(1); id: $result->getString(0),
$this->passwordHash = $result->isNull(2) ? null : $result->getString(2); name: $result->getString(1),
$this->emailAddr = $result->getString(3); passwordHash: $result->getStringOrNull(2),
$this->registerRemoteAddr = $result->getString(4); emailAddr: $result->getString(3),
$this->lastRemoteAddr = $result->isNull(5) ? null : $result->getString(5); registerRemoteAddr: $result->getString(4),
$this->super = $result->getInteger(6) !== 0; lastRemoteAddr: $result->getStringOrNull(5),
$this->countryCode = $result->getString(7); super: $result->getBoolean(6),
$this->colour = $result->isNull(8) ? null : $result->getInteger(8); countryCode: $result->getString(7),
$this->created = $result->getInteger(9); colour: $result->getIntegerOrNull(8),
$this->lastActive = $result->isNull(10) ? null : $result->getInteger(10); created: $result->getInteger(9),
$this->deleted = $result->isNull(11) ? null : $result->getInteger(11); lastActive: $result->getIntegerOrNull(10),
$this->displayRoleId = $result->isNull(12) ? null : (string)$result->getInteger(12); deleted: $result->getIntegerOrNull(11),
$this->totpKey = $result->isNull(13) ? null : $result->getString(13); displayRoleId: $result->getStringOrNull(12),
$this->aboutContent = $result->isNull(14) ? null : $result->getString(14); totpKey: $result->getStringOrNull(13),
$this->aboutParser = $result->getInteger(15); aboutContent: $result->getStringOrNull(14),
$this->signatureContent = $result->isNull(16) ? null : $result->getString(16); aboutParser: $result->getInteger(15),
$this->signatureParser = $result->getInteger(17); signatureContent: $result->getStringOrNull(16),
$this->birthdate = $result->isNull(18) ? null : $result->getString(18); signatureParser: $result->getInteger(17),
$this->backgroundSettings = $result->isNull(19) ? null : $result->getInteger(19); birthdate: $result->getStringOrNull(18),
$this->title = $result->getString(20); backgroundSettings: $result->getIntegerOrNull(19),
title: $result->getString(20),
);
} }
public function getId(): string { public function getId(): string {

View file

@ -112,7 +112,7 @@ class Users {
?bool $reverseOrder = null, ?bool $reverseOrder = null,
?array $searchQuery = null, ?array $searchQuery = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
// remove this hack when search server // remove this hack when search server
$hasSearchQuery = $searchQuery !== null; $hasSearchQuery = $searchQuery !== null;
$searchLimitResults = false; $searchLimitResults = false;
@ -211,13 +211,7 @@ class Users {
} }
$stmt->execute(); $stmt->execute();
$users = []; return $stmt->getResult()->getIterator(UserInfo::fromResult(...));
$result = $stmt->getResult();
while($result->next())
$users[] = new UserInfo($result);
return $users;
} }
public const GET_USER_ID = 0x01; public const GET_USER_ID = 0x01;
@ -283,7 +277,7 @@ class Users {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('User not found.'); throw new RuntimeException('User not found.');
return new UserInfo($result); return UserInfo::fromResult($result);
} }
public function createUser( public function createUser(

View file

@ -5,18 +5,22 @@ use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class WarningInfo { class WarningInfo {
private string $id; public function __construct(
private string $userId; private string $id,
private ?string $modId; private string $userId,
private string $body; private ?string $modId,
private int $created; private string $body,
private int $created,
) {}
public function __construct(IDbResult $result) { public static function fromResult(IDbResult $result): WarningInfo {
$this->id = (string)$result->getInteger(0); return new WarningInfo(
$this->userId = (string)$result->getInteger(1); id: $result->getString(0),
$this->modId = $result->isNull(2) ? null : (string)$result->getInteger(2); userId: $result->getString(1),
$this->body = $result->getString(3); modId: $result->getStringOrNull(2),
$this->created = $result->getInteger(4); body: $result->getString(3),
created: $result->getInteger(4)
);
} }
public function getId(): string { public function getId(): string {

View file

@ -53,18 +53,14 @@ class Warnings {
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
$count = 0;
if($result->next()) return $result->next() ? $result->getInteger(0) : 0;
$count = $result->getInteger(0);
return $count;
} }
public function getWarningsWithDefaultBacklog( public function getWarningsWithDefaultBacklog(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
return $this->getWarnings( return $this->getWarnings(
$userInfo, $userInfo,
self::VISIBLE_BACKLOG, self::VISIBLE_BACKLOG,
@ -76,7 +72,7 @@ class Warnings {
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?int $backlog = null, ?int $backlog = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
@ -111,13 +107,7 @@ class Warnings {
} }
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResult()->getIterator(WarningInfo::fromResult(...));
$warns = [];
while($result->next())
$warns[] = new WarningInfo($result);
return $warns;
} }
public function getWarning(string $warnId): WarningInfo { public function getWarning(string $warnId): WarningInfo {
@ -130,7 +120,7 @@ class Warnings {
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Could not find warning info for ID $warnId.'); throw new RuntimeException('Could not find warning info for ID $warnId.');
return new WarningInfo($result); return WarningInfo::fromResult($result);
} }
public function createWarning( public function createWarning(

View file

@ -79,7 +79,7 @@
{% set show_profile_fields = not profile_is_guest and (profile_is_editing ? perms.edit_profile : profile_fields_display_values|default([]) is not empty) %} {% set show_profile_fields = not profile_is_guest and (profile_is_editing ? perms.edit_profile : profile_fields_display_values|default([]) is not empty) %}
{% set show_background_settings = profile_is_editing and perms.edit_background %} {% set show_background_settings = profile_is_editing and perms.edit_background %}
{% set show_birthdate = profile_is_editing and perms.edit_birthdate %} {% set show_birthdate = profile_is_editing and perms.edit_birthdate %}
{% set show_active_forum_info = not profile_is_guest and not profile_is_deleted and not profile_is_editing or (profile_active_category_info is defined and profile_active_category_info is not empty or profile_active_topic_info.topic_id|default(0) > 0) %} {% set show_active_forum_info = not profile_is_guest and not profile_is_deleted and not profile_is_editing and profile_active_topic_info.id|default(0) > 0 %}
{% set show_warnings = profile_warnings is defined and profile_warnings|length > 0 %} {% set show_warnings = profile_warnings is defined and profile_warnings|length > 0 %}
{% set show_sidebar = (not profile_is_banned or profile_can_edit) and (profile_is_guest or show_profile_fields or show_background_settings or show_birthdate or show_active_forum_info or show_warnings) %} {% set show_sidebar = (not profile_is_banned or profile_can_edit) and (profile_is_guest or show_profile_fields or show_background_settings or show_birthdate or show_active_forum_info or show_warnings) %}

View file

@ -132,6 +132,15 @@ msz_sched_task_func('Resync statistics counters.', true, function() use ($msz) {
'users:warnings:visible' => 'SELECT COUNT(*) FROM msz_users_warnings WHERE warn_created > NOW() - INTERVAL 90 DAY', 'users:warnings:visible' => 'SELECT COUNT(*) FROM msz_users_warnings WHERE warn_created > NOW() - INTERVAL 90 DAY',
'users:bans:total' => 'SELECT COUNT(*) FROM msz_users_bans', 'users:bans:total' => 'SELECT COUNT(*) FROM msz_users_bans',
'users:bans:active' => 'SELECT COUNT(*) FROM msz_users_bans WHERE ban_expires IS NULL OR ban_expires > NOW()', 'users:bans:active' => 'SELECT COUNT(*) FROM msz_users_bans WHERE ban_expires IS NULL OR ban_expires > NOW()',
'pms:msgs:total' => 'SELECT COUNT(*) FROM msz_messages',
'pms:msgs:messages' => 'SELECT COUNT(DISTINCT msg_id) FROM msz_messages',
'pms:msgs:replies' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_reply_to IS NULL',
'pms:msgs:drafts' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_sent IS NULL',
'pms:msgs:unread' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_read IS NULL',
'pms:msgs:deleted' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_deleted IS NOT NULL',
'pms:msgs:plain' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_parser = 0',
'pms:msgs:bbcode' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_parser = 1',
'pms:msgs:markdown' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_parser = 2',
]; ];
foreach($stats as $name => $query) { foreach($stats as $name => $query) {
@ -149,6 +158,88 @@ msz_sched_task_func('Recalculate permissions (maybe)...', false, function() use
$msz->getPerms()->precalculatePermissions($msz->getForumContext()->getCategories()); $msz->getPerms()->precalculatePermissions($msz->getForumContext()->getCategories());
}); });
msz_sched_task_func('Omega disliking comments...', true, function() use ($msz) {
$commentIds = $msz->getConfig()->getArray('comments.omegadislike');
if(!is_array($commentIds))
return;
$dbConn = $msz->getDbConn();
$stmt = $dbConn->prepare('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) SELECT ?, user_id, -1 FROM msz_users');
foreach($commentIds as $commentId) {
$stmt->addParameter(1, $commentId);
$stmt->execute();
}
});
msz_sched_task_func('Announcing random topics...', true, function() use ($msz) {
$categoryIds = $msz->getConfig()->getArray('forum.rngannounce');
if(!is_array($categoryIds))
return;
$dbConn = $msz->getDbConn();
$stmtRevert = $dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 0 WHERE forum_id = ? AND topic_type = 2');
$stmtRandom = $dbConn->prepare('SELECT topic_id FROM msz_forum_topics WHERE forum_id = ? AND topic_deleted IS NULL ORDER BY RAND() LIMIT 1');
$stmtAnnounce = $dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 2 WHERE topic_id = ?');
foreach($categoryIds as $categoryId) {
$stmtRevert->addParameter(1, $categoryId);
$stmtRevert->execute();
$stmtRandom->addParameter(1, $categoryId);
$stmtRandom->execute();
$resultRandom = $stmtRandom->getResult();
if($resultRandom->next()) {
$stmtAnnounce->addParameter(1, $resultRandom->getInteger(0));
$stmtAnnounce->execute();
}
}
});
msz_sched_task_func('Changing category icons...', false, function() use ($msz) {
$config = $msz->getConfig();
$categoryIds = $config->getArray('forum.rngicon');
if(!is_array($categoryIds))
return;
$dbConn = $msz->getDbConn();
$stmtIcon = $dbConn->prepare('UPDATE msz_forum_categories SET forum_icon = COALESCE(?, forum_icon), forum_name = COALESCE(?, forum_name), forum_colour = ((ROUND(RAND() * 255) << 16) | (ROUND(RAND() * 255) << 8) | ROUND(RAND() * 255)) WHERE forum_id = ?');
$stmtUnlock = $dbConn->prepare('UPDATE msz_forum_topics SET topic_locked = IF(?, NULL, COALESCE(topic_locked, NOW())) WHERE topic_id = ?');
foreach($categoryIds as $categoryId) {
$scoped = $config->scopeTo(sprintf('forum.rngicon.fc%d', $categoryId));
$icons = $scoped->getArray('icons');
$icon = $icons[array_rand($icons)];
$name = null;
$names = $scoped->getArray('names');
for($i = 0; $i < count($names); $i += 2)
if($names[$i] === $icon) {
$name = $names[$i + 1] ?? null;
break;
}
$name ??= $scoped->getString('name');
$stmtIcon->addParameter(1, $icon);
$stmtIcon->addParameter(2, empty($name) ? null : $name);
$stmtIcon->addParameter(3, $categoryId);
$stmtIcon->execute();
$unlockModes = $scoped->getArray('unlock');
foreach($unlockModes as $unlockMode) {
$unlockScoped = $scoped->scopeTo(sprintf('unlock.%s', $unlockMode));
$topicId = $unlockScoped->getInteger('topic');
$match = $unlockScoped->getArray('match');
$matches = in_array($icon, $match);
$stmtUnlock->addParameter(1, $matches ? 1 : 0);
$stmtUnlock->addParameter(2, $topicId);
$stmtUnlock->execute();
}
}
});
echo 'Running ' . count($schedTasks) . ' tasks...' . PHP_EOL; echo 'Running ' . count($schedTasks) . ' tasks...' . PHP_EOL;
$dbConn = $msz->getDbConn(); $dbConn = $msz->getDbConn();