Compare commits

...

21 commits

Author SHA1 Message Date
fc6a899f16 Fixed some URLs not getting registered properly. 2024-10-05 15:28:56 +00:00
1f16de2239 Fixed casing oversight. 2024-10-05 14:39:43 +00:00
1550a5da57 Removed all references to the IPAddress class. 2024-10-05 14:22:14 +00:00
7ef1974c88 Fixed undropkicked I. 2024-10-05 03:36:53 +00:00
0f45a5f60f Updated to latest Index version. 2024-10-05 02:40:29 +00:00
324fe21d73 Use attributes for JSON encoding. 2024-09-30 17:38:08 +00:00
153abde3a2 Updated libraries. 2024-09-30 17:37:41 +00:00
f8aaa71260 Added optional string role IDs for the API. 2024-09-16 21:44:37 +00:00
37a3bc1ee6 Updated libraries. 2024-09-16 20:51:46 +00:00
f547812d5a Added RPC endpoint for fetching user info. 2024-09-05 20:08:31 +00:00
8a06836985 Added auth RPC routes. 2024-08-25 23:03:46 +00:00
34528ae413 Fixed return type. 2024-08-18 20:54:39 +00:00
0bf7ca0d52 Replaced internal Flashii ID routes with RPC library. 2024-08-16 19:29:57 +00:00
cc9fccdf18 Updated Index and switched to Carbon for date handling. 2024-08-04 21:37:12 +00:00
ca77b501e7 Removed stray Jeff. 2024-07-27 20:11:06 +00:00
2439f87df9 Added very preliminary support for Bearer tokens to chat authentication. 2024-07-21 01:50:42 +00:00
400253e04b Added interop endpoints for Hanyuu. 2024-07-20 19:35:50 +00:00
01c60e3027 Updated libraries. 2024-07-18 03:42:16 +00:00
37d8413118 Updated build script. 2024-06-11 00:48:44 +00:00
8cfa07bc8c SharpConfig -> FileConfig 2024-06-03 23:04:59 +00:00
a65579bf9d Updated libraries. 2024-06-03 23:04:21 +00:00
148 changed files with 2526 additions and 2333 deletions

View file

@ -1,6 +1,6 @@
.news__feeds {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr;
grid-gap: 2px;
padding: 2px;
}

View file

@ -1,5 +1,5 @@
#include csrfp.js
#include msgbox.js
#include msgbox.jsx
#include utility.js
#include messages/actbtn.js
#include messages/list.js

171
build.js
View file

@ -1,157 +1,30 @@
// IMPORTS
const assproc = require('@railcomm/assproc');
const { join: pathJoin } = require('path');
const fs = require('fs');
const swc = require('@swc/core');
const path = require('path');
const util = require('util');
const postcss = require('postcss');
const utils = require('./assets/utils.js');
const assproc = require('./assets/assproc.js');
// CONFIG
const rootDir = __dirname;
const srcDir = path.join(rootDir, 'assets');
const srcCurrentInfo = path.join(srcDir, 'current.json');
const pubDir = path.join(rootDir, 'public');
const pubAssetsDir = path.join(pubDir, 'assets');
const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug'));
const buildTasks = {
js: [
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
],
css: [
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
],
};
// PREP
const postcssPlugins = [ require('autoprefixer')({ remove: false }) ];
if(!isDebugBuild)
postcssPlugins.push(require('cssnano')({
preset: [
'cssnano-preset-default',
{
minifyGradients: false,
reduceIdents: false,
zindex: true,
}
],
}));
const swcJscOptions = {
target: 'es2021',
loose: false,
externalHelpers: false,
keepClassNames: true,
preserveAllComments: false,
transform: {},
parser: {
syntax: 'ecmascript',
jsx: true,
dynamicImport: false,
privateMethod: false,
functionBind: false,
exportDefaultFrom: false,
exportNamespaceFrom: false,
decorators: false,
decoratorsBeforeExport: false,
topLevelAwait: true,
importMeta: false,
},
transform: {
react: {
runtime: 'classic',
pragma: '$er',
},
},
};
// BUILD
(async () => {
const files = {};
const isDebug = fs.existsSync(pathJoin(__dirname, '.debug'));
console.log('Ensuring assets directory exists...');
fs.mkdirSync(pubAssetsDir, { recursive: true });
const env = {
root: __dirname,
source: pathJoin(__dirname, 'assets'),
public: pathJoin(__dirname, 'public'),
debug: isDebug,
swc: {
es: 'es2021',
},
};
const tasks = {
js: [
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
],
css: [
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
],
};
console.log();
console.log('JS assets');
for(const info of buildTasks.js) {
console.log(`=> Building ${info.source}...`);
const files = await assproc.process(env, tasks);
let origTarget = undefined;
if('es' in info) {
origTarget = swcJscOptions.target;
swcJscOptions.target = info.es;
}
const assprocOpts = {
prefix: '#',
entry: info.entry || 'main.js',
};
const swcOpts = {
filename: info.source,
sourceMaps: false,
isModule: false,
minify: !isDebugBuild,
jsc: swcJscOptions,
};
const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts)
.then(output => swc.transform(output, swcOpts))
.then(output => {
const name = utils.strtr(info.name, { hash: utils.shortHash(output.code) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
fs.writeFileSync(path.join(pubDir, pubName), output.code);
return pubName;
});
if(origTarget !== undefined)
swcJscOptions.target = origTarget;
files[info.source] = pubName;
}
console.log();
console.log('CSS assets');
for(const info of buildTasks.css) {
console.log(`=> Building ${info.source}...`);
const sourcePath = path.join(srcDir, info.source);
const assprocOpts = {
prefix: '@',
entry: info.entry || 'main.css',
};
const postcssOpts = { from: sourcePath };
files[info.source] = await assproc.process(sourcePath, assprocOpts)
.then(output => postcss(postcssPlugins).process(output, postcssOpts)
.then(output => {
const name = utils.strtr(info.name, { hash: utils.shortHash(output.css) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
fs.writeFileSync(path.join(pubDir, pubName), output.css);
return pubName;
}));
}
console.log();
console.log('Writing assets info...');
fs.writeFileSync(srcCurrentInfo, JSON.stringify(files));
console.log();
console.log('Cleaning up old builds...');
assproc.housekeep(pubAssetsDir);
fs.writeFileSync(pathJoin(__dirname, 'assets/current.json'), JSON.stringify(files));
})();

View file

@ -1,15 +1,13 @@
{
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"flashwave/index": "dev-master",
"flashwave/sasae": "dev-master",
"flashwave/index": "^0.2410",
"flashwave/aiwass": "^1.1",
"erusev/parsedown": "~1.6",
"chillerlan/php-qrcode": "^4.3",
"symfony/mailer": "^6.0",
"matomo/device-detector": "^6.1",
"sentry/sdk": "^4.0",
"flashwave/syokuhou": "dev-master"
"nesbot/carbon": "^3.7"
},
"autoload": {
"classmap": [
@ -34,6 +32,6 @@
}
},
"require-dev": {
"phpstan/phpstan": "^1.10"
"phpstan/phpstan": "^1.11"
}
}

1024
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
// Switching to the Index migration system!!!!!!
final class InitialStructureNdx_20230107_023235 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class InitialStructureNdx_20230107_023235 implements DbMigration {
public function migrate(DbConnection $conn): void {
$hasMszTrack = false;
// check if the old migrations table exists

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class CreateTopicRedirsTable_20230430_001226 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class CreateTopicRedirsTable_20230430_001226 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('
CREATE TABLE msz_forum_topics_redirects (
topic_id INT(10) UNSIGNED NOT NULL,

View file

@ -1,10 +1,10 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
use Misuzu\ClientInfo;
final class UpdateUserAgentStorage_20230721_121854 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class UpdateUserAgentStorage_20230721_121854 implements DbMigration {
public function migrate(DbConnection $conn): void {
// convert user agent fields to BLOB and add field for client info storage
$conn->execute('
ALTER TABLE msz_login_attempts

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class AddModeratorNotesTable_20230724_201010 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class AddModeratorNotesTable_20230724_201010 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('
CREATE TABLE msz_users_modnotes (
note_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class AddNewBansTable_20230726_175936 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class AddNewBansTable_20230726_175936 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('
CREATE TABLE msz_users_bans (
ban_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class RedoWarningsTable_20230726_210150 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class RedoWarningsTable_20230726_210150 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('
CREATE TABLE msz_users_warnings (
warn_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class PluraliseUsersForRoleRelations_20230727_130516 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class PluraliseUsersForRoleRelations_20230727_130516 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('RENAME TABLE msz_user_roles TO msz_users_roles');
}
}

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class CreateCountersTable_20230728_212101 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class CreateCountersTable_20230728_212101 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('
CREATE TABLE msz_counters (
counter_name VARBINARY(64) NOT NULL,

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class UpdateCollationsInVariousTables_20230803_114403 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class UpdateCollationsInVariousTables_20230803_114403 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('
ALTER TABLE msz_audit_log
CHANGE COLUMN log_action log_action VARCHAR(50) NOT NULL COLLATE "ascii_general_ci" AFTER user_id,

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class NewPermissionsSystem_20230830_213930 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class NewPermissionsSystem_20230830_213930 implements DbMigration {
public function migrate(DbConnection $conn): void {
// make sure cron doesn't fuck us over
$conn->execute('DELETE FROM msz_config WHERE config_name = "perms.needsRecalc"');

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class CreateMessagesTable_20240130_233734 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class CreateMessagesTable_20240130_233734 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute('
CREATE TABLE msz_messages (
msg_id BINARY(8) NOT NULL,

View file

@ -1,9 +1,9 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class BaseSixtyFourEncodePmsInDb_20240602_194809 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
final class BaseSixtyFourEncodePmsInDb_20240602_194809 implements DbMigration {
public function migrate(DbConnection $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`

View file

@ -0,0 +1,13 @@
<?php
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class AddRoleIdString_20240916_205613 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute(<<<SQL
ALTER TABLE msz_roles
ADD COLUMN role_string VARCHAR(20) NULL DEFAULT NULL COLLATE 'ascii_general_ci' AFTER role_id,
ADD UNIQUE INDEX roles_string_unique (role_string);
SQL);
}
}

View file

@ -13,6 +13,7 @@ Below are a number of links to source code repositories related to Flashii.net a
- [Seria](https://patchii.net/flashii/seria): Software used by the downloads tracker.
- [Mince](https://patchii.net/flashii/mince): Source code for the Minecraft servers subwebsite.
- [Awaki](https://patchii.net/flashii/awaki): Redirect service hosted on fii.moe.
- [Aleister](https://patchii.net/flashii/aleister): Public API gateway.
## Tools & Software
- [SoFii](https://patchii.net/flashii/sofii): Launcher for Soldier of Fortune 2
@ -21,8 +22,7 @@ Below are a number of links to source code repositories related to Flashii.net a
## First-Party Libraries
- [Index](https://patchii.net/flash/index): Base library used in almost any component of the website that uses PHP.
- [Sasae](https://patchii.net/flash/sasae): Extension to the Twig templating library.
- [Syokuhou](https://patchii.net/flash/syokuhou): Configuration library.
- [Aiwass](https://patchii.net/flashii/aiwass): Internal RPC extension, mainly used to supply data to the API gateway.
## Historical
- [AJAX Chat (fork)](https://patchii.net/flashii/ajax-chat): Old chat software (2013-2015). Still kept on life support for the nostalgia.

View file

@ -1,10 +1,9 @@
<?php
namespace Misuzu;
use Index\Environment;
use Index\Data\DbTools;
use Syokuhou\DbConfig;
use Syokuhou\SharpConfig;
use Index\Db\DbTools;
use Index\Config\Db\DbConfig;
use Index\Config\Fs\FsConfig;
define('MSZ_STARTUP', microtime(true));
define('MSZ_ROOT', __DIR__);
@ -19,11 +18,11 @@ define('MSZ_ASSETS', MSZ_ROOT . '/assets');
require_once MSZ_ROOT . '/vendor/autoload.php';
Environment::setDebug(MSZ_DEBUG);
error_reporting(MSZ_DEBUG ? -1 : 0);
mb_internal_encoding('UTF-8');
date_default_timezone_set('UTC');
date_default_timezone_set('GMT');
$cfg = SharpConfig::fromFile(MSZ_CONFIG . '/config.cfg');
$cfg = FsConfig::fromFile(MSZ_CONFIG . '/config.cfg');
if($cfg->hasValues('sentry:dsn'))
(function($cfg) {

801
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,5 @@
{
"dependencies": {
"@swc/core": "^1.5.24",
"autoprefixer": "^10.4.19",
"cssnano": "^6.1.2",
"postcss": "^8.4.38"
"@railcomm/assproc": "^1.0.0"
}
}

View file

@ -4,3 +4,6 @@ parameters:
- src
bootstrapFiles:
- misuzu.php
dynamicConstantNames:
- MSZ_CLI
- MSZ_DEBUG

View file

@ -4,7 +4,7 @@ namespace Misuzu;
use Exception;
use Misuzu\Auth\AuthTokenCookie;
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));

View file

@ -24,4 +24,4 @@ if($authInfo->isLoggedIn()) {
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
}
Tools::redirect($msz->getURLs()->format('index'));;
Tools::redirect($msz->getUrls()->format('index'));;

View file

@ -4,7 +4,7 @@ namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('settings-account'));

View file

@ -4,7 +4,7 @@ namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));

View file

@ -3,7 +3,7 @@ namespace Misuzu;
use Misuzu\Auth\AuthTokenCookie;
$urls = $msz->getURLs();
$urls = $msz->getUrls();
if(CSRF::validateRequest()) {
$tokenInfo = $msz->getAuthInfo()->getTokenInfo();

View file

@ -5,7 +5,7 @@ use RuntimeException;
use Misuzu\TOTPGenerator;
use Misuzu\Auth\AuthTokenCookie;
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$authInfo = $msz->getAuthInfo();
if($authInfo->isLoggedIn()) {
Tools::redirect($urls->format('index'));

View file

@ -4,7 +4,7 @@ namespace Misuzu;
use RuntimeException;
$usersCtx = $msz->getUsersContext();
$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getURLs()->format('index');
$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getUrls()->format('index');
if(!Tools::isLocalURL($redirect))
Template::displayInfo('Possible request forgery detected.', 403);

View file

@ -32,14 +32,14 @@ if($mode === 'mark') {
$forumCategories->updateUserReadCategory($userInfo, $categoryInfo);
}
Tools::redirect($msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
Tools::redirect($msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
return;
}
Template::render('confirm', [
'title' => 'Mark forum as read',
'message' => 'Are you sure you want to mark ' . ($categoryId < 1 ? 'the entire' : 'this') . ' forum as read?',
'return' => $msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
'return' => $msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
'params' => [
'forum' => $categoryId,
]

View file

@ -94,7 +94,7 @@ MD;
$markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $ranking->position,
$ranking->user?->getName() ?? 'Deleted User',
$msz->getSiteInfo()->getURL(),
$msz->getURLs()->format('user-profile', ['user' => $ranking->userId]),
$msz->getUrls()->format('user-profile', ['user' => $ranking->userId]),
number_format($ranking->postsCount));
}

View file

@ -3,7 +3,7 @@ namespace Misuzu;
use RuntimeException;
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$forumCtx = $msz->getForumContext();
$forumPosts = $forumCtx->getPosts();
$usersCtx = $msz->getUsersContext();

View file

@ -3,9 +3,10 @@ namespace Misuzu;
use stdClass;
use RuntimeException;
use Index\DateTime;
use Misuzu\Forum\ForumTopicInfo;
use Misuzu\Parsers\Parser;
use Index\XDateTime;
use Carbon\CarbonImmutable;
$authInfo = $msz->getAuthInfo();
if(!$authInfo->isLoggedIn())
@ -152,11 +153,11 @@ if(!empty($_POST)) {
if($mode === 'create') {
$postTimeout = $cfg->getInteger('forum.posting.timeout', 5);
if($postTimeout > 0) {
$postTimeoutThreshold = DateTime::now()->modify(sprintf('-%d seconds', $postTimeout));
$postTimeoutThreshold = new CarbonImmutable(sprintf('-%d seconds', $postTimeout));
$lastPostCreatedAt = $forumPosts->getUserLastPostCreatedAt($currentUser);
if($lastPostCreatedAt->isMoreThan($postTimeoutThreshold)) {
$waitSeconds = $postTimeout + ($lastPostCreatedAt->getUnixTimeSeconds() - time());
if(XDateTime::compare($lastPostCreatedAt, $postTimeoutThreshold) > 0) {
$waitSeconds = $postTimeout + ((int)$lastPostCreatedAt->format('U') - time());
$notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($waitSeconds));
$notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident.";
@ -257,7 +258,7 @@ if(!empty($_POST)) {
if(empty($notices)) {
// does this ternary ever return forum-topic?
$redirect = $msz->getURLs()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
$redirect = $msz->getUrls()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
'topic' => $topicId ?? 0,
'post' => $postId ?? 0,
]);

View file

@ -4,7 +4,7 @@ namespace Misuzu;
use stdClass;
use RuntimeException;
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$forumCtx = $msz->getForumContext();
$forumCategories = $forumCtx->getCategories();
$forumTopics = $forumCtx->getTopics();

View file

@ -3,9 +3,9 @@ namespace Misuzu;
use DateTimeInterface;
use RuntimeException;
use Index\DateTime;
use Index\XArray;
use Misuzu\Changelog\Changelog;
use Carbon\CarbonImmutable;
use Index\{XArray,XDateTime};
$authInfo = $msz->getAuthInfo();
if(!$authInfo->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
@ -15,7 +15,7 @@ $changeActions = [];
foreach(Changelog::ACTIONS as $action)
$changeActions[$action] = Changelog::actionText($action);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$changelog = $msz->getChangelog();
$changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$changeInfo = null;
@ -58,8 +58,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(empty($createdAt))
$createdAt = null;
else {
$createdAt = DateTime::createFromFormat(DateTimeInterface::ATOM, $createdAt . ':00Z');
if($createdAt->getUnixTimeSeconds() < 0)
$createdAt = CarbonImmutable::createFromFormat(DateTimeInterface::ATOM, $createdAt . ':00Z');
if((int)$createdAt->format('U') < 0)
$createdAt = null;
}
@ -72,7 +72,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$summary = null;
if($body === $changeInfo->getBody())
$body = null;
if($createdAt !== null && $createdAt->equals($changeInfo->getCreatedAt()))
if($createdAt !== null && XDateTime::compare($createdAt, $changeInfo->getCreatedAt()) === 0)
$createdAt = null;
$updateUserInfo = $userId !== $changeInfo->getUserId();

View file

@ -6,7 +6,7 @@ use RuntimeException;
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
Template::throwError(403);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$changelog = $msz->getChangelog();
$tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
$loadTagInfo = fn() => $changelog->getTag($tagId);

View file

@ -5,7 +5,7 @@ $authInfo = $msz->getAuthInfo();
if(!$authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
Template::throwError(403);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$forumCtx = $msz->getForumContext();
$forumTopicRedirects = $forumCtx->getTopicRedirects();

View file

@ -97,7 +97,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
[$emoteInfo->getId()]
);
Tools::redirect($msz->getURLs()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
Tools::redirect($msz->getUrls()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
return;
}

View file

@ -37,7 +37,7 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
}
}
Tools::redirect($msz->getURLs()->format('manage-general-emoticons'));
Tools::redirect($msz->getUrls()->format('manage-general-emoticons'));
return;
}

View file

@ -13,7 +13,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$valueName = $valueInfo->getName();
$msz->createAuditLog('CONFIG_DELETE', [$valueName]);
$cfg->removeValues($valueName);
Tools::redirect($msz->getURLs()->format('manage-general-settings'));
Tools::redirect($msz->getUrls()->format('manage-general-settings'));
return;
}

View file

@ -1,7 +1,7 @@
<?php
namespace Misuzu;
use Syokuhou\DbConfig;
use Index\Config\Db\DbConfig;
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
Template::throwError(403);
@ -73,7 +73,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]);
$applyFunc($sName, $sValue);
Tools::redirect($msz->getURLs()->format('manage-general-settings'));
Tools::redirect($msz->getUrls()->format('manage-general-settings'));
return;
}

View file

@ -6,7 +6,7 @@ use RuntimeException;
if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
Template::throwError(403);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$news = $msz->getNews();
$categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$loadCategoryInfo = fn() => $news->getCategory(categoryId: $categoryId);

View file

@ -7,7 +7,7 @@ $authInfo = $msz->getAuthInfo();
if(!$authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
Template::throwError(403);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$news = $msz->getNews();
$postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
$loadPostInfo = fn() => $news->getPost($postId);

View file

@ -3,13 +3,13 @@ namespace Misuzu;
use DateTimeInterface;
use RuntimeException;
use Index\DateTime;
use Carbon\CarbonImmutable;
$authInfo = $msz->getAuthInfo();
if(!$authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
Template::throwError(403);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$usersCtx = $msz->getUsersContext();
$bans = $usersCtx->getBans();
@ -56,7 +56,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if($expires === -1) {
$expires = null;
} elseif($expires === -2) {
$expires = DateTime::createFromFormat(DateTimeInterface::ATOM, $expiresCustom . ':00Z');
$expires = CarbonImmutable::createFromFormat(DateTimeInterface::ATOM, $expiresCustom . ':00Z');
} else {
echo 'Invalid duration specified.';
break;

View file

@ -13,7 +13,7 @@ $hasUserId = filter_has_var(INPUT_GET, 'u');
if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId))
Template::throwError(400);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$usersCtx = $msz->getUsersContext();
$modNotes = $usersCtx->getModNotes();

View file

@ -3,7 +3,7 @@ namespace Misuzu;
use RuntimeException;
use Index\Colour\Colour;
use Index\Colour\ColourRGB;
use Index\Colour\ColourRgb;
use Misuzu\Perm;
$authInfo = $msz->getAuthInfo();
@ -42,6 +42,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
break;
}
$roleString = (string)filter_input(INPUT_POST, 'ur_string');
$roleName = (string)filter_input(INPUT_POST, 'ur_name');
$roleHide = !empty($_POST['ur_hidden']);
$roleLeavable = !empty($_POST['ur_leavable']);
@ -54,6 +55,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$colourBlue = (int)filter_input(INPUT_POST, 'ur_col_blue', FILTER_SANITIZE_NUMBER_INT);
Template::set([
'role_ur_string' => $roleString,
'role_ur_name' => $roleName,
'role_ur_hidden' => $roleHide,
'role_ur_leavable' => $roleLeavable,
@ -84,7 +86,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$roleColour = $colourInherit
? Colour::none()
: new ColourRGB($colourRed, $colourGreen, $colourBlue);
: new ColourRgb($colourRed, $colourGreen, $colourBlue);
if(mb_strlen($roleDesc) > 1000) {
echo 'Description may not be longer than 1000 characters.';
@ -96,11 +98,31 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
break;
}
if(strlen($roleString) > 20) {
echo 'Role string may not be longer than 20 characters.';
break;
}
if(strlen($roleString) > 1 && !ctype_alpha($roleString[0])) {
echo 'Role string most start with an alphabetical character.';
break;
}
if($isNew) {
$roleInfo = $roles->createRole($roleName, $roleRank, $roleColour, $roleTitle, $roleDesc, $roleHide, $roleLeavable);
$roleInfo = $roles->createRole(
$roleName,
$roleRank,
$roleColour,
string: $roleString,
title: $roleTitle,
description: $roleDesc,
hidden: $roleHide,
leavable: $roleLeavable
);
} else {
if($roleName === $roleInfo->getName())
$roleName = null;
if($roleString === $roleInfo->getString())
$roleString = null;
if($roleHide === $roleInfo->isHidden())
$roleHide = null;
if($roleLeavable === $roleInfo->isLeavable())
@ -115,7 +137,17 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if((string)$roleColour === (string)$roleInfo->getColour())
$roleColour = null;
$roles->updateRole($roleInfo, $roleName, $roleRank, $roleColour, $roleTitle, $roleDesc, $roleHide, $roleLeavable);
$roles->updateRole(
$roleInfo,
string: $roleString,
name: $roleName,
rank: $roleRank,
colour: $roleColour,
title: $roleTitle,
description: $roleDesc,
hidden: $roleHide,
leavable: $roleLeavable
);
}
$msz->createAuditLog(
@ -136,7 +168,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$msz->getConfig()->setBoolean('perms.needsRecalc', true);
}
Tools::redirect($msz->getURLs()->format('manage-role', ['role' => $roleInfo->getId()]));
Tools::redirect($msz->getUrls()->format('manage-role', ['role' => $roleInfo->getId()]));
return;
}

View file

@ -12,7 +12,7 @@ $viewerPerms = $authInfo->getPerms('user');
if(!$authInfo->isLoggedIn())
Template::throwError(403);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$roles = $usersCtx->getRoles();

View file

@ -7,7 +7,7 @@ $authInfo = $msz->getAuthInfo();
if(!$authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
Template::throwError(403);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$warns = $usersCtx->getWarnings();

View file

@ -5,7 +5,6 @@ use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Index\ByteFormat;
use Index\DateTime;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\Assets\UserAvatarAsset;
@ -15,7 +14,7 @@ $userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0;
$profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
$urls = $msz->getURLs();
$urls = $msz->getUrls();
$usersCtx = $msz->getUsersContext();
$users = $usersCtx->getUsers();
$forumCtx = $msz->getForumContext();

View file

@ -3,7 +3,6 @@ namespace Misuzu;
use ZipArchive;
use Index\XString;
use Index\IO\FileStream;
use Misuzu\Users\UserInfo;
$authInfo = $msz->getAuthInfo();
@ -42,7 +41,7 @@ function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, ar
}
$tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . sprintf('msz-user-data-%s-%s-%s.tmp', $userId, $baseName, XString::random(8));
$tmpStream = FileStream::newWrite($tmpName);
$tmpHandle = fopen($tmpName, 'wb');
try {
$stmt = $dbConn->prepare(sprintf('SELECT %s FROM msz_%s WHERE %s = ?', implode(', ', $fields), $baseName, $userIdField));
@ -85,12 +84,12 @@ function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, ar
$row[$fieldInfo['name']] = $fieldValue;
}
$tmpStream->write(json_encode($row, JSON_INVALID_UTF8_SUBSTITUTE));
$tmpStream->write("\n");
fwrite($tmpHandle, json_encode($row, JSON_INVALID_UTF8_SUBSTITUTE));
fwrite($tmpHandle, "\n");
}
} finally {
$tmpStream->flush();
$tmpStream->close();
fflush($tmpHandle);
fclose($tmpHandle);
}
$archive->addFile($tmpName, $baseName . '.jsonl');

View file

@ -37,7 +37,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
}
if($activeSessionKilled) {
Tools::redirect($msz->getURLs()->format('index'));
Tools::redirect($msz->getUrls()->format('index'));
return;
} else break;
}

View file

@ -2,10 +2,7 @@
namespace Misuzu;
use RuntimeException;
use Misuzu\Auth\AuthTokenBuilder;
use Misuzu\Auth\AuthTokenCookie;
use Misuzu\Auth\AuthTokenInfo;
use Sasae\SasaeEnvironment;
use Misuzu\Auth\{AuthTokenBuilder,AuthTokenCookie,AuthTokenInfo};
require_once __DIR__ . '/../misuzu.php';

View file

@ -2,28 +2,23 @@
namespace Misuzu\AuditLog;
use InvalidArgumentException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class AuditLog {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
public function countLogs(
UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null
?string $remoteAddr = null
): int {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$hasUserInfo = $userInfo !== null;
$hasRemoteAddr = $remoteAddr !== null;
@ -59,13 +54,11 @@ class AuditLog {
public function getLogs(
UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null,
?string $remoteAddr = null,
?Pagination $pagination = null
): iterable {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$hasUserInfo = $userInfo !== null;
$hasRemoteAddr = $remoteAddr !== null;
@ -106,13 +99,11 @@ class AuditLog {
UserInfo|string|null $userInfo,
string $action,
array $params = [],
IPAddress|string $remoteAddr = '::1',
string $remoteAddr = '::1',
string $countryCode = 'XX'
): void {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
// action names should have stricter validation,
// i do want to switch to a lowercase colon separated format later but i'll save that for the unified log in Hanyuu

View file

@ -2,9 +2,8 @@
namespace Misuzu\AuditLog;
use ValueError;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class AuditLogInfo {
public function __construct(
@ -16,7 +15,7 @@ class AuditLogInfo {
private string $country,
) {}
public static function fromResult(IDbResult $result): AuditLogInfo {
public static function fromResult(DbResult $result): AuditLogInfo {
return new AuditLogInfo(
userId: $result->getStringOrNull(0),
action: $result->getString(1),
@ -47,18 +46,14 @@ class AuditLogInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function getRemoteAddressRaw(): string {
public function getRemoteAddress(): string {
return $this->address;
}
public function getRemoteAddress(): IPAddress {
return IPAddress::parse($this->address);
}
public function getCountryCode(): string {
return $this->country;
}

View file

@ -1,8 +1,8 @@
<?php
namespace Misuzu\Auth;
use Index\Data\IDbConnection;
use Syokuhou\IConfig;
use Index\Config\Config;
use Index\Db\DbConnection;
class AuthContext {
private Sessions $sessions;
@ -10,9 +10,9 @@ class AuthContext {
private RecoveryTokens $recoveryTokens;
private TwoFactorAuthSessions $tfaSessions;
private IConfig $config;
private Config $config;
public function __construct(IDbConnection $dbConn, IConfig $config) {
public function __construct(DbConnection $dbConn, Config $config) {
$this->config = $config;
$this->sessions = new Sessions($dbConn);
$this->loginAttempts = new LoginAttempts($dbConn);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu\Auth;
use Index\XArray;
use Misuzu\Auth\SessionInfo;
use Misuzu\Forum\ForumCategoryInfo;
use Misuzu\Perms\IPermissionResult;

View file

@ -0,0 +1,63 @@
<?php
namespace Misuzu\Auth;
use RuntimeException;
use Misuzu\Users\{UsersContext,UserInfo};
use Aiwass\Server\{RpcActionHandler,RpcProcedure};
use Index\Config\Config;
final class AuthRpcActions extends RpcActionHandler {
public function __construct(
private Config $impersonateConfig,
private UsersContext $usersCtx,
private AuthContext $authCtx
) {}
private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
if($impersonator->isSuperUser())
return true;
$whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId()));
return in_array($targetId, $whitelist, true);
}
#[RpcProcedure('misuzu:auth:attemptMisuzuAuth')]
public function procAttemptMisuzuAuth(string $remoteAddr, string $token): array {
$tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token);
if(!$tokenInfo->isEmpty())
$token = $tokenInfo->getSessionToken();
$sessions = $this->authCtx->getSessions();
try {
$sessionInfo = $sessions->getSession(sessionToken: $token);
} catch(RuntimeException $ex) {
return ['method' => 'misuzu', 'error' => 'token'];
}
if($sessionInfo->hasExpired()) {
$sessions->deleteSessions(sessionInfos: $sessionInfo);
return ['method' => 'misuzu', 'error' => 'expired'];
}
$sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
$users = $this->usersCtx->getUsers();
$userInfo = $users->getUser($sessionInfo->getUserId(), 'id');
if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
$userInfoReal = $userInfo;
try {
$userInfo = $users->getUser($tokenInfo->getImpersonatedUserId(), 'id');
} catch(RuntimeException $ex) {
$userInfo = $userInfoReal;
}
}
return [
'method' => 'misuzu',
'type' => 'user',
'user' => $userInfo->getId(),
'expires' => $sessionInfo->getExpiresTime(),
];
}
}

View file

@ -2,8 +2,7 @@
namespace Misuzu\Auth;
use RuntimeException;
use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64;
use Index\UriBase64;
class AuthTokenPacker {
private const EPOCH_V2 = 1682985600;
@ -64,32 +63,40 @@ class AuthTokenPacker {
$unpackTime = unpack('Nts', $timestamp);
if($unpackTime === false)
throw new RuntimeException('$token does not contain a valid timestamp.');
return AuthTokenInfo::empty();
$timestamp = $unpackTime['ts'] + self::EPOCH_V2;
$stream = MemoryStream::fromString($data);
$stream->seek(0);
$handle = fopen('php://memory', 'rb+');
if($handle === false)
return AuthTokenInfo::empty();
for(;;) {
$length = $stream->readChar();
if($length === null)
break;
try {
fwrite($handle, $data);
fseek($handle, 0);
$length = ord($length);
if($length < 1)
break;
for(;;) {
$length = fgetc($handle);
if($length === false)
break;
$name = $stream->read($length);
$value = null;
$length = $stream->readChar();
if($length !== null) {
$length = ord($length);
if($length > 0)
$value = $stream->read($length);
}
if($length < 1)
break;
$builder->setProperty($name, $value);
$name = fread($handle, $length);
$value = null;
$length = fgetc($handle);
if($length !== false) {
$length = ord($length);
if($length > 0)
$value = fread($handle, $length);
}
$builder->setProperty($name, $value);
}
} finally {
fclose($handle);
}
} else
return AuthTokenInfo::empty();

View file

@ -1,10 +1,9 @@
<?php
namespace Misuzu\Auth;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Misuzu\ClientInfo;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class LoginAttemptInfo {
public function __construct(
@ -17,7 +16,7 @@ class LoginAttemptInfo {
private string $clientInfo,
) {}
public static function fromResult(IDbResult $result): LoginAttemptInfo {
public static function fromResult(DbResult $result): LoginAttemptInfo {
return new LoginAttemptInfo(
userId: $result->getStringOrNull(0),
success: $result->getBoolean(1),
@ -41,14 +40,10 @@ class LoginAttemptInfo {
return $this->success;
}
public function getRemoteAddressRaw(): string {
public function getRemoteAddress(): string {
return $this->remoteAddr;
}
public function getRemoteAddress(): IPAddress {
return IPAddress::parse($this->remoteAddr);
}
public function getCountryCode(): string {
return $this->countryCode;
}
@ -57,8 +52,8 @@ class LoginAttemptInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function getUserAgentString(): string {

View file

@ -1,10 +1,7 @@
<?php
namespace Misuzu\Auth;
use Index\TimeSpan;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
@ -15,22 +12,18 @@ class LoginAttempts {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
public function countAttempts(
?bool $success = null,
UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null
?string $remoteAddr = null,
int|null $timeRange = null
): int {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan)
$timeRange = (int)$timeRange->totalSeconds();
$hasSuccess = $success !== null;
$hasUserInfo = $userInfo !== null;
@ -69,7 +62,7 @@ class LoginAttempts {
return $count;
}
public function countRemainingAttempts(IPAddress|string $remoteAddr): int {
public function countRemainingAttempts(string $remoteAddr): int {
return self::REMAINING_MAX - $this->countAttempts(
success: false,
timeRange: self::REMAINING_WINDOW,
@ -80,16 +73,12 @@ class LoginAttempts {
public function getAttempts(
?bool $success = null,
UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null,
?string $remoteAddr = null,
?int $timeRange = null,
?Pagination $pagination = null
): iterable {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan)
$timeRange = (int)$timeRange->totalSeconds();
$hasSuccess = $success !== null;
$hasUserInfo = $userInfo !== null;
@ -132,14 +121,12 @@ class LoginAttempts {
public function recordAttempt(
bool $success,
IPAddress|string $remoteAddr,
string $remoteAddr,
string $countryCode,
string $userAgentString,
?ClientInfo $clientInfo = null,
UserInfo|string|null $userInfo = null
): void {
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();

View file

@ -1,9 +1,8 @@
<?php
namespace Misuzu\Auth;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class RecoveryTokenInfo {
public const LIFETIME = 60 * 60;
@ -13,7 +12,7 @@ class RecoveryTokenInfo {
private int $created;
private ?string $code;
public function __construct(IDbResult $result) {
public function __construct(DbResult $result) {
$this->userId = (string)$result->getInteger(0);
$this->remoteAddr = $result->getString(1);
$this->created = $result->getInteger(2);
@ -24,28 +23,24 @@ class RecoveryTokenInfo {
return $this->userId;
}
public function getRemoteAddressRaw(): string {
public function getRemoteAddress(): string {
return $this->remoteAddr;
}
public function getRemoteAddress(): IPAddress {
return IPAddress::parse($this->remoteAddr);
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function getExpiresTime(): int {
return $this->created + self::LIFETIME;
}
public function getExpiresAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created + self::LIFETIME);
public function getExpiresAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->getExpiresTime());
}
public function hasExpired(): bool {

View file

@ -3,10 +3,8 @@ namespace Misuzu\Auth;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Index\Serialisation\Base32;
use Index\Base32;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
@ -14,7 +12,7 @@ use Misuzu\Users\UserInfo;
class RecoveryTokens {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
@ -25,14 +23,12 @@ class RecoveryTokens {
public function getToken(
UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null,
?string $remoteAddr = null,
?string $verifyCode = null,
?bool $isUnused = null
): RecoveryTokenInfo {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$hasUserInfo = $userInfo !== null;
$hasRemoteAddr = $remoteAddr !== null;
@ -76,12 +72,11 @@ class RecoveryTokens {
public function createToken(
UserInfo|string $userInfo,
IPAddress|string $remoteAddr
string $remoteAddr
): RecoveryTokenInfo {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$verifyCode = self::generateCode();
$stmt = $this->cache->get('INSERT INTO msz_users_password_resets (user_id, reset_ip, verification_code) VALUES (?, INET6_ATON(?), ?)');

View file

@ -1,10 +1,9 @@
<?php
namespace Misuzu\Auth;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Misuzu\ClientInfo;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class SessionInfo {
public function __construct(
@ -22,7 +21,7 @@ class SessionInfo {
private ?int $lastActive,
) {}
public static function fromResult(IDbResult $result): SessionInfo {
public static function fromResult(DbResult $result): SessionInfo {
return new SessionInfo(
id: $result->getString(0),
userId: $result->getString(1),
@ -51,26 +50,18 @@ class SessionInfo {
return $this->token;
}
public function getFirstRemoteAddressRaw(): string {
public function getFirstRemoteAddress(): string {
return $this->firstRemoteAddr;
}
public function getFirstRemoteAddress(): IPAddress {
return IPAddress::parse($this->firstRemoteAddr);
}
public function hasLastRemoteAddress(): bool {
return $this->lastRemoteAddr !== null;
}
public function getLastRemoteAddressRaw(): string {
public function getLastRemoteAddress(): ?string {
return $this->lastRemoteAddr;
}
public function getLastRemoteAddress(): ?IPAddress {
return $this->lastRemoteAddr === null ? null : IPAddress::parse($this->lastRemoteAddr);
}
public function getUserAgentString(): string {
return $this->userAgent;
}
@ -91,8 +82,8 @@ class SessionInfo {
return $this->expires;
}
public function getExpiresAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->expires);
public function getExpiresAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->expires);
}
public function shouldBumpExpires(): bool {
@ -107,8 +98,8 @@ class SessionInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function hasLastActive(): bool {
@ -119,7 +110,7 @@ class SessionInfo {
return $this->lastActive;
}
public function getLastActiveAt(): ?DateTime {
return $this->lastActive === null ? null : DateTime::fromUnixTimeSeconds($this->lastActive);
public function getLastActiveAt(): ?CarbonImmutable {
return $this->lastActive === null ? null : CarbonImmutable::createFromTimestampUTC($this->lastActive);
}
}

View file

@ -4,19 +4,16 @@ namespace Misuzu\Auth;
use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class Sessions {
private IDbConnection $dbConn;
private DbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
@ -123,15 +120,13 @@ class Sessions {
public function createSession(
UserInfo|string $userInfo,
IPAddress|string $remoteAddr,
string $remoteAddr,
string $countryCode,
string $userAgentString,
?ClientInfo $clientInfo = null
): SessionInfo {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$sessionToken = self::generateToken();
$clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));
@ -243,7 +238,7 @@ class Sessions {
public function recordSessionActivity(
SessionInfo|string|null $sessionInfo = null,
?string $sessionToken = null,
IPAddress|string|null $remoteAddr = null
?string $remoteAddr = null
): void {
if($sessionInfo === null && $sessionToken === null)
throw new InvalidArgumentException('Either $sessionInfo or $sessionToken needs to be set.');
@ -251,8 +246,6 @@ class Sessions {
throw new InvalidArgumentException('Only one of $sessionInfo and $sessionToken may be set at once.');
if($sessionInfo instanceof SessionInfo)
$sessionInfo = $sessionInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$hasSessionInfo = $sessionInfo !== null;
$hasSessionToken = $sessionToken !== null;

View file

@ -2,14 +2,13 @@
namespace Misuzu\Auth;
use Index\XString;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\Users\UserInfo;
class TwoFactorAuthSessions {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}

View file

@ -1,13 +1,23 @@
<?php
namespace Misuzu;
use Index\Security\CSRFP;
use Index\CsrfToken;
final class CSRF {
private static CSRFP $instance;
private static CsrfToken $instance;
private static string $secretKey = '';
public static function create(string $identity, ?string $secretKey = null): CsrfToken {
if($secretKey === null)
$secretKey = self::$secretKey;
else
self::$secretKey = $secretKey;
return new CsrfToken($secretKey, $identity);
}
public static function init(string $secretKey, string $identity): void {
self::$instance = new CSRFP($secretKey, $identity);
self::$instance = self::create($identity, $secretKey);
}
public static function validate(string $token, int $tolerance = -1): bool {

View file

@ -1,8 +1,8 @@
<?php
namespace Misuzu\Changelog;
use Index\DateTime;
use Index\Data\IDbResult;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class ChangeInfo {
public function __construct(
@ -14,7 +14,7 @@ class ChangeInfo {
private string $body,
) {}
public static function fromResult(IDbResult $result): ChangeInfo {
public static function fromResult(DbResult $result): ChangeInfo {
return new ChangeInfo(
id: $result->getString(0),
userId: $result->getStringOrNull(1),
@ -53,8 +53,8 @@ class ChangeInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function getDate(): string {

View file

@ -2,8 +2,8 @@
namespace Misuzu\Changelog;
use Stringable;
use Index\DateTime;
use Index\Data\IDbResult;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class ChangeTagInfo implements Stringable {
private string $id;
@ -13,7 +13,7 @@ class ChangeTagInfo implements Stringable {
private int $archived;
private int $changes;
public function __construct(IDbResult $result) {
public function __construct(DbResult $result) {
$this->id = (string)$result->getInteger(0);
$this->name = $result->getString(1);
$this->description = $result->getString(2);
@ -38,16 +38,16 @@ class ChangeTagInfo implements Stringable {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function getArchivedTime(): int {
return $this->archived;
}
public function getArchivedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getArchivedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->archived);
}
public function isArchived(): bool {

View file

@ -3,11 +3,8 @@ namespace Misuzu\Changelog;
use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use DateTimeInterface;
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
@ -15,12 +12,12 @@ class Changelog {
// not a strict list but useful to have
public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert'];
private IDbConnection $dbConn;
private DbConnection $dbConn;
private DbStatementCache $cache;
private array $tags = [];
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
@ -66,13 +63,13 @@ class Changelog {
public function countChanges(
UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null,
DateTimeInterface|int|null $dateTime = null,
?array $tags = null
): int {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds();
if($dateTime instanceof DateTimeInterface)
$dateTime = (int)$dateTime->format('U');
$args = 0;
$hasUserInfo = $userInfo !== null;
@ -118,14 +115,14 @@ class Changelog {
public function getChanges(
UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null,
DateTimeInterface|int|null $dateTime = null,
?array $tags = null,
?Pagination $pagination = null
): iterable {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds();
if($dateTime instanceof DateTimeInterface)
$dateTime = (int)$dateTime->format('U');
$args = 0;
$hasUserInfo = $userInfo !== null;
@ -189,14 +186,14 @@ class Changelog {
string $summary,
string $body = '',
UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null
DateTimeInterface|int|null $createdAt = null
): ChangeInfo {
if(is_string($action))
$action = self::convertToActionId($action);
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds();
if($createdAt instanceof DateTimeInterface)
$createdAt = (int)$createdAt->format('U');
$summary = trim($summary);
if(empty($summary))
@ -233,7 +230,7 @@ class Changelog {
?string $body = null,
bool $updateUserInfo = false,
UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null
DateTimeInterface|int|null $createdAt = null
): void {
if($infoOrId instanceof ChangeInfo)
$infoOrId = $infoOrId->getId();
@ -242,8 +239,8 @@ class Changelog {
$action = self::convertToActionId($action);
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds();
if($createdAt instanceof DateTimeInterface)
$createdAt = (int)$createdAt->format('U');
if($summary !== null) {
$summary = trim($summary);

View file

@ -3,18 +3,20 @@ namespace Misuzu\Changelog;
use ErrorException;
use RuntimeException;
use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Syndication\FeedBuilder;
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\{Comments,CommentsEx};
use Misuzu\Feeds\{Feed,FeedItem,AtomFeedSerializer,RssFeedSerializer};
use Misuzu\URLs\{URLInfo,URLRegistry};
use Misuzu\Users\UsersContext;
final class ChangelogRoutes extends RouteHandler {
final class ChangelogRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait;
public function __construct(
private SiteInfo $siteInfo,
private URLRegistry $urls,
private UrlRegistry $urls,
private Changelog $changelog,
private UsersContext $usersCtx,
private AuthInfo $authInfo,
@ -27,7 +29,7 @@ final class ChangelogRoutes extends RouteHandler {
}
#[HttpGet('/changelog')]
#[URLInfo('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
#[UrlFormat('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
public function getIndex($response, $request) {
$filterDate = (string)$request->getParam('date');
$filterUser = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
@ -96,8 +98,8 @@ final class ChangelogRoutes extends RouteHandler {
}
#[HttpGet('/changelog/change/([0-9]+)')]
#[URLInfo('changelog-change', '/changelog/change/<change>')]
#[URLInfo('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
#[UrlFormat('changelog-change', '/changelog/change/<change>')]
#[UrlFormat('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
public function getChange($response, $request, string $changeId) {
try {
$changeInfo = $this->changelog->getChange($changeId);
@ -117,44 +119,29 @@ final class ChangelogRoutes extends RouteHandler {
]);
}
private function createFeed(string $feedMode): Feed {
#[HttpGet('/changelog.(xml|rss|atom)')]
#[UrlFormat('changelog-feed', '/changelog.xml')]
public function getFeed($response) {
$response->setContentType('application/rss+xml; charset=utf-8');
$siteName = $this->siteInfo->getName();
$siteUrl = $this->siteInfo->getURL();
$changes = $this->changelog->getChanges(pagination: new Pagination(10));
$feed = (new Feed)
->setTitle($siteName . ' » Changelog')
->setDescription('Live feed of changes to ' . $siteName . '.')
->setContentUrl($this->siteInfo->getURL() . $this->urls->format('changelog-index'))
->setFeedUrl($this->siteInfo->getURL() . $this->urls->format("changelog-feed-{$feedMode}"));
$feed = new FeedBuilder;
$feed->setTitle(sprintf('%s » Changelog', $siteName));
$feed->setDescription(sprintf('Live feed of changes to %s.', $siteName));
$feed->setContentUrl($siteUrl . $this->urls->format('changelog-index'));
$feed->setFeedUrl($siteUrl . $this->urls->format('changelog-feed'));
foreach($changes as $change) {
$changeUrl = $this->siteInfo->getURL() . $this->urls->format('changelog-change', ['change' => $change->getId()]);
$commentsUrl = $this->siteInfo->getURL() . $this->urls->format('changelog-change-comments', ['change' => $change->getId()]);
foreach($changes as $change)
$feed->createEntry(function($item) use ($change, $siteUrl) {
$item->setTitle(sprintf('%s: %s', $change->getActionText(), $change->getSummary()));
$item->setCreatedAt($change->getCreatedTime());
$item->setContentUrl($siteUrl . $this->urls->format('changelog-change', ['change' => $change->getId()]));
$item->setCommentsUrl($siteUrl . $this->urls->format('changelog-change-comments', ['change' => $change->getId()]));
});
$feedItem = (new FeedItem)
->setTitle($change->getActionText() . ': ' . $change->getSummary())
->setCreationDate($change->getCreatedTime())
->setUniqueId($changeUrl)
->setContentUrl($changeUrl)
->setCommentsUrl($commentsUrl);
$feed->addItem($feedItem);
}
return $feed;
}
#[HttpGet('/changelog.rss')]
#[URLInfo('changelog-feed-rss', '/changelog.rss')]
public function getFeedRSS($response) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed($this->createFeed('rss'));
}
#[HttpGet('/changelog.atom')]
#[URLInfo('changelog-feed-atom', '/changelog.atom')]
public function getFeedAtom($response) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed($this->createFeed('atom'));
return $feed->toXmlString();
}
}

View file

@ -5,10 +5,12 @@ use stdClass;
use JsonSerializable;
use RuntimeException;
use Stringable;
use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;
use DeviceDetector\{ClientHints,DeviceDetector};
use Index\Json\{JsonProperty,JsonSerializableTrait};
class ClientInfo implements Stringable, JsonSerializable {
use JsonSerializableTrait;
private const SERIALIZE_VERSION = 1;
public function __construct(
@ -19,6 +21,39 @@ class ClientInfo implements Stringable, JsonSerializable {
private string $modelName
) {}
#[JsonProperty('version')]
public function getVersion(): int {
return self::SERIALIZE_VERSION;
}
#[JsonProperty('bot')]
public function getBotInfo(): bool|array|null {
if($this->botInfo === true || is_array($this->botInfo))
return $this->botInfo;
return null;
}
#[JsonProperty('client')]
public function getClientInfo(): ?array {
return $this->clientInfo;
}
#[JsonProperty('os')]
public function getOsInfo(): ?array {
return $this->osInfo;
}
#[JsonProperty('vendor', omitIfValue: '')]
public function getVendorName(): string {
return $this->brandName;
}
#[JsonProperty('model', omitIfValue: '')]
public function getModelName(): string {
return $this->modelName;
}
public function __toString(): string {
if($this->botInfo === true || is_array($this->botInfo)) {
if($this->botInfo === true)
@ -64,24 +99,6 @@ class ClientInfo implements Stringable, JsonSerializable {
return json_encode($this);
}
public function jsonSerialize(): mixed {
$data = new stdClass;
$data->version = self::SERIALIZE_VERSION;
if($this->botInfo === true || is_array($this->botInfo))
$data->bot = $this->botInfo;
if($this->clientInfo !== null)
$data->client = $this->clientInfo;
if($this->osInfo !== null)
$data->os = $this->osInfo;
if($this->brandName !== '')
$data->vendor = $this->brandName;
if($this->modelName !== '')
$data->model = $this->modelName;
return $data;
}
public static function decode(string $encoded): self {
$data = json_decode($encoded, true);
$version = $data['version'] ?? 0;

View file

@ -3,17 +3,15 @@ namespace Misuzu\Comments;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class Comments {
private IDbConnection $dbConn;
private DbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}

View file

@ -1,9 +1,9 @@
<?php
namespace Misuzu\Comments;
use Index\DateTime;
use Index\Data\IDbResult;
use Misuzu\Users\UserInfo;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class CommentsCategoryInfo {
public function __construct(
@ -15,7 +15,7 @@ class CommentsCategoryInfo {
private int $comments,
) {}
public static function fromResult(IDbResult $result): CommentsCategoryInfo {
public static function fromResult(DbResult $result): CommentsCategoryInfo {
return new CommentsCategoryInfo(
id: $result->getString(0),
name: $result->getString(1),
@ -54,16 +54,17 @@ class CommentsCategoryInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function getLockedTime(): ?int {
return $this->locked;
}
public function getLockedAt(): ?DateTime {
return $this->locked === null ? null : DateTime::fromUnixTimeSeconds($this->locked);
public function getLockedAt(): ?CarbonImmutable {
return $this->locked === null ? null : CarbonImmutable::createFromTimestampUTC($this->locked);
}
public function isLocked(): bool {

View file

@ -1,8 +1,8 @@
<?php
namespace Misuzu\Comments;
use Index\DateTime;
use Index\Data\IDbResult;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class CommentsPostInfo {
public function __construct(
@ -22,7 +22,7 @@ class CommentsPostInfo {
) {}
public static function fromResult(
IDbResult $result,
DbResult $result,
bool $includeRepliesCount = false,
bool $includeVotesCount = false
): CommentsPostInfo {
@ -86,16 +86,16 @@ class CommentsPostInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function getPinnedTime(): ?int {
return $this->pinned;
}
public function getPinnedAt(): DateTime {
return $this->pinned === null ? null : DateTime::fromUnixTimeSeconds($this->pinned);
public function getPinnedAt(): ?CarbonImmutable {
return $this->pinned === null ? null : CarbonImmutable::createFromTimestampUTC($this->pinned);
}
public function isPinned(): bool {
@ -106,8 +106,8 @@ class CommentsPostInfo {
return $this->updated;
}
public function getUpdatedAt(): DateTime {
return $this->updated === null ? null : DateTime::fromUnixTimeSeconds($this->updated);
public function getUpdatedAt(): ?CarbonImmutable {
return $this->updated === null ? null : CarbonImmutable::createFromTimestampUTC($this->updated);
}
public function isEdited(): bool {
@ -118,8 +118,8 @@ class CommentsPostInfo {
return $this->deleted;
}
public function getDeletedAt(): DateTime {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted);
public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
}
public function isDeleted(): bool {

View file

@ -1,14 +1,14 @@
<?php
namespace Misuzu\Comments;
use Index\Data\IDbResult;
use Index\Db\DbResult;
class CommentsPostVoteInfo {
private string $commentId;
private string $userId;
private int $weight;
public function __construct(IDbResult $result) {
public function __construct(DbResult $result) {
$this->commentId = (string)$result->getInteger(0);
$this->userId = (string)$result->getInteger(1);
$this->weight = $result->getInteger(2);

View file

@ -1,8 +1,8 @@
<?php
namespace Misuzu\Counters;
use Index\DateTime;
use Index\Data\IDbResult;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class CounterInfo {
public function __construct(
@ -11,7 +11,7 @@ class CounterInfo {
private int $updated,
) {}
public static function fromResult(IDbResult $result): CounterInfo {
public static function fromResult(DbResult $result): CounterInfo {
return new CounterInfo(
name: $result->getString(0),
value: $result->getInteger(1),
@ -31,7 +31,7 @@ class CounterInfo {
return $this->updated;
}
public function getUpdatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->updated);
public function getUpdatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->updated);
}
}

View file

@ -2,9 +2,7 @@
namespace Misuzu\Counters;
use InvalidArgumentException;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\Pagination;
// insert increment and decrement calls in places someday
@ -12,7 +10,7 @@ use Misuzu\Pagination;
class Counters {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}

View file

@ -2,7 +2,7 @@
namespace Misuzu\Emoticons;
use Stringable;
use Index\Data\IDbResult;
use Index\Db\DbResult;
class EmoteInfo implements Stringable {
public function __construct(
@ -12,7 +12,7 @@ class EmoteInfo implements Stringable {
private string $url,
) {}
public static function fromResult(IDbResult $result): EmoteInfo {
public static function fromResult(DbResult $result): EmoteInfo {
return new EmoteInfo(
id: $result->getString(0),
order: $result->getInteger(1),

View file

@ -2,7 +2,7 @@
namespace Misuzu\Emoticons;
use Stringable;
use Index\Data\IDbResult;
use Index\Db\DbResult;
class EmoteStringInfo implements Stringable {
public function __construct(
@ -11,7 +11,7 @@ class EmoteStringInfo implements Stringable {
private string $string,
) {}
public static function fromResult(IDbResult $result): EmoteStringInfo {
public static function fromResult(DbResult $result): EmoteStringInfo {
return new EmoteStringInfo(
emoteId: $result->getString(0),
order: $result->getInteger(1),

View file

@ -3,8 +3,7 @@ namespace Misuzu\Emoticons;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Db\{DbConnection,DbStatementCache};
class Emotes {
private const EMOTE_ORDER = [
@ -13,10 +12,10 @@ class Emotes {
'rank' => 'emote_hierarchy',
];
private IDbConnection $dbConn;
private DbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}

View file

@ -1,117 +0,0 @@
<?php
namespace Misuzu\Feeds;
use DOMDocument;
use DOMElement;
use DOMNode;
class AtomFeedSerializer extends XmlFeedSerializer {
protected function formatTime(int $time): string {
return date('c', $time);
}
protected function createRoot(DOMDocument $document, Feed $feed): DOMNode {
$atom = $document->appendChild($document->createElement('feed'));
if($atom instanceof DOMElement)
$atom->setAttribute('xmlns', 'http://www.w3.org/2005/Atom');
$atom->appendChild(
$document->createElement(
'id',
$feed->hasContentUrl()
? $this->cleanString($feed->getContentUrl())
: time()
)
);
return $atom;
}
protected function createTitle(DOMDocument $document, string $title): DOMNode {
return $document->createElement('title', $this->cleanString($title));
}
protected function createDescription(DOMDocument $document, string $description): ?DOMNode {
return $document->createElement('subtitle', $this->cleanString($description));
}
protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMNode {
return $document->createElement('updated', $this->formatTime($lastUpdate));
}
protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
$link = $document->createElement('link');
$link->setAttribute('href', $this->cleanString($contentUrl));
return $link;
}
protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMNode {
$link = $document->createElement('link');
$link->setAttribute('href', $this->cleanString($feedUrl));
$link->setAttribute('ref', 'self');
return $link;
}
protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMNode {
$elem = $document->createElement('entry');
$elem->appendChild(
$document->createElement(
'id',
$feedItem->hasContentUrl()
? $this->cleanString($feedItem->getContentUrl())
: time()
)
);
return $elem;
}
protected function createItemTitle(DOMDocument $document, string $title): DOMNode {
return $document->createElement('title', $this->cleanString($title));
}
protected function createItemSummary(DOMDocument $document, string $summary): ?DOMNode {
return $document->createElement('summary', $this->cleanString($summary));
}
protected function createItemContent(DOMDocument $document, string $content): ?DOMNode {
$elem = $document->createElement('content', $this->cleanString($content));
$elem->setAttribute('type', 'html');
return $elem;
}
protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMNode {
return $document->createElement('updated', $this->formatTime($creationDate));
}
protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMNode {
return null;
}
protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
$elem = $document->createElement('link');
$elem->setAttribute('href', $this->cleanString($contentUrl));
$elem->setAttribute('type', 'text/html');
return $elem;
}
protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMNode {
return null;
}
protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMNode {
if(empty($authorName) && empty($authorUrl))
return null;
$elem = $document->createElement('author');
if(!empty($authorName))
$elem->appendChild($document->createElement('name', $this->cleanString($authorName)));
if(!empty($authorUrl))
$elem->appendChild($document->createElement('uri', $this->cleanString($authorUrl)));
return $elem;
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace Misuzu\Feeds;
class Feed {
private string $title = '';
private ?string $description = null;
private ?int $lastUpdate = null;
private ?string $contentUrl = null;
private ?string $feedUrl = null;
private array $feedItems = [];
public function getTitle(): string {
return $this->title;
}
public function setTitle(string $title): self {
$this->title = $title;
return $this;
}
public function getDescription(): string {
return $this->description ?? '';
}
public function hasDescription(): bool {
return isset($this->description);
}
public function setDescription(?string $description): self {
$this->description = $description;
return $this;
}
public function getLastUpdate(): int {
return $this->lastUpdate ?? 0;
}
public function hasLastUpdate(): bool {
return isset($this->lastUpdate);
}
public function setLastUpdate(?int $lastUpdate): self {
$this->lastUpdate = $lastUpdate;
return $this;
}
public function getContentUrl(): string {
return $this->contentUrl ?? '';
}
public function hasContentUrl(): bool {
return isset($this->contentUrl);
}
public function setContentUrl(?string $contentUrl): self {
$this->contentUrl = $contentUrl;
return $this;
}
public function getFeedUrl(): string {
return $this->feedUrl ?? '';
}
public function hasFeedUrl(): bool {
return isset($this->feedUrl);
}
public function setFeedUrl(?string $feedUrl): self {
$this->feedUrl = $feedUrl;
return $this;
}
public function getItems(): array {
return $this->feedItems;
}
public function hasItems(): bool {
return count($this->feedItems) > 0;
}
public function addItem(FeedItem $item): self {
$this->feedItems[] = $item;
return $this;
}
}

View file

@ -1,110 +0,0 @@
<?php
namespace Misuzu\Feeds;
class FeedItem {
private string $title = '';
private ?string $summary = null;
private ?string $content = null;
private ?int $creationDate = null;
private ?string $uniqueId = null;
private ?string $contentUrl = null;
private ?string $commentsUrl = null;
private ?string $authorName = null;
private ?string $authorUrl = null;
public function getTitle(): string {
return $this->title;
}
public function setTitle(string $title): self {
$this->title = $title;
return $this;
}
public function getSummary(): string {
return $this->summary ?? '';
}
public function hasSummary(): bool {
return isset($this->summary);
}
public function setSummary(?string $summary): self {
$this->summary = $summary;
return $this;
}
public function getContent(): string {
return $this->content ?? '';
}
public function hasContent(): bool {
return isset($this->content);
}
public function setContent(?string $content): self {
$this->content = $content;
return $this;
}
public function getCreationDate(): int {
return $this->creationDate;
}
public function hasCreationDate(): bool {
return isset($this->creationDate);
}
public function setCreationDate(?int $creationDate): self {
$this->creationDate = $creationDate;
return $this;
}
public function getUniqueId(): string {
return $this->uniqueId ?? '';
}
public function hasUniqueId(): bool {
return isset($this->uniqueId);
}
public function setUniqueId(?string $uniqueId): self {
$this->uniqueId = $uniqueId;
return $this;
}
public function getContentUrl(): string {
return $this->contentUrl ?? '';
}
public function hasContentUrl(): bool {
return isset($this->contentUrl);
}
public function setContentUrl(?string $contentUrl): self {
$this->contentUrl = $contentUrl;
return $this;
}
public function getCommentsUrl(): string {
return $this->commentsUrl ?? '';
}
public function hasCommentsUrl(): bool {
return isset($this->commentsUrl);
}
public function setCommentsUrl(?string $commentsUrl): self {
$this->commentsUrl = $commentsUrl;
return $this;
}
public function getAuthorName(): string {
return $this->authorName ?? '';
}
public function hasAuthorName(): bool {
return isset($this->authorName);
}
public function setAuthorName(?string $authorName): self {
$this->authorName = $authorName;
return $this;
}
public function getAuthorUrl(): string {
return $this->authorUrl ?? '';
}
public function hasAuthorUrl(): bool {
return isset($this->authorUrl);
}
public function setAuthorUrl(?string $authorUrl): self {
$this->authorUrl = $authorUrl;
return $this;
}
}

View file

@ -1,6 +0,0 @@
<?php
namespace Misuzu\Feeds;
abstract class FeedSerializer {
abstract public function serializeFeed(Feed $feed): string;
}

View file

@ -1,85 +0,0 @@
<?php
namespace Misuzu\Feeds;
use DOMDocument;
use DOMElement;
use DOMNode;
class RssFeedSerializer extends XmlFeedSerializer {
protected function formatTime(int $time): string {
return date('r', $time);
}
protected function createRoot(DOMDocument $document, Feed $feed): DOMNode {
$rss = $document->appendChild($document->createElement('rss'));
if($rss instanceof DOMElement) {
$rss->setAttribute('version', '2.0');
$rss->setAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom');
}
$channel = $rss->appendChild($document->createElement('channel'));
$channel->appendChild($document->createElement('ttl', '900'));
return $channel;
}
protected function createTitle(DOMDocument $document, string $title): DOMNode {
return $document->createElement('title', $this->cleanString($title));
}
protected function createDescription(DOMDocument $document, string $description): ?DOMNode {
return $document->createElement('description', $this->cleanString($description));
}
protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMNode {
return $document->createElement('pubDate', $this->formatTime($lastUpdate));
}
protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
return $document->createElement('link', $this->cleanString($contentUrl));
}
protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMNode {
$link = $document->createElement('atom:link');
$link->setAttribute('href', $this->cleanString($feedUrl));
$link->setAttribute('ref', 'self');
return $link;
}
protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMNode {
return $document->createElement('item');
}
protected function createItemTitle(DOMDocument $document, string $title): DOMNode {
return $document->createElement('title', $this->cleanString($title));
}
protected function createItemSummary(DOMDocument $document, string $summary): ?DOMNode {
return $document->createElement('description', $this->cleanString($summary));
}
protected function createItemContent(DOMDocument $document, string $content): ?DOMNode {
return null;
}
protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMNode {
return $document->createElement('pubDate', $this->formatTime($creationDate));
}
protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMNode {
$elem = $document->createElement('guid', $uniqueId);
$elem->setAttribute('isPermaLink', 'true');
return $elem;
}
protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
return $document->createElement('link', $contentUrl);
}
protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMNode {
return $document->createElement('comments', $commentsUrl);
}
protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMNode {
return null;
}
}

View file

@ -1,79 +0,0 @@
<?php
namespace Misuzu\Feeds;
use DOMDocument;
use DOMNode;
abstract class XmlFeedSerializer extends FeedSerializer {
public function serializeFeed(Feed $feed): string {
$document = new DOMDocument('1.0', 'utf-8');
$root = $this->createRoot($document, $feed);
$root->appendChild($this->createTitle($document, $feed->getTitle()));
if($feed->hasDescription())
self::appendChild($root, $this->createDescription($document, $feed->getDescription()));
if($feed->hasLastUpdate())
self::appendChild($root, $this->createLastUpdate($document, $feed->getLastUpdate()));
if($feed->hasContentUrl())
self::appendChild($root, $this->createContentUrl($document, $feed->getContentUrl()));
if($feed->hasFeedUrl())
self::appendChild($root, $this->createFeedUrl($document, $feed->getFeedUrl()));
if($feed->hasItems()) {
foreach($feed->getItems() as $item) {
$root->appendChild($this->serializeFeedItem($document, $item));
}
}
return $document->saveXML();
}
private function serializeFeedItem(DOMDocument $document, FeedItem $feedItem): DOMNode {
$elem = $this->createItem($document, $feedItem);
$elem->appendChild($this->createItemTitle($document, $feedItem->getTitle()));
if($feedItem->hasSummary())
self::appendChild($elem, $this->createItemSummary($document, $feedItem->getSummary()));
if($feedItem->hasContent())
self::appendChild($elem, $this->createItemContent($document, $feedItem->getContent()));
if($feedItem->hasCreationDate())
self::appendChild($elem, $this->createItemCreationDate($document, $feedItem->getCreationDate()));
if($feedItem->hasUniqueId())
self::appendChild($elem, $this->createItemUniqueId($document, $feedItem->getUniqueId()));
if($feedItem->hasContentUrl())
self::appendChild($elem, $this->createItemContentUrl($document, $feedItem->getContentUrl()));
if($feedItem->hasCommentsUrl())
self::appendChild($elem, $this->createItemCommentsUrl($document, $feedItem->getCommentsUrl()));
if($feedItem->hasAuthorName() || $feedItem->hasAuthorUrl())
self::appendChild($elem, $this->createItemAuthor($document, $feedItem->getAuthorName(), $feedItem->getAuthorUrl()));
return $elem;
}
protected function cleanString(string $string): string {
return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT | ENT_SUBSTITUTE);
}
protected static function appendChild(DOMNode $parent, ?DOMNode $elem): ?DOMNode {
if($elem !== null)
return $parent->appendChild($elem);
return $elem;
}
abstract protected function formatTime(int $time): string;
abstract protected function createRoot(DOMDocument $document, Feed $feed): DOMNode;
abstract protected function createTitle(DOMDocument $document, string $title): DOMNode;
abstract protected function createDescription(DOMDocument $document, string $description): ?DOMNode;
abstract protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMNode;
abstract protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode;
abstract protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMNode;
abstract protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMNode;
abstract protected function createItemTitle(DOMDocument $document, string $title): DOMNode;
abstract protected function createItemSummary(DOMDocument $document, string $summary): ?DOMNode;
abstract protected function createItemContent(DOMDocument $document, string $content): ?DOMNode;
abstract protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMNode;
abstract protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMNode;
abstract protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode;
abstract protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMNode;
abstract protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMNode;
}

View file

@ -5,16 +5,14 @@ use InvalidArgumentException;
use RuntimeException;
use stdClass;
use Index\Colour\Colour;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class ForumCategories {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}

View file

@ -1,9 +1,9 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Carbon\CarbonImmutable;
use Index\Colour\Colour;
use Index\Data\IDbResult;
use Index\Db\DbResult;
class ForumCategoryInfo {
// should the types just be replaced with flags indicating what's allowed?
@ -44,7 +44,7 @@ class ForumCategoryInfo {
private int $postsCount,
) {}
public static function fromResult(IDbResult $result): ForumCategoryInfo {
public static function fromResult(DbResult $result): ForumCategoryInfo {
return new ForumCategoryInfo(
id: $result->getString(0),
order: $result->getInteger(1),
@ -176,8 +176,8 @@ class ForumCategoryInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
public function isArchived(): bool {

View file

@ -2,7 +2,7 @@
namespace Misuzu\Forum;
use stdClass;
use Index\Data\IDbConnection;
use Index\Db\DbConnection;
use Misuzu\Users\UserInfo;
class ForumContext {
@ -14,7 +14,7 @@ class ForumContext {
private array $totalUserTopics = [];
private array $totalUserPosts = [];
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->categories = new ForumCategories($dbConn);
$this->topics = new ForumTopics($dbConn);
$this->topicRedirects = new ForumTopicRedirects($dbConn);

View file

@ -1,10 +1,10 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Misuzu\Parsers\Parser;
use Carbon\CarbonImmutable;
use Index\XDateTime;
use Index\Db\DbResult;
class ForumPostInfo {
public function __construct(
@ -21,7 +21,7 @@ class ForumPostInfo {
private ?int $deleted,
) {}
public static function fromResult(IDbResult $result): ForumPostInfo {
public static function fromResult(DbResult $result): ForumPostInfo {
return new ForumPostInfo(
id: $result->getString(0),
topicId: $result->getString(1),
@ -57,14 +57,10 @@ class ForumPostInfo {
return $this->userId;
}
public function getRemoteAddressRaw(): string {
public function getRemoteAddress(): string {
return $this->remoteAddr;
}
public function getRemoteAddress(): IPAddress {
return IPAddress::parse($this->remoteAddr);
}
public function getBody(): string {
return $this->body;
}
@ -93,16 +89,17 @@ class ForumPostInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
private static ?DateTime $markAsEditedThreshold = null;
private static ?CarbonImmutable $markAsEditedThreshold = null;
public function shouldMarkAsEdited(): bool {
if(self::$markAsEditedThreshold === null)
self::$markAsEditedThreshold = DateTime::now()->modify('-5 minutes');
self::$markAsEditedThreshold = new CarbonImmutable('-5 minutes');
return $this->getCreatedAt()->isLessThan(self::$markAsEditedThreshold);
return XDateTime::compare($this->getCreatedAt(), self::$markAsEditedThreshold) < 0;
}
public function isEdited(): bool {
@ -113,16 +110,17 @@ class ForumPostInfo {
return $this->edited;
}
public function getEditedAt(): ?DateTime {
return $this->edited === null ? null : DateTime::fromUnixTimeSeconds($this->edited);
public function getEditedAt(): ?CarbonImmutable {
return $this->edited === null ? null : CarbonImmutable::createFromTimestampUTC($this->edited);
}
private static ?DateTime $canBeDeletedThreshold = null;
private static ?CarbonImmutable $canBeDeletedThreshold = null;
public function canBeDeleted(): bool {
if(self::$canBeDeletedThreshold === null)
self::$canBeDeletedThreshold = DateTime::now()->modify('-1 week');
self::$canBeDeletedThreshold = new CarbonImmutable('-1 week');
return $this->getCreatedAt()->isMoreThanOrEqual(self::$canBeDeletedThreshold);
return XDateTime::compare($this->getCreatedAt(), self::$canBeDeletedThreshold) >= 0;
}
public function isDeleted(): bool {
@ -133,7 +131,7 @@ class ForumPostInfo {
return $this->deleted;
}
public function getDeletedAt(): ?DateTime {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted);
public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
}
}

View file

@ -4,19 +4,16 @@ namespace Misuzu\Forum;
use InvalidArgumentException;
use RuntimeException;
use stdClass;
use Index\DateTime;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Carbon\CarbonImmutable;
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class ForumPosts {
private IDbConnection $dbConn;
private DbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
@ -267,7 +264,7 @@ class ForumPosts {
public function createPost(
ForumTopicInfo|string $topicInfo,
UserInfo|string|null $userInfo,
IPAddress|string $remoteAddr,
string $remoteAddr,
string $body,
int $bodyParser,
bool $displaySignature,
@ -285,9 +282,6 @@ class ForumPosts {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$stmt = $this->cache->get('INSERT INTO msz_forum_posts (topic_id, forum_id, user_id, post_ip, post_text, post_parse, post_display_signature) VALUES (?, ?, ?, INET6_ATON(?), ?, ?, ?)');
$stmt->addParameter(1, $topicInfo);
$stmt->addParameter(2, $categoryInfo);
@ -303,7 +297,7 @@ class ForumPosts {
public function updatePost(
ForumPostInfo|string $postInfo,
IPAddress|string|null $remoteAddr = null,
?string $remoteAddr = null,
?string $body = null,
?int $bodyParser = null,
?bool $displaySignature = null,
@ -316,9 +310,6 @@ class ForumPosts {
$values = [];
if($remoteAddr !== null) {
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$fields[] = 'post_ip = INET6_ATON(?)';
$values[] = $remoteAddr;
}
@ -395,8 +386,8 @@ class ForumPosts {
return $result->getInteger(0);
}
public function getUserLastPostCreatedAt(UserInfo|string $userInfo): DateTime {
return DateTime::fromUnixTimeSeconds($this->getUserLastPostCreatedTime($userInfo));
public function getUserLastPostCreatedAt(UserInfo|string $userInfo): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->getUserLastPostCreatedTime($userInfo));
}
public function generatePostRankings(

View file

@ -1,8 +1,9 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Index\Data\IDbResult;
use Carbon\CarbonImmutable;
use Index\XDateTime;
use Index\Db\DbResult;
class ForumTopicInfo {
public const TYPE_DISCUSSION = 0;
@ -32,7 +33,7 @@ class ForumTopicInfo {
private ?int $locked,
) {}
public static function fromResult(IDbResult $result): ForumTopicInfo {
public static function fromResult(DbResult $result): ForumTopicInfo {
return new ForumTopicInfo(
id: $result->getString(0),
categoryId: $result->getString(1),
@ -136,25 +137,25 @@ class ForumTopicInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
private static ?DateTime $lastActiveAt = null;
private static ?CarbonImmutable $lastActiveAt = null;
public function isActive(): bool {
if(self::$lastActiveAt === null)
self::$lastActiveAt = DateTime::now()->modify('-1 month');
self::$lastActiveAt = new CarbonImmutable('-1 month');
return $this->getBumpedAt()->isMoreThanOrEqual(self::$lastActiveAt);
return XDateTime::compare($this->getBumpedAt(), self::$lastActiveAt) >= 0;
}
public function getBumpedTime(): int {
return $this->bumped;
}
public function getBumpedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->bumped);
public function getBumpedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->bumped);
}
public function isDeleted(): bool {
@ -165,8 +166,8 @@ class ForumTopicInfo {
return $this->deleted;
}
public function getDeletedAt(): ?DateTime {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted);
public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
}
public function isLocked(): bool {
@ -177,7 +178,7 @@ class ForumTopicInfo {
return $this->locked;
}
public function getLockedAt(): ?DateTime {
return $this->locked === null ? null : DateTime::fromUnixTimeSeconds($this->locked);
public function getLockedAt(): ?CarbonImmutable {
return $this->locked === null ? null : CarbonImmutable::createFromTimestampUTC($this->locked);
}
}

View file

@ -1,8 +1,8 @@
<?php
namespace Misuzu\Forum;
use Index\DateTime;
use Index\Data\IDbResult;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
class ForumTopicRedirectInfo {
public function __construct(
@ -12,7 +12,7 @@ class ForumTopicRedirectInfo {
private int $created,
) {}
public static function fromResult(IDbResult $result): ForumTopicRedirectInfo {
public static function fromResult(DbResult $result): ForumTopicRedirectInfo {
return new ForumTopicRedirectInfo(
topicId: $result->getString(0),
userId: $result->getStringOrNull(1),
@ -41,7 +41,7 @@ class ForumTopicRedirectInfo {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
public function getCreatedAt(): CarbonImmutable {
return CarbonImmutable::createFromTimestampUTC($this->created);
}
}

View file

@ -2,15 +2,14 @@
namespace Misuzu\Forum;
use RuntimeException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class ForumTopicRedirects {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}

View file

@ -4,17 +4,15 @@ namespace Misuzu\Forum;
use InvalidArgumentException;
use RuntimeException;
use stdClass;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class ForumTopics {
private IDbConnection $dbConn;
private DbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
public function __construct(DbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}

View file

@ -0,0 +1,162 @@
<?php
namespace Misuzu\Hanyuu;
use RuntimeException;
use Misuzu\CSRF;
use Misuzu\Auth\AuthContext;
use Misuzu\Users\{UsersContext,UserInfo};
use Aiwass\Server\{RpcActionHandler,RpcProcedure};
use Index\Colour\Colour;
use Index\Config\Config;
use Index\Urls\UrlRegistry;
final class HanyuuRpcActions extends RpcActionHandler {
public function __construct(
private $getBaseUrl,
private Config $impersonateConfig,
private UrlRegistry $urls,
private UsersContext $usersCtx,
private AuthContext $authCtx
) {}
private static function createPayload(string $name, array $attrs = []): array {
$payload = ['name' => $name, 'attrs' => []];
foreach($attrs as $name => $value) {
if($value === null)
continue;
$payload['attrs'][(string)$name] = $value;
}
return $payload;
}
private static function createErrorPayload(string $code, ?string $text = null): array {
$attrs = ['code' => $code];
if($text !== null && $text !== '')
$attrs['text'] = $text;
return self::createPayload('error', $attrs);
}
private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
if($impersonator->isSuperUser())
return true;
return in_array(
$targetId,
$this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId())),
true
);
}
#[RpcProcedure('mszhau:authCheck')]
public function procAuthCheck(string $method, string $remoteAddr, string $token, string $avatars = '') {
if($method !== 'Misuzu')
return self::createErrorPayload('auth:check:method', 'Requested auth method is not supported.');
if(filter_var($remoteAddr, FILTER_VALIDATE_IP) === false)
return self::createErrorPayload('auth:check:remote_addr', 'Provided remote address is not in a valid format.');
$avatarResolutions = trim($avatars);
if($avatarResolutions === '') {
$avatarResolutions = [];
} else {
$avatarResolutions = explode(',', $avatarResolutions);
foreach($avatarResolutions as $key => $avatarRes) {
if(!ctype_digit($avatarRes))
return self::createErrorPayload('auth:check:avatars', 'Avatar resolution set must be a comma separated set of numbers or empty.');
$avatarResolutions[$key] = (int)$avatarRes;
}
$avatarResolutions = array_unique($avatarResolutions);
}
$baseUrl = ($this->getBaseUrl)();
$loginUrl = $baseUrl . $this->urls->format('auth-login');
$registerUrl = $baseUrl . $this->urls->format('auth-register');
$tokenPacker = $this->authCtx->createAuthTokenPacker();
$tokenInfo = $tokenPacker->unpack(trim($token));
if($tokenInfo->isEmpty())
return self::createPayload('auth:check:fail', ['reason' => 'empty', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
$sessions = $this->authCtx->getSessions();
try {
$sessionInfo = $sessions->getSession(sessionToken: $tokenInfo->getSessionToken());
} catch(RuntimeException $ex) {
return self::createPayload('auth:check:fail', ['reason' => 'session', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
}
if($sessionInfo->hasExpired()) {
$sessions->deleteSessions(sessionInfos: $sessionInfo);
return self::createPayload('auth:check:fail', ['reason' => 'expired', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
}
$sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
$users = $this->usersCtx->getUsers();
$userInfo = $userInfoReal = $users->getUser($sessionInfo->getUserId(), 'id');
if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
try {
$userInfo = $users->getUser($tokenInfo->getImpersonatedUserId(), 'id');
} catch(RuntimeException $ex) {
$userInfo = $userInfoReal;
}
}
$response = [];
$response['session'] = [
'token' => $sessionInfo->getToken(),
'created_at' => $sessionInfo->getCreatedTime(),
'expires_at' => $sessionInfo->getExpiresTime(),
'lifetime_extends' => $sessionInfo->shouldBumpExpires(),
];
$banInfo = $this->usersCtx->tryGetActiveBan($userInfo);
if($banInfo !== null)
$response['ban'] = [
'severity' => $banInfo->getSeverity(),
'reason' => $banInfo->getPublicReason(),
'created_at' => $banInfo->getCreatedTime(),
'is_permanent' => $banInfo->isPermanent(),
'expires_at' => $banInfo->getExpiresTime(),
'duration_str' => $banInfo->getDurationString(),
'remaining_str' => $banInfo->getRemainingString(),
];
$gatherRequestedAvatars = function($userInfo) use ($avatarResolutions, $baseUrl) {
$formatAvatarUrl = fn($res = 0) => (
$baseUrl . $this->urls->format('user-avatar', ['user' => $userInfo->getId(), 'res' => $res])
);
$avatars = ['original' => $formatAvatarUrl()];
foreach($avatarResolutions as $avatarRes)
$avatars[sprintf('x%d', $avatarRes)] = $formatAvatarUrl($avatarRes);
return $avatars;
};
$extractUserInfo = fn($userInfo) => [
'id' => $userInfo->getId(),
'name' => $userInfo->getName(),
'colour' => (string)$users->getUserColour($userInfo),
'rank' => $users->getUserRank($userInfo),
'is_super' => $userInfo->isSuperUser(),
'country_code' => $userInfo->getCountryCode(),
'is_deleted' => $userInfo->isDeleted(),
'has_totp' => $userInfo->hasTOTPKey(),
'profile_url' => $baseUrl . $this->urls->format('user-profile', ['user' => $userInfo->getId()]),
'avatars' => $gatherRequestedAvatars($userInfo),
];
$response['user'] = $extractUserInfo($userInfo);
if($userInfo !== $userInfoReal) {
$response['guise'] = $extractUserInfo($userInfoReal);
$csrfp = CSRF::create($sessionInfo->getToken());
$response['guise']['revert_url'] = $baseUrl . $this->urls->format('auth-revert', ['csrf' => $csrfp->createToken()]);
}
return self::createPayload('auth:check:success', $response);
}
}

View file

@ -2,23 +2,25 @@
namespace Misuzu\Home;
use RuntimeException;
use Index\DateTime;
use Index\Data\{DbTools,IDbConnection};
use Index\Http\Routing\{HttpGet,RouteHandler};
use Syokuhou\IConfig;
use Index\XDateTime;
use Index\Config\Config;
use Index\Db\{DbConnection,DbTools};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait};
use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\Auth\AuthInfo;
use Misuzu\Changelog\Changelog;
use Misuzu\Comments\Comments;
use Misuzu\Counters\Counters;
use Misuzu\News\News;
use Misuzu\URLs\URLInfo;
use Misuzu\Users\UsersContext;
class HomeRoutes extends RouteHandler {
class HomeRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait;
public function __construct(
private IConfig $config,
private IDbConnection $dbConn,
private Config $config,
private DbConnection $dbConn,
private SiteInfo $siteInfo,
private AuthInfo $authInfo,
private Changelog $changelog,
@ -151,7 +153,7 @@ class HomeRoutes extends RouteHandler {
}
#[HttpGet('/')]
#[URLInfo('index', '/')]
#[UrlFormat('index', '/')]
public function getIndex(...$args) {
return $this->authInfo->isLoggedIn()
? $this->getHome(...$args)
@ -167,7 +169,7 @@ class HomeRoutes extends RouteHandler {
$stats['users:online:recent'] = count($onlineUserInfos);
$birthdays = [];
$birthdayInfos = $this->usersCtx->getUsers()->getUsers(deleted: false, birthdate: DateTime::now(), orderBy: 'random');
$birthdayInfos = $this->usersCtx->getUsers()->getUsers(deleted: false, birthdate: XDateTime::now(), orderBy: 'random');
foreach($birthdayInfos as $birthdayInfo)
$birthdays[] = [
'info' => $birthdayInfo,

View file

@ -1,12 +1,14 @@
<?php
namespace Misuzu\Info;
use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait};
use Misuzu\Template;
use Misuzu\Parsers\Parser;
use Misuzu\URLs\URLInfo;
class InfoRoutes extends RouteHandler {
class InfoRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait, UrlSourceTrait;
private const DOCS_PATH = MSZ_ROOT . '/docs';
private const PROJECT_PATHS = [
'misuzu' => MSZ_ROOT,
@ -18,14 +20,14 @@ class InfoRoutes extends RouteHandler {
];
#[HttpGet('/info')]
#[URLInfo('info-index', '/info')]
#[UrlFormat('info-index', '/info')]
public function getIndex() {
return Template::renderRaw('info.index');
}
#[HttpGet('/info/([A-Za-z0-9_]+)')]
#[URLInfo('info', '/info/<title>')]
#[URLInfo('info-doc', '/info/<title>')]
#[UrlFormat('info', '/info/<title>')]
#[UrlFormat('info-doc', '/info/<title>')]
public function getDocsPage($response, $request, string $name) {
return $this->serveMarkdownDocument(
sprintf('%s/%s.md', self::DOCS_PATH, $name)
@ -71,7 +73,7 @@ class InfoRoutes extends RouteHandler {
}
#[HttpGet('/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')]
#[URLInfo('info-project-doc', '/info/<project>/<title>')]
#[UrlFormat('info-project-doc', '/info/<project>/<title>')]
public function getProjectPage($response, $request, string $project, string $name) {
if(!array_key_exists($project, self::PROJECT_PATHS))
return 404;

View file

@ -1,15 +1,19 @@
<?php
namespace Misuzu;
use Index\Http\Routing\{HttpGet,RouteHandler};
use Misuzu\URLs\{IURLSource,URLInfo,URLRegistry};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource};
class LegacyRoutes implements RouteHandler, UrlSource {
use RouteHandlerTrait;
class LegacyRoutes extends RouteHandler implements IURLSource {
public function __construct(
private URLRegistry $urls
private UrlRegistry $urls
) {}
public function registerURLs(URLRegistry $urls): void {
public function registerUrls(UrlRegistry $urls): void {
UrlFormat::register($urls, $this);
// eventually this should be handled by context classes
$urls->register('search-index', '/search.php');
$urls->register('search-query', '/search.php', ['q' => '<query>'], '<section>');
@ -156,20 +160,6 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
$response->redirect($location, true);
}
#[HttpGet('/news.php/rss')]
public function getNewsRssPHP($response, $request): void {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = $this->urls->format($catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss', ['category' => $catId]);
$response->redirect($location, true);
}
#[HttpGet('/news.php/atom')]
public function getNewsAtomPHP($response, $request): void {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = $this->urls->format($catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom', ['category' => $catId]);
$response->redirect($location, true);
}
#[HttpGet('/news/index.php')]
public function getNewsIndexPHP($response, $request): void {
$response->redirect($this->urls->format('news-index', [
@ -192,25 +182,15 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
]), true);
}
#[HttpGet('/news.php/rss')]
#[HttpGet('/news.php/atom')]
#[HttpGet('/news/feed.php')]
public function getNewsFeedPHP($response, $request): int {
return 400;
}
#[HttpGet('/news/feed.php/rss')]
public function getNewsFeedRssPHP($response, $request): void {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$response->redirect($this->urls->format(
$catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss',
['category' => $catId]
), true);
}
#[HttpGet('/news/feed.php/atom')]
public function getNewsFeedAtomPHP($response, $request): void {
public function getNewsFeedPHP($response, $request): void {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$response->redirect($this->urls->format(
$catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom',
$catId > 0 ? 'news-category-feed' : 'news-feed',
['category' => $catId]
), true);
}
@ -262,7 +242,7 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
}
#[HttpGet('/manage')]
#[URLInfo('manage-index', '/manage')]
#[UrlFormat('manage-index', '/manage')]
public function getManageIndex($response): void {
$response->redirect($this->urls->format('manage-general-overview'));
}

Some files were not shown because too many files have changed in this diff Show more