Compare commits

...

15 commits

Author SHA1 Message Date
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
44a4bb6e6f Prevent access to private messages when impersonating a user. 2024-06-02 19:57:58 +00:00
ec00cfa176 Base64 encode PM titles and bodies in the database.
To prevent personal discomfort with having to do database messages and seeing people's personal conversations.
I haven't run into it yet, but I'd rather avoid it altogether.
2024-06-02 19:54:33 +00:00
1d295df8da Added broom closet PM stats. 2024-06-02 19:43:57 +00:00
57 changed files with 1876 additions and 781 deletions

View file

@ -1,5 +1,5 @@
#include csrfp.js #include csrfp.js
#include msgbox.js #include msgbox.jsx
#include utility.js #include utility.js
#include messages/actbtn.js #include messages/actbtn.js
#include messages/list.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 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 () => { (async () => {
const files = {}; const isDebug = fs.existsSync(pathJoin(__dirname, '.debug'));
console.log('Ensuring assets directory exists...'); const env = {
fs.mkdirSync(pubAssetsDir, { recursive: true }); 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(); const files = await assproc.process(env, tasks);
console.log('JS assets');
for(const info of buildTasks.js) {
console.log(`=> Building ${info.source}...`);
let origTarget = undefined; fs.writeFileSync(pathJoin(__dirname, 'assets/current.json'), JSON.stringify(files));
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);
})(); })();

View file

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

825
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

761
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -3,9 +3,10 @@ namespace Misuzu;
use stdClass; use stdClass;
use RuntimeException; use RuntimeException;
use Index\DateTime;
use Misuzu\Forum\ForumTopicInfo; use Misuzu\Forum\ForumTopicInfo;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Index\XDateTime;
use Carbon\CarbonImmutable;
$authInfo = $msz->getAuthInfo(); $authInfo = $msz->getAuthInfo();
if(!$authInfo->isLoggedIn()) if(!$authInfo->isLoggedIn())
@ -152,11 +153,11 @@ if(!empty($_POST)) {
if($mode === 'create') { if($mode === 'create') {
$postTimeout = $cfg->getInteger('forum.posting.timeout', 5); $postTimeout = $cfg->getInteger('forum.posting.timeout', 5);
if($postTimeout > 0) { if($postTimeout > 0) {
$postTimeoutThreshold = DateTime::now()->modify(sprintf('-%d seconds', $postTimeout)); $postTimeoutThreshold = new CarbonImmutable(sprintf('-%d seconds', $postTimeout));
$lastPostCreatedAt = $forumPosts->getUserLastPostCreatedAt($currentUser); $lastPostCreatedAt = $forumPosts->getUserLastPostCreatedAt($currentUser);
if($lastPostCreatedAt->isMoreThan($postTimeoutThreshold)) { if(XDateTime::compare($lastPostCreatedAt, $postTimeoutThreshold) > 0) {
$waitSeconds = $postTimeout + ($lastPostCreatedAt->getUnixTimeSeconds() - time()); $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[] = 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."; $notices[] = "It's possible that your post went through successfully and you pressed the submit button twice by accident.";

View file

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

View file

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

View file

@ -5,7 +5,6 @@ use stdClass;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\ByteFormat; use Index\ByteFormat;
use Index\DateTime;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\Assets\UserAvatarAsset; use Misuzu\Users\Assets\UserAvatarAsset;

View file

@ -2,7 +2,7 @@
namespace Misuzu\AuditLog; namespace Misuzu\AuditLog;
use ValueError; use ValueError;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
@ -47,8 +47,8 @@ class AuditLogInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getRemoteAddressRaw(): string { public function getRemoteAddressRaw(): string {

View file

@ -1,7 +1,6 @@
<?php <?php
namespace Misuzu\Auth; namespace Misuzu\Auth;
use Index\XArray;
use Misuzu\Auth\SessionInfo; use Misuzu\Auth\SessionInfo;
use Misuzu\Forum\ForumCategoryInfo; use Misuzu\Forum\ForumCategoryInfo;
use Misuzu\Perms\IPermissionResult; 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 Syokuhou\IConfig;
final class AuthRpcActions extends RpcActionHandler {
public function __construct(
private IConfig $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,8 @@
namespace Misuzu\Auth; namespace Misuzu\Auth;
use RuntimeException; use RuntimeException;
use Index\UriBase64;
use Index\IO\MemoryStream; use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64;
class AuthTokenPacker { class AuthTokenPacker {
private const EPOCH_V2 = 1682985600; private const EPOCH_V2 = 1682985600;

View file

@ -1,10 +1,10 @@
<?php <?php
namespace Misuzu\Auth; namespace Misuzu\Auth;
use Index\DateTime; use Misuzu\ClientInfo;
use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\ClientInfo;
class LoginAttemptInfo { class LoginAttemptInfo {
public function __construct( public function __construct(
@ -57,8 +57,8 @@ class LoginAttemptInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getUserAgentString(): string { public function getUserAgentString(): string {

View file

@ -1,7 +1,6 @@
<?php <?php
namespace Misuzu\Auth; namespace Misuzu\Auth;
use Index\TimeSpan;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Net\IPAddress; use Index\Net\IPAddress;
@ -23,14 +22,12 @@ class LoginAttempts {
?bool $success = null, ?bool $success = null,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null int|null $timeRange = null
): int { ): int {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan)
$timeRange = (int)$timeRange->totalSeconds();
$hasSuccess = $success !== null; $hasSuccess = $success !== null;
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
@ -81,15 +78,13 @@ class LoginAttempts {
?bool $success = null, ?bool $success = null,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null, int|null $timeRange = null,
?Pagination $pagination = null ?Pagination $pagination = null
): iterable { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan)
$timeRange = (int)$timeRange->totalSeconds();
$hasSuccess = $success !== null; $hasSuccess = $success !== null;
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Auth; namespace Misuzu\Auth;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
@ -36,16 +36,16 @@ class RecoveryTokenInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getExpiresTime(): int { public function getExpiresTime(): int {
return $this->created + self::LIFETIME; return $this->created + self::LIFETIME;
} }
public function getExpiresAt(): DateTime { public function getExpiresAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created + self::LIFETIME); return CarbonImmutable::createFromTimestampUTC($this->getExpiresTime());
} }
public function hasExpired(): bool { public function hasExpired(): bool {

View file

@ -3,10 +3,10 @@ namespace Misuzu\Auth;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\Base32;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Index\Serialisation\Base32;
use Misuzu\ClientInfo; use Misuzu\ClientInfo;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;

View file

@ -1,10 +1,10 @@
<?php <?php
namespace Misuzu\Auth; namespace Misuzu\Auth;
use Index\DateTime; use Misuzu\ClientInfo;
use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\ClientInfo;
class SessionInfo { class SessionInfo {
public function __construct( public function __construct(
@ -91,8 +91,8 @@ class SessionInfo {
return $this->expires; return $this->expires;
} }
public function getExpiresAt(): DateTime { public function getExpiresAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->expires); return CarbonImmutable::createFromTimestampUTC($this->expires);
} }
public function shouldBumpExpires(): bool { public function shouldBumpExpires(): bool {
@ -107,8 +107,8 @@ class SessionInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function hasLastActive(): bool { public function hasLastActive(): bool {
@ -119,7 +119,7 @@ class SessionInfo {
return $this->lastActive; return $this->lastActive;
} }
public function getLastActiveAt(): ?DateTime { public function getLastActiveAt(): ?CarbonImmutable {
return $this->lastActive === null ? null : DateTime::fromUnixTimeSeconds($this->lastActive); return $this->lastActive === null ? null : CarbonImmutable::createFromTimestampUTC($this->lastActive);
} }
} }

View file

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

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Changelog; namespace Misuzu\Changelog;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ChangeInfo { class ChangeInfo {
@ -53,8 +53,8 @@ class ChangeInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getDate(): string { public function getDate(): string {

View file

@ -2,7 +2,7 @@
namespace Misuzu\Changelog; namespace Misuzu\Changelog;
use Stringable; use Stringable;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ChangeTagInfo implements Stringable { class ChangeTagInfo implements Stringable {
@ -38,16 +38,16 @@ class ChangeTagInfo implements Stringable {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getArchivedTime(): int { public function getArchivedTime(): int {
return $this->archived; return $this->archived;
} }
public function getArchivedAt(): DateTime { public function getArchivedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->archived);
} }
public function isArchived(): bool { public function isArchived(): bool {

View file

@ -3,7 +3,7 @@ namespace Misuzu\Changelog;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\DateTime; use DateTimeInterface;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
@ -66,13 +66,13 @@ class Changelog {
public function countChanges( public function countChanges(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null, DateTimeInterface|int|null $dateTime = null,
?array $tags = null ?array $tags = null
): int { ): int {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($dateTime instanceof DateTime) if($dateTime instanceof DateTimeInterface)
$dateTime = $dateTime->getUnixTimeSeconds(); $dateTime = (int)$dateTime->format('U');
$args = 0; $args = 0;
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
@ -118,14 +118,14 @@ class Changelog {
public function getChanges( public function getChanges(
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null, DateTimeInterface|int|null $dateTime = null,
?array $tags = null, ?array $tags = null,
?Pagination $pagination = null ?Pagination $pagination = null
): iterable { ): iterable {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($dateTime instanceof DateTime) if($dateTime instanceof DateTimeInterface)
$dateTime = $dateTime->getUnixTimeSeconds(); $dateTime = (int)$dateTime->format('U');
$args = 0; $args = 0;
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
@ -189,14 +189,14 @@ class Changelog {
string $summary, string $summary,
string $body = '', string $body = '',
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null DateTimeInterface|int|null $createdAt = null
): ChangeInfo { ): ChangeInfo {
if(is_string($action)) if(is_string($action))
$action = self::convertToActionId($action); $action = self::convertToActionId($action);
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($createdAt instanceof DateTime) if($createdAt instanceof DateTimeInterface)
$createdAt = $createdAt->getUnixTimeSeconds(); $createdAt = (int)$createdAt->format('U');
$summary = trim($summary); $summary = trim($summary);
if(empty($summary)) if(empty($summary))
@ -233,7 +233,7 @@ class Changelog {
?string $body = null, ?string $body = null,
bool $updateUserInfo = false, bool $updateUserInfo = false,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null DateTimeInterface|int|null $createdAt = null
): void { ): void {
if($infoOrId instanceof ChangeInfo) if($infoOrId instanceof ChangeInfo)
$infoOrId = $infoOrId->getId(); $infoOrId = $infoOrId->getId();
@ -242,8 +242,8 @@ class Changelog {
$action = self::convertToActionId($action); $action = self::convertToActionId($action);
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($createdAt instanceof DateTime) if($createdAt instanceof DateTimeInterface)
$createdAt = $createdAt->getUnixTimeSeconds(); $createdAt = (int)$createdAt->format('U');
if($summary !== null) { if($summary !== null) {
$summary = trim($summary); $summary = trim($summary);

View file

@ -1,9 +1,9 @@
<?php <?php
namespace Misuzu\Comments; namespace Misuzu\Comments;
use Index\DateTime;
use Index\Data\IDbResult;
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;
use Carbon\CarbonImmutable;
use Index\Data\IDbResult;
class CommentsCategoryInfo { class CommentsCategoryInfo {
public function __construct( public function __construct(
@ -54,16 +54,17 @@ class CommentsCategoryInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getLockedTime(): ?int { public function getLockedTime(): ?int {
return $this->locked; return $this->locked;
} }
public function getLockedAt(): ?DateTime { public function getLockedAt(): ?CarbonImmutable {
return $this->locked === null ? null : DateTime::fromUnixTimeSeconds($this->locked); return $this->locked === null ? null : CarbonImmutable::createFromTimestampUTC($this->locked);
} }
public function isLocked(): bool { public function isLocked(): bool {

View file

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

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Counters; namespace Misuzu\Counters;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class CounterInfo { class CounterInfo {
@ -31,7 +31,7 @@ class CounterInfo {
return $this->updated; return $this->updated;
} }
public function getUpdatedAt(): DateTime { public function getUpdatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->updated); return CarbonImmutable::createFromTimestampUTC($this->updated);
} }
} }

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Forum; namespace Misuzu\Forum;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Data\IDbResult; use Index\Data\IDbResult;
@ -176,8 +176,8 @@ class ForumCategoryInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function isArchived(): bool { public function isArchived(): bool {

View file

@ -1,10 +1,11 @@
<?php <?php
namespace Misuzu\Forum; namespace Misuzu\Forum;
use Index\DateTime; use Misuzu\Parsers\Parser;
use Carbon\CarbonImmutable;
use Index\XDateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\Parsers\Parser;
class ForumPostInfo { class ForumPostInfo {
public function __construct( public function __construct(
@ -93,16 +94,17 @@ class ForumPostInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
private static ?DateTime $markAsEditedThreshold = null; private static ?CarbonImmutable $markAsEditedThreshold = null;
public function shouldMarkAsEdited(): bool { public function shouldMarkAsEdited(): bool {
if(self::$markAsEditedThreshold === null) 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 { public function isEdited(): bool {
@ -113,16 +115,17 @@ class ForumPostInfo {
return $this->edited; return $this->edited;
} }
public function getEditedAt(): ?DateTime { public function getEditedAt(): ?CarbonImmutable {
return $this->edited === null ? null : DateTime::fromUnixTimeSeconds($this->edited); return $this->edited === null ? null : CarbonImmutable::createFromTimestampUTC($this->edited);
} }
private static ?DateTime $canBeDeletedThreshold = null; private static ?CarbonImmutable $canBeDeletedThreshold = null;
public function canBeDeleted(): bool { public function canBeDeleted(): bool {
if(self::$canBeDeletedThreshold === null) 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 { public function isDeleted(): bool {
@ -133,7 +136,7 @@ class ForumPostInfo {
return $this->deleted; return $this->deleted;
} }
public function getDeletedAt(): ?DateTime { public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
} }
} }

View file

@ -4,7 +4,7 @@ namespace Misuzu\Forum;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use stdClass; use stdClass;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
@ -395,8 +395,8 @@ class ForumPosts {
return $result->getInteger(0); return $result->getInteger(0);
} }
public function getUserLastPostCreatedAt(UserInfo|string $userInfo): DateTime { public function getUserLastPostCreatedAt(UserInfo|string $userInfo): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->getUserLastPostCreatedTime($userInfo)); return CarbonImmutable::createFromTimestampUTC($this->getUserLastPostCreatedTime($userInfo));
} }
public function generatePostRankings( public function generatePostRankings(

View file

@ -1,7 +1,8 @@
<?php <?php
namespace Misuzu\Forum; namespace Misuzu\Forum;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\XDateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ForumTopicInfo { class ForumTopicInfo {
@ -136,25 +137,25 @@ class ForumTopicInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
private static ?DateTime $lastActiveAt = null; private static ?CarbonImmutable $lastActiveAt = null;
public function isActive(): bool { public function isActive(): bool {
if(self::$lastActiveAt === null) 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 { public function getBumpedTime(): int {
return $this->bumped; return $this->bumped;
} }
public function getBumpedAt(): DateTime { public function getBumpedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->bumped); return CarbonImmutable::createFromTimestampUTC($this->bumped);
} }
public function isDeleted(): bool { public function isDeleted(): bool {
@ -165,8 +166,8 @@ class ForumTopicInfo {
return $this->deleted; return $this->deleted;
} }
public function getDeletedAt(): ?DateTime { public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
} }
public function isLocked(): bool { public function isLocked(): bool {
@ -177,7 +178,7 @@ class ForumTopicInfo {
return $this->locked; return $this->locked;
} }
public function getLockedAt(): ?DateTime { public function getLockedAt(): ?CarbonImmutable {
return $this->locked === null ? null : DateTime::fromUnixTimeSeconds($this->locked); return $this->locked === null ? null : CarbonImmutable::createFromTimestampUTC($this->locked);
} }
} }

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Forum; namespace Misuzu\Forum;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ForumTopicRedirectInfo { class ForumTopicRedirectInfo {
@ -41,7 +41,7 @@ class ForumTopicRedirectInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
} }

View file

@ -0,0 +1,162 @@
<?php
namespace Misuzu\Hanyuu;
use RuntimeException;
use Misuzu\CSRF;
use Misuzu\Auth\AuthContext;
use Misuzu\URLs\URLRegistry;
use Misuzu\Users\{UsersContext,UserInfo};
use Aiwass\Server\{RpcActionHandler,RpcProcedure};
use Index\Colour\Colour;
use Syokuhou\IConfig;
final class HanyuuRpcActions extends RpcActionHandler {
public function __construct(
private $getBaseUrl,
private IConfig $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,7 +2,7 @@
namespace Misuzu\Home; namespace Misuzu\Home;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\XDateTime;
use Index\Data\{DbTools,IDbConnection}; use Index\Data\{DbTools,IDbConnection};
use Index\Http\Routing\{HttpGet,RouteHandler}; use Index\Http\Routing\{HttpGet,RouteHandler};
use Syokuhou\IConfig; use Syokuhou\IConfig;
@ -167,7 +167,7 @@ class HomeRoutes extends RouteHandler {
$stats['users:online:recent'] = count($onlineUserInfos); $stats['users:online:recent'] = count($onlineUserInfos);
$birthdays = []; $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) foreach($birthdayInfos as $birthdayInfo)
$birthdays[] = [ $birthdays[] = [
'info' => $birthdayInfo, 'info' => $birthdayInfo,

View file

@ -1,9 +1,9 @@
<?php <?php
namespace Misuzu\Messages; namespace Misuzu\Messages;
use Index\DateTime;
use Index\Data\IDbResult;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Carbon\CarbonImmutable;
use Index\Data\IDbResult;
class MessageInfo { class MessageInfo {
public function __construct( public function __construct(
@ -98,8 +98,8 @@ class MessageInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function isSent(): bool { public function isSent(): bool {
@ -110,8 +110,8 @@ class MessageInfo {
return $this->sent; return $this->sent;
} }
public function getSentAt(): ?DateTime { public function getSentAt(): ?CarbonImmutable {
return $this->sent === null ? null : DateTime::fromUnixTimeSeconds($this->sent); return $this->sent === null ? null : CarbonImmutable::createFromTimestampUTC($this->sent);
} }
public function isRead(): bool { public function isRead(): bool {
@ -122,8 +122,8 @@ class MessageInfo {
return $this->read; return $this->read;
} }
public function getReadAt(): ?DateTime { public function getReadAt(): ?CarbonImmutable {
return $this->read === null ? null : DateTime::fromUnixTimeSeconds($this->read); return $this->read === null ? null : CarbonImmutable::createFromTimestampUTC($this->read);
} }
public function isDeleted(): bool { public function isDeleted(): bool {
@ -134,8 +134,8 @@ class MessageInfo {
return $this->deleted; return $this->deleted;
} }
public function getDeletedAt(): ?DateTime { public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
} }
public function getDisplayTime(): int { public function getDisplayTime(): int {
@ -144,7 +144,7 @@ class MessageInfo {
return $this->getCreatedTime(); return $this->getCreatedTime();
} }
public function getDisplayAt(): DateTime { public function getDisplayAt(): CarbonImmutable {
if($this->isSent()) if($this->isSent())
return $this->getSentAt(); return $this->getSentAt();
return $this->getCreatedAt(); return $this->getCreatedAt();

View file

@ -3,7 +3,7 @@ namespace Misuzu\Messages;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\DateTime; use DateTimeInterface;
use Index\Data\{DbStatementCache,DbTools,IDbConnection}; use Index\Data\{DbStatementCache,DbTools,IDbConnection};
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\UserInfo; use Misuzu\Users\UserInfo;
@ -104,7 +104,7 @@ class MessagesDatabase {
$hasPagination = $pagination !== null; $hasPagination = $pagination !== null;
$args = 0; $args = 0;
$query = 'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages'; $query = 'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, FROM_BASE64(msg_title), FROM_BASE64(msg_body), msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages';
if($hasOwnerInfo) { if($hasOwnerInfo) {
++$args; ++$args;
$query .= ' WHERE msg_owner_id = ?'; $query .= ' WHERE msg_owner_id = ?';
@ -162,7 +162,7 @@ class MessagesDatabase {
bool $useReplyTo = false bool $useReplyTo = false
): MessageInfo { ): MessageInfo {
$stmt = $this->cache->get(sprintf( $stmt = $this->cache->get(sprintf(
'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages WHERE msg_id = %s AND msg_owner_id = ?', 'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, FROM_BASE64(msg_title), FROM_BASE64(msg_body), msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages WHERE msg_id = %s AND msg_owner_id = ?',
!$useReplyTo || $messageInfoOrId instanceof MessageInfo ? '?' : '(SELECT msg_reply_to FROM msz_messages WHERE msg_id = ?)' !$useReplyTo || $messageInfoOrId instanceof MessageInfo ? '?' : '(SELECT msg_reply_to FROM msz_messages WHERE msg_id = ?)'
)); ));
@ -189,10 +189,10 @@ class MessagesDatabase {
string $body, string $body,
int $parser, int $parser,
MessageInfo|string|null $replyTo = null, MessageInfo|string|null $replyTo = null,
DateTime|int|null $sentAt = null, DateTimeInterface|int|null $sentAt = null,
DateTime|int|null $readAt = null DateTimeInterface|int|null $readAt = null
): MessageInfo { ): MessageInfo {
$stmt = $this->cache->get('INSERT INTO msz_messages (msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, msg_sent, msg_read) VALUES (?, ?, ?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?))'); $stmt = $this->cache->get('INSERT INTO msz_messages (msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, msg_sent, msg_read) VALUES (?, ?, ?, ?, ?, TO_BASE64(?), TO_BASE64(?), ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?))');
$stmt->addParameter(1, $messageId); $stmt->addParameter(1, $messageId);
$stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo); $stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
$stmt->addParameter(3, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo); $stmt->addParameter(3, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo);
@ -201,8 +201,8 @@ class MessagesDatabase {
$stmt->addParameter(6, $title); $stmt->addParameter(6, $title);
$stmt->addParameter(7, $body); $stmt->addParameter(7, $body);
$stmt->addParameter(8, $parser); $stmt->addParameter(8, $parser);
$stmt->addParameter(9, $sentAt instanceof DateTime ? $sentAt->getUnixTimeSeconds() : $sentAt); $stmt->addParameter(9, $sentAt instanceof DateTimeInterface ? (int)$sentAt->format('U') : $sentAt);
$stmt->addParameter(10, $readAt instanceof DateTime ? $readAt->getUnixTimeSeconds() : $readAt); $stmt->addParameter(10, $readAt instanceof DateTimeInterface ? (int)$readAt->format('U') : $readAt);
$stmt->execute(); $stmt->execute();
return $this->getMessageInfo($ownerInfo, $messageId); return $this->getMessageInfo($ownerInfo, $messageId);
@ -214,8 +214,8 @@ class MessagesDatabase {
?string $title = null, ?string $title = null,
?string $body = null, ?string $body = null,
?int $parser = null, ?int $parser = null,
DateTime|int|null|false $sentAt = false, DateTimeInterface|int|null|false $sentAt = false,
DateTime|int|null|false $readAt = false DateTimeInterface|int|null|false $readAt = false
): void { ): void {
$setQuery = []; $setQuery = [];
$setValues = []; $setValues = [];
@ -233,12 +233,12 @@ class MessagesDatabase {
} }
if($title !== null) { if($title !== null) {
$setQuery[] = 'msg_title = ?'; $setQuery[] = 'msg_title = TO_BASE64(?)';
$setValues[] = $title; $setValues[] = $title;
} }
if($body !== null) { if($body !== null) {
$setQuery[] = 'msg_body = ?'; $setQuery[] = 'msg_body = TO_BASE64(?)';
$setValues[] = $body; $setValues[] = $body;
} }
@ -249,12 +249,12 @@ class MessagesDatabase {
if($sentAt !== false) { if($sentAt !== false) {
$setQuery[] = 'msg_sent = FROM_UNIXTIME(?)'; $setQuery[] = 'msg_sent = FROM_UNIXTIME(?)';
$setValues[] = $sentAt instanceof DateTime ? $sentAt->getUnixTimeSeconds() : $sentAt; $setValues[] = $sentAt instanceof DateTimeInterface ? (int)$sentAt->format('U') : $sentAt;
} }
if($readAt !== false) { if($readAt !== false) {
$setQuery[] = 'msg_read = FROM_UNIXTIME(?)'; $setQuery[] = 'msg_read = FROM_UNIXTIME(?)';
$setValues[] = $readAt instanceof DateTime ? $readAt->getUnixTimeSeconds() : $readAt; $setValues[] = $readAt instanceof DateTimeInterface ? (int)$readAt->format('U') : $readAt;
} }
if(empty($whereQuery)) if(empty($whereQuery))

View file

@ -39,6 +39,10 @@ class MessagesRoutes extends RouteHandler {
if(!$this->authInfo->isLoggedIn()) if(!$this->authInfo->isLoggedIn())
return 401; return 401;
// do not allow access to PMs when impersonating in production mode
if(!MSZ_DEBUG && $this->authInfo->isImpersonating())
return 403;
$globalPerms = $this->authInfo->getPerms('global'); $globalPerms = $this->authInfo->getPerms('global');
if(!$globalPerms->check(Perm::G_MESSAGES_VIEW)) if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
return 403; return 403;
@ -353,7 +357,6 @@ class MessagesRoutes extends RouteHandler {
'error' => [ 'error' => [
'name' => 'msgs:recipient_invalid', 'name' => 'msgs:recipient_invalid',
'text' => 'Name of the recipient was incorrectly formatted.', 'text' => 'Name of the recipient was incorrectly formatted.',
'jeff' => $recipient,
], ],
]; ];
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {

View file

@ -1,11 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Index\Environment;
use Index\Data\IDbConnection;
use Index\Data\Migration\{IDbMigrationRepo,DbMigrationManager,FsDbMigrationRepo};
use Sasae\SasaeEnvironment;
use Syokuhou\IConfig;
use Misuzu\Template; use Misuzu\Template;
use Misuzu\Auth\{AuthContext,AuthInfo}; use Misuzu\Auth\{AuthContext,AuthInfo};
use Misuzu\AuditLog\AuditLog; use Misuzu\AuditLog\AuditLog;
@ -20,6 +15,12 @@ use Misuzu\Perms\Permissions;
use Misuzu\Profile\ProfileFields; use Misuzu\Profile\ProfileFields;
use Misuzu\URLs\URLRegistry; use Misuzu\URLs\URLRegistry;
use Misuzu\Users\{UsersContext,UserInfo}; use Misuzu\Users\{UsersContext,UserInfo};
use Aiwass\HmacVerificationProvider;
use Aiwass\Server\RpcServer;
use Index\Data\IDbConnection;
use Index\Data\Migration\{IDbMigrationRepo,DbMigrationManager,FsDbMigrationRepo};
use Sasae\SasaeEnvironment;
use Syokuhou\IConfig;
// this class should function as the root for everything going forward // this class should function as the root for everything going forward
// no more magical static classes that are just kind of assumed to exist // no more magical static classes that are just kind of assumed to exist
@ -191,7 +192,7 @@ class MisuzuContext {
['eeprom.appmsgs:s', '', 'eeprom_app_messages'], ['eeprom.appmsgs:s', '', 'eeprom_app_messages'],
]); ]);
$isDebug = Environment::isDebug(); $isDebug = MSZ_DEBUG;
$globals['site_info'] = $this->siteInfo; $globals['site_info'] = $this->siteInfo;
$globals['auth_info'] = $this->authInfo; $globals['auth_info'] = $this->authInfo;
$globals['active_ban_info'] = $this->usersCtx->tryGetActiveBan($this->authInfo->getUserInfo()); $globals['active_ban_info'] = $this->usersCtx->tryGetActiveBan($this->authInfo->getUserInfo());
@ -280,6 +281,37 @@ class MisuzuContext {
$routingCtx->register(new LegacyRoutes($this->urls)); $routingCtx->register(new LegacyRoutes($this->urls));
$rpcServer = new RpcServer;
$routingCtx->getRouter()->register($rpcServer->createRouteHandler(
new HmacVerificationProvider(fn() => $this->config->getString('aleister.secret'))
));
$rpcServer->register(new Auth\AuthRpcActions(
$this->config->scopeTo('impersonate'),
$this->usersCtx,
$this->authCtx
));
$rpcServer->register(new Users\UsersRpcActions(
$this->siteInfo,
$this->urls,
$this->usersCtx
));
// This RPC server will eventually despawn when Hanyuu fully owns auth
$hanyuuRpcServer = new RpcServer;
$routingCtx->getRouter()->scopeTo('/_hanyuu')->register($hanyuuRpcServer->createRouteHandler(
new HmacVerificationProvider(fn() => $this->config->getString('hanyuu.secret'))
));
$hanyuuRpcServer->register(new Hanyuu\HanyuuRpcActions(
fn() => $this->config->getString('hanyuu.endpoint'),
$this->config->scopeTo('impersonate'),
$this->urls,
$this->usersCtx,
$this->authCtx
));
return $routingCtx; return $routingCtx;
} }
} }

View file

@ -1,10 +1,11 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Index\DateTime; use DateTimeInterface;
use Misuzu\MisuzuContext; use Misuzu\MisuzuContext;
use Misuzu\Tools; use Misuzu\Tools;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Carbon\CarbonImmutable;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
@ -46,30 +47,18 @@ final class MisuzuSasaeExtension extends AbstractExtension {
return $this->assets?->{$name} ?? ''; return $this->assets?->{$name} ?? '';
} }
public function timeFormat(DateTime|string|int|null $dateTime): string { public function timeFormat(DateTimeInterface|string|int|null $dateTime): string {
if($dateTime === null) if($dateTime === null)
return 'never'; return 'never';
if(is_string($dateTime)) if(is_string($dateTime))
$dateTime = new DateTime($dateTime); $dateTime = new CarbonImmutable($dateTime);
elseif(is_int($dateTime)) elseif(is_int($dateTime))
$dateTime = DateTime::fromUnixTimeSeconds($dateTime); $dateTime = CarbonImmutable::createFromTimestampUTC($dateTime);
elseif(!($dateTime instanceof CarbonImmutable))
$dateTime = new CarbonImmutable($dateTime);
$string = ''; return $dateTime->diffForHumans();
$now = DateTime::now();
$isDiffYear = $now->getYear() !== $dateTime->getYear();
if($isDiffYear || $now->getMonth() !== $dateTime->getMonth() || $now->getDay() !== $dateTime->getDay()) {
$string .= $dateTime->format('M jS');
if($isDiffYear)
$string .= $dateTime->format(' Y');
$string .= ', ';
}
$string .= $dateTime->format('G:i ');
$string .= $dateTime->isUTC() ? 'UTC' : $dateTime->format('T');
return $string;
} }
public function getHeaderMenu(): array { public function getHeaderMenu(): array {

View file

@ -3,7 +3,7 @@ namespace Misuzu\News;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\DateTime; use DateTimeInterface;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Data\IDbResult; use Index\Data\IDbResult;
@ -286,14 +286,14 @@ class News {
string $body, string $body,
bool $featured = false, bool $featured = false,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $schedule = null DateTimeInterface|int|null $schedule = null
): NewsPostInfo { ): NewsPostInfo {
if($categoryInfo instanceof NewsCategoryInfo) if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId(); $categoryInfo = $categoryInfo->getId();
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($schedule instanceof DateTime) if($schedule instanceof DateTimeInterface)
$schedule = $schedule->getUnixTimeSeconds(); $schedule = (int)$schedule->format('U');
$title = trim($title); $title = trim($title);
if(empty($title)) if(empty($title))
@ -351,7 +351,7 @@ class News {
?bool $featured = null, ?bool $featured = null,
bool $updateUserInfo = false, bool $updateUserInfo = false,
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $schedule = null DateTimeInterface|int|null $schedule = null
): void { ): void {
if($postInfo instanceof NewsPostInfo) if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId(); $postInfo = $postInfo->getId();
@ -359,8 +359,8 @@ class News {
$categoryInfo = $categoryInfo->getId(); $categoryInfo = $categoryInfo->getId();
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($schedule instanceof DateTime) if($schedule instanceof DateTimeInterface)
$schedule = $schedule->getUnixTimeSeconds(); $schedule = (int)$schedule->format('U');
if($title !== null) { if($title !== null) {
$title = trim($title); $title = trim($title);

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\News; namespace Misuzu\News;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class NewsCategoryInfo { class NewsCategoryInfo {
@ -45,8 +45,8 @@ class NewsCategoryInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getPostsCount(): int { public function getPostsCount(): int {

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\News; namespace Misuzu\News;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class NewsPostInfo { class NewsPostInfo {
@ -84,8 +84,8 @@ class NewsPostInfo {
return $this->scheduled; return $this->scheduled;
} }
public function getScheduledAt(): DateTime { public function getScheduledAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->scheduled); return CarbonImmutable::createFromTimestampUTC($this->scheduled);
} }
public function isPublished(): bool { public function isPublished(): bool {
@ -96,16 +96,16 @@ class NewsPostInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getUpdatedTime(): int { public function getUpdatedTime(): int {
return $this->updated; return $this->updated;
} }
public function getUpdatedAt(): DateTime { public function getUpdatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->updated); return CarbonImmutable::createFromTimestampUTC($this->updated);
} }
public function isEdited(): bool { public function isEdited(): bool {
@ -120,7 +120,7 @@ class NewsPostInfo {
return $this->deleted; return $this->deleted;
} }
public function getDeletedAt(): ?DateTime { public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
} }
} }

View file

@ -2,7 +2,6 @@
namespace Misuzu\News; namespace Misuzu\News;
use RuntimeException; use RuntimeException;
use Index\DateTime;
use Index\Data\{DbTools,IDbConnection}; use Index\Data\{DbTools,IDbConnection};
use Index\Http\Routing\{HttpGet,RouteHandler}; use Index\Http\Routing\{HttpGet,RouteHandler};
use Misuzu\{Pagination,SiteInfo,Template}; use Misuzu\{Pagination,SiteInfo,Template};

View file

@ -4,8 +4,7 @@ namespace Misuzu\Perms;
use stdClass; use stdClass;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\XDateTime;
use Index\Environment;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
@ -371,10 +370,10 @@ class Permissions {
} }
private static function precalculatePermissionsLog(string $fmt, ...$args): void { private static function precalculatePermissionsLog(string $fmt, ...$args): void {
if(!Environment::isConsole()) if(!MSZ_CLI)
return; return;
echo DateTime::now()->format('[H:i:s.u] '); echo XDateTime::now()->format('[H:i:s.u] ');
vprintf($fmt, $args); vprintf($fmt, $args);
echo PHP_EOL; echo PHP_EOL;
} }

View file

@ -2,15 +2,15 @@
namespace Misuzu\SharpChat; namespace Misuzu\SharpChat;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour;
use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler};
use Syokuhou\IConfig;
use Misuzu\RoutingContext; use Misuzu\RoutingContext;
use Misuzu\Auth\{AuthContext,AuthInfo,Sessions}; use Misuzu\Auth\{AuthContext,AuthInfo,Sessions};
use Misuzu\Emoticons\Emotes; use Misuzu\Emoticons\Emotes;
use Misuzu\Perms\Permissions; use Misuzu\Perms\Permissions;
use Misuzu\URLs\URLRegistry; use Misuzu\URLs\URLRegistry;
use Misuzu\Users\{Bans,UsersContext,UserInfo}; use Misuzu\Users\{Bans,UsersContext,UserInfo};
use Index\Colour\Colour;
use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler};
use Syokuhou\IConfig;
final class SharpChatRoutes extends RouteHandler { final class SharpChatRoutes extends RouteHandler {
private string $hashKey; private string $hashKey;
@ -188,7 +188,50 @@ final class SharpChatRoutes extends RouteHandler {
if(!hash_equals($realHash, $userHash)) if(!hash_equals($realHash, $userHash))
return ['success' => false, 'reason' => 'hash']; return ['success' => false, 'reason' => 'hash'];
if($authMethod === 'SESS' || $authMethod === 'Misuzu') { if(strcasecmp($authMethod, 'Bearer') === 0) {
$bearerCheck = $this->config->getString('bearerCheck');
if($bearerCheck === '')
return ['success' => false, 'reason' => 'unsupported'];
$req = curl_init($bearerCheck);
try {
curl_setopt_array($req, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HEADER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Misuzu',
CURLOPT_HTTPHEADER => [
sprintf('Authorization: Bearer %s', $authToken),
],
]);
$response = curl_exec($req);
if($response === false)
return ['success' => false, 'reason' => 'request'];
} finally {
curl_close($req);
}
$decoded = json_decode($response);
if($decoded === null)
return ['success' => false, 'reason' => 'decode'];
if(empty($decoded->user_id))
return ['success' => false, 'reason' => 'token'];
try {
$userInfo = $this->usersCtx->getUsers()->getUser($decoded->user_id, 'id');
} catch(RuntimeException $ex) {
return ['success' => false, 'reason' => 'user'];
}
} elseif($authMethod === 'SESS' || strcasecmp($authMethod, 'Misuzu') === 0) {
$tokenPacker = $this->authCtx->createAuthTokenPacker(); $tokenPacker = $this->authCtx->createAuthTokenPacker();
$tokenInfo = $tokenPacker->unpack($authToken); $tokenInfo = $tokenPacker->unpack($authToken);
if($tokenInfo->isEmpty()) { if($tokenInfo->isEmpty()) {

View file

@ -2,7 +2,7 @@
namespace Misuzu; namespace Misuzu;
use InvalidArgumentException; use InvalidArgumentException;
use Index\Serialisation\Base32; use Index\Base32;
class TOTPGenerator { class TOTPGenerator {
public const DIGITS = 6; public const DIGITS = 6;

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Users; namespace Misuzu\Users;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class BanInfo { class BanInfo {
@ -69,8 +69,8 @@ class BanInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function isPermanent(): bool { public function isPermanent(): bool {
@ -81,8 +81,8 @@ class BanInfo {
return $this->expires; return $this->expires;
} }
public function getExpiresAt(): ?DateTime { public function getExpiresAt(): ?CarbonImmutable {
return $this->expires === null ? null : DateTime::fromUnixTimeSeconds($this->expires); return $this->expires === null ? null : CarbonImmutable::createFromTimestampUTC($this->expires);
} }
public function isActive(): bool { public function isActive(): bool {

View file

@ -3,11 +3,11 @@ namespace Misuzu\Users;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\DateTime; use DateTimeInterface;
use Misuzu\Pagination;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Misuzu\Pagination;
class Bans { class Bans {
public const SEVERITY_MAX = 10; public const SEVERITY_MAX = 10;
@ -152,7 +152,7 @@ class Bans {
public function createBan( public function createBan(
UserInfo|string $userInfo, UserInfo|string $userInfo,
DateTime|int|null $expires, DateTimeInterface|int|null $expires,
string $publicReason, string $publicReason,
string $privateReason, string $privateReason,
int $severity = self::SEVERITY_DEFAULT, int $severity = self::SEVERITY_DEFAULT,
@ -164,8 +164,8 @@ class Bans {
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if($modInfo instanceof UserInfo) if($modInfo instanceof UserInfo)
$modInfo = $modInfo->getId(); $modInfo = $modInfo->getId();
if($expires instanceof DateTime) if($expires instanceof DateTimeInterface)
$expires = $expires->getUnixTimeSeconds(); $expires = (int)$expires->format('U');
$stmt = $this->cache->get('INSERT INTO msz_users_bans (user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, ban_expires) VALUES (?, ?, ?, ?, ?, FROM_UNIXTIME(?))'); $stmt = $this->cache->get('INSERT INTO msz_users_bans (user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, ban_expires) VALUES (?, ?, ?, ?, ?, FROM_UNIXTIME(?))');
$stmt->addParameter(1, $userInfo); $stmt->addParameter(1, $userInfo);

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Users; namespace Misuzu\Users;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class ModNoteInfo { class ModNoteInfo {
@ -45,8 +45,8 @@ class ModNoteInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function getTitle(): string { public function getTitle(): string {

View file

@ -2,7 +2,7 @@
namespace Misuzu\Users; namespace Misuzu\Users;
use Stringable; use Stringable;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Data\IDbResult; use Index\Data\IDbResult;
@ -89,8 +89,8 @@ class RoleInfo implements Stringable {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function __toString(): string { public function __toString(): string {

View file

@ -1,12 +1,11 @@
<?php <?php
namespace Misuzu\Users; namespace Misuzu\Users;
use Index\DateTime; use Misuzu\Parsers\Parser;
use Index\TimeZoneInfo; use Carbon\CarbonImmutable;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\Parsers\Parser;
class UserInfo { class UserInfo {
public function __construct( public function __construct(
@ -131,8 +130,8 @@ class UserInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
public function hasLastActive(): bool { public function hasLastActive(): bool {
@ -143,8 +142,8 @@ class UserInfo {
return $this->lastActive; return $this->lastActive;
} }
public function getLastActiveAt(): ?DateTime { public function getLastActiveAt(): ?CarbonImmutable {
return $this->lastActive === null ? null : DateTime::fromUnixTimeSeconds($this->lastActive); return $this->lastActive === null ? null : CarbonImmutable::createFromTimestampUTC($this->lastActive);
} }
public function isDeleted(): bool { public function isDeleted(): bool {
@ -155,8 +154,8 @@ class UserInfo {
return $this->deleted; return $this->deleted;
} }
public function getDeletedAt(): ?DateTime { public function getDeletedAt(): ?CarbonImmutable {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
} }
public function hasDisplayRoleId(): bool { public function hasDisplayRoleId(): bool {
@ -231,15 +230,15 @@ class UserInfo {
return $this->birthdate; return $this->birthdate;
} }
public function getBirthdate(): ?DateTime { public function getBirthdate(): ?CarbonImmutable {
return $this->birthdate === null ? null : DateTime::createFromFormat('Y-m-d', $this->birthdate, TimeZoneInfo::utc()); return $this->birthdate === null ? null : CarbonImmutable::createFromFormat('Y-m-d', $this->birthdate, 'UTC');
} }
public function getAge(): int { public function getAge(): int {
$birthdate = $this->getBirthdate(); $birthdate = $this->getBirthdate();
if($birthdate === null || $birthdate->getYear() < 1900) if($birthdate === null || (int)$birthdate->format('Y') < 1900)
return -1; return -1;
return (int)$birthdate->diff(DateTime::now())->format('%y'); return (int)$birthdate->diff(CarbonImmutable::now())->format('%y');
} }
public function hasBackgroundSettings(): bool { public function hasBackgroundSettings(): bool {

View file

@ -3,7 +3,7 @@ namespace Misuzu\Users;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\DateTime; use DateTimeInterface;
use Index\XString; use Index\XString;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
@ -46,7 +46,7 @@ class Users {
UserInfo|string|null $after = null, UserInfo|string|null $after = null,
?int $lastActiveInMinutes = null, ?int $lastActiveInMinutes = null,
?int $newerThanDays = null, ?int $newerThanDays = null,
?DateTime $birthdate = null, ?DateTimeInterface $birthdate = null,
?bool $deleted = null ?bool $deleted = null
): int { ): int {
if($roleInfo instanceof RoleInfo) if($roleInfo instanceof RoleInfo)
@ -106,7 +106,7 @@ class Users {
UserInfo|string|null $after = null, UserInfo|string|null $after = null,
?int $lastActiveInMinutes = null, ?int $lastActiveInMinutes = null,
?int $newerThanDays = null, ?int $newerThanDays = null,
?DateTime $birthdate = null, ?DateTimeInterface $birthdate = null,
?bool $deleted = null, ?bool $deleted = null,
?string $orderBy = null, ?string $orderBy = null,
?bool $reverseOrder = null, ?bool $reverseOrder = null,

View file

@ -0,0 +1,78 @@
<?php
namespace Misuzu\Users;
use RuntimeException;
use Misuzu\SiteInfo;
use Misuzu\URLs\URLRegistry;
use Misuzu\Users\Assets\UserAvatarAsset;
use Aiwass\Server\{RpcActionHandler,RpcQuery};
use Index\Colour\{Colour,ColourRGB};
final class UsersRpcActions extends RpcActionHandler {
public function __construct(
private SiteInfo $siteInfo,
private URLRegistry $urls,
private UsersContext $usersCtx
) {}
#[RpcQuery('misuzu:users:getUser')]
public function queryGetUser(string $userId): array {
try {
$userInfo = $this->usersCtx->getUserInfo($userId, Users::GET_USER_ID);
} catch(RuntimeException) {
return ['error' => 'notfound'];
}
// TODO: there should be some kinda privacy controls for users
$rank = $this->usersCtx->getUserRank($userInfo);
$colour = $this->usersCtx->getUserColour($userInfo);
if($colour->shouldInherit()) {
$colourRaw = null;
$colourCSS = (string)$colour;
} else {
// Index doesn't have a proper toRawRGB func???
$colourRaw = Colour::toMisuzu($colour) & 0xFFFFFF;
$colourCSS = (string)ColourRGB::convert($colour);
}
$baseUrl = $this->siteInfo->getURL();
$avatars = [];
$formatAvatarUrl = fn($res = 0) => (
$baseUrl . $this->urls->format('user-avatar', ['user' => $userInfo->getId(), 'res' => $res])
);
$avatars[] = ['res' => 0, 'url' => $formatAvatarUrl()];
foreach(UserAvatarAsset::DIMENSIONS as $res)
$avatars[] = ['res' => $res, 'url' => $formatAvatarUrl($res)];
$avatars = array_reverse($avatars);
$output = [
'id' => $userInfo->getId(),
'name' => $userInfo->getName(),
'colour_raw' => $colourRaw,
'colour_css' => $colourCSS,
'rank' => $rank,
'country_code' => $userInfo->getCountryCode(),
'avatar_urls' => $avatars,
'profile_url' => $baseUrl . $this->urls->format('user-profile', ['user' => $userInfo->getId()]),
'created_at' => $userInfo->getCreatedAt()->toIso8601ZuluString(),
];
if($userInfo->hasLastActive())
$output['last_active_at'] = $userInfo->getLastActiveAt()->toIso8601ZuluString();
if($userInfo->hasTitle())
$output['title'] = $userInfo->getTitle();
if($userInfo->isSuperUser())
$output['is_super'] = true;
if($userInfo->isDeleted())
$output['is_deleted'] = true;
return $output;
}
}

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\Users; namespace Misuzu\Users;
use Index\DateTime; use Carbon\CarbonImmutable;
use Index\Data\IDbResult; use Index\Data\IDbResult;
class WarningInfo { class WarningInfo {
@ -55,7 +55,7 @@ class WarningInfo {
return $this->created; return $this->created;
} }
public function getCreatedAt(): DateTime { public function getCreatedAt(): CarbonImmutable {
return DateTime::fromUnixTimeSeconds($this->created); return CarbonImmutable::createFromTimestampUTC($this->created);
} }
} }

View file

@ -132,6 +132,15 @@ msz_sched_task_func('Resync statistics counters.', true, function() use ($msz) {
'users:warnings:visible' => 'SELECT COUNT(*) FROM msz_users_warnings WHERE warn_created > NOW() - INTERVAL 90 DAY', 'users:warnings:visible' => 'SELECT COUNT(*) FROM msz_users_warnings WHERE warn_created > NOW() - INTERVAL 90 DAY',
'users:bans:total' => 'SELECT COUNT(*) FROM msz_users_bans', 'users:bans:total' => 'SELECT COUNT(*) FROM msz_users_bans',
'users:bans:active' => 'SELECT COUNT(*) FROM msz_users_bans WHERE ban_expires IS NULL OR ban_expires > NOW()', 'users:bans:active' => 'SELECT COUNT(*) FROM msz_users_bans WHERE ban_expires IS NULL OR ban_expires > NOW()',
'pms:msgs:total' => 'SELECT COUNT(*) FROM msz_messages',
'pms:msgs:messages' => 'SELECT COUNT(DISTINCT msg_id) FROM msz_messages',
'pms:msgs:replies' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_reply_to IS NULL',
'pms:msgs:drafts' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_sent IS NULL',
'pms:msgs:unread' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_read IS NULL',
'pms:msgs:deleted' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_deleted IS NOT NULL',
'pms:msgs:plain' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_parser = 0',
'pms:msgs:bbcode' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_parser = 1',
'pms:msgs:markdown' => 'SELECT COUNT(*) FROM msz_messages WHERE msg_parser = 2',
]; ];
foreach($stats as $name => $query) { foreach($stats as $name => $query) {