Compare commits
48 commits
Author | SHA1 | Date | |
---|---|---|---|
b2301dde45 | |||
39be84fcc0 | |||
242e70eabf | |||
174ceaa4e7 | |||
058b409adf | |||
8e006c7003 | |||
23d47fa6d2 | |||
bdad34e065 | |||
fc6a899f16 | |||
1f16de2239 | |||
1550a5da57 | |||
7ef1974c88 | |||
0f45a5f60f | |||
324fe21d73 | |||
153abde3a2 | |||
f8aaa71260 | |||
37a3bc1ee6 | |||
f547812d5a | |||
8a06836985 | |||
34528ae413 | |||
0bf7ca0d52 | |||
cc9fccdf18 | |||
ca77b501e7 | |||
2439f87df9 | |||
400253e04b | |||
01c60e3027 | |||
37d8413118 | |||
8cfa07bc8c | |||
a65579bf9d | |||
44a4bb6e6f | |||
ec00cfa176 | |||
1d295df8da | |||
6a88ed8b11 | |||
36bcf1ab1d | |||
5d3e1d4960 | |||
9bb943bacf | |||
107d16cf46 | |||
0afc5186a7 | |||
0300bae994 | |||
cb0c64f8ed | |||
89ef9d9ad1 | |||
c02d922dc6 | |||
80cd6222c4 | |||
344a3c9160 | |||
df5dbdf3ad | |||
c0caceed7b | |||
be54ce2c22 | |||
070dc5e782 |
162 changed files with 3694 additions and 3304 deletions
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2017-2023, flashwave <me@flash.moe>
|
||||
Copyright (c) 2017-2024, flashwave <me@flash.moe>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.news__feeds {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
|
|
@ -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
171
build.js
|
@ -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));
|
||||
})();
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
{
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"flashwave/index": "dev-master",
|
||||
"flashwave/sasae": "dev-master",
|
||||
"flashwave/index": "^0.2410",
|
||||
"flashii/rpcii": "^2.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
1213
composer.lock
generated
1213
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"');
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
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`
|
||||
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`;
|
||||
');
|
||||
}
|
||||
}
|
13
database/2024_09_16_205613_add_role_id_string.php
Normal file
13
database/2024_09_16_205613_add_role_id_string.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
- [RPCii](https://patchii.net/flashii/rpcii): 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.
|
||||
|
|
15
misuzu.php
15
misuzu.php
|
@ -1,10 +1,9 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Index\Environment;
|
||||
use Index\Data\DbTools;
|
||||
use Syokuhou\DbConfig;
|
||||
use Syokuhou\SharpConfig;
|
||||
use Index\Db\DbBackends;
|
||||
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) {
|
||||
|
@ -38,7 +37,7 @@ if($cfg->hasValues('sentry:dsn'))
|
|||
});
|
||||
})($cfg->scopeTo('sentry'));
|
||||
|
||||
$db = DbTools::create($cfg->getString('database:dsn', 'null:'));
|
||||
$db = DbBackends::create($cfg->getString('database:dsn', 'null:'));
|
||||
$db->execute('SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';');
|
||||
|
||||
$cfg = new DbConfig($db, 'msz_config');
|
||||
|
|
942
package-lock.json
generated
942
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@swc/core": "^1.3.69",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cssnano": "^6.0.1",
|
||||
"postcss": "^8.4.26"
|
||||
"@railcomm/assproc": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,3 +4,6 @@ parameters:
|
|||
- src
|
||||
bootstrapFiles:
|
||||
- misuzu.php
|
||||
dynamicConstantNames:
|
||||
- MSZ_CLI
|
||||
- MSZ_DEBUG
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -24,4 +24,4 @@ if($authInfo->isLoggedIn()) {
|
|||
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
|
||||
}
|
||||
|
||||
Tools::redirect($msz->getURLs()->format('index'));;
|
||||
Tools::redirect($msz->getUrls()->format('index'));;
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -3,7 +3,7 @@ namespace Misuzu;
|
|||
|
||||
use Misuzu\Auth\AuthTokenCookie;
|
||||
|
||||
$urls = $msz->getURLs();
|
||||
$urls = $msz->getUrls();
|
||||
if(CSRF::validateRequest()) {
|
||||
$tokenInfo = $msz->getAuthInfo()->getTokenInfo();
|
||||
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -168,7 +168,7 @@ $perms = $perms->checkMany([
|
|||
]);
|
||||
|
||||
Template::render('forum.forum', [
|
||||
'forum_breadcrumbs' => $forumCategories->getCategoryAncestry($categoryInfo),
|
||||
'forum_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
|
||||
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
|
||||
'forum_info' => $categoryInfo,
|
||||
'forum_children' => $children,
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
|
||||
$urls = $msz->getURLs();
|
||||
$urls = $msz->getUrls();
|
||||
$forumCtx = $msz->getForumContext();
|
||||
$forumPosts = $forumCtx->getPosts();
|
||||
$usersCtx = $msz->getUsersContext();
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
@ -296,7 +297,7 @@ try {
|
|||
}
|
||||
|
||||
Template::render('forum.posting', [
|
||||
'posting_breadcrumbs' => $forumCategories->getCategoryAncestry($categoryInfo),
|
||||
'posting_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
|
||||
'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
|
||||
'posting_user' => $currentUser,
|
||||
'posting_user_colour' => $usersCtx->getUserColour($currentUser),
|
||||
|
|
|
@ -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();
|
||||
|
@ -291,7 +291,11 @@ $postInfos = $forumPosts->getPosts(
|
|||
if(empty($postInfos))
|
||||
Template::throwError(404);
|
||||
|
||||
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
|
||||
try {
|
||||
$originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
|
||||
} catch(RuntimeException $ex) {
|
||||
Template::throwError(404);
|
||||
}
|
||||
|
||||
$posts = [];
|
||||
|
||||
|
@ -326,7 +330,7 @@ $perms = $perms->checkMany([
|
|||
]);
|
||||
|
||||
Template::render('forum.topic', [
|
||||
'topic_breadcrumbs' => $forumCategories->getCategoryAncestry($topicInfo),
|
||||
'topic_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($topicInfo)),
|
||||
'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo),
|
||||
'topic_info' => $topicInfo,
|
||||
'category_info' => $categoryInfo,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ else
|
|||
try {
|
||||
$isNew = false;
|
||||
$emoteInfo = $emotes->getEmote($emoteId);
|
||||
$emoteStrings = $emotes->getEmoteStrings($emoteInfo);
|
||||
$emoteStrings = iterator_to_array($emotes->getEmoteStrings($emoteInfo));
|
||||
} catch(RuntimeException $ex) {
|
||||
Template::throwError(404);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ $pagination = new Pagination($auditLog->countLogs(), 50);
|
|||
if(!$pagination->hasValidOffset())
|
||||
Template::throwError(404);
|
||||
|
||||
$logs = $auditLog->getLogs(pagination: $pagination);
|
||||
$logs = iterator_to_array($auditLog->getLogs(pagination: $pagination));
|
||||
$userInfos = [];
|
||||
$userColours = [];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
@ -106,7 +106,7 @@ if(CSRF::validateRequest() && $canEdit) {
|
|||
}
|
||||
|
||||
$existingRoles = [];
|
||||
foreach($roles->getRoles(userInfo: $userInfo) as $roleInfo)
|
||||
foreach(iterator_to_array($roles->getRoles(userInfo: $userInfo)) as $roleInfo)
|
||||
$existingRoles[$roleInfo->getId()] = $roleInfo;
|
||||
|
||||
$removeRoles = [];
|
||||
|
@ -226,7 +226,7 @@ if(CSRF::validateRequest() && $canEdit) {
|
|||
return;
|
||||
}
|
||||
|
||||
$rolesAll = $roles->getRoles();
|
||||
$rolesAll = iterator_to_array($roles->getRoles());
|
||||
$userRoleIds = $users->hasRoles($userInfo, $rolesAll);
|
||||
|
||||
Template::render('manage.users.user', [
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
@ -113,7 +112,7 @@ if($isEditing) {
|
|||
if(!$perms->edit_profile) {
|
||||
$notices[] = 'You\'re not allowed to edit your profile';
|
||||
} else {
|
||||
$profileFieldInfos = $profileFields->getFields();
|
||||
$profileFieldInfos = iterator_to_array($profileFields->getFields());
|
||||
$profileFieldsSetInfos = [];
|
||||
$profileFieldsSetValues = [];
|
||||
$profileFieldsRemove = [];
|
||||
|
@ -299,7 +298,7 @@ $profileStats->forum_post_count = $forumCtx->countTotalUserPosts($userInfo);
|
|||
$profileStats->comments_count = $msz->getComments()->countPosts(userInfo: $userInfo, deleted: false);
|
||||
|
||||
if(!$viewingAsGuest) {
|
||||
Template::set('profile_warnings', $usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo));
|
||||
Template::set('profile_warnings', iterator_to_array($usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo)));
|
||||
|
||||
if((!$isBanned || $canEdit)) {
|
||||
$unranked = $cfg->getValues([
|
||||
|
@ -323,9 +322,9 @@ if(!$viewingAsGuest) {
|
|||
);
|
||||
$activeTopicInfo = $activeTopicStats->success ? $forumTopics->getTopic(topicId: $activeTopicStats->topicId) : null;
|
||||
|
||||
$profileFieldValues = $profileFields->getFieldValues($userInfo);
|
||||
$profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues);
|
||||
$profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues);
|
||||
$profileFieldValues = iterator_to_array($profileFields->getFieldValues($userInfo));
|
||||
$profileFieldInfos = $profileFieldInfos ?? iterator_to_array($profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues));
|
||||
$profileFieldFormats = iterator_to_array($profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues));
|
||||
|
||||
$profileFieldRawValues = [];
|
||||
$profileFieldLinkValues = [];
|
||||
|
|
|
@ -116,7 +116,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
|
|||
if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
|
||||
$userInfo = $users->getUser($userInfo->getId(), 'id');
|
||||
|
||||
$userRoles = $roles->getRoles(userInfo: $userInfo);
|
||||
$userRoles = iterator_to_array($roles->getRoles(userInfo: $userInfo));
|
||||
|
||||
Template::render('settings.account', [
|
||||
'errors' => $errors,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -15,8 +15,8 @@ $auditLog = $msz->getAuditLog();
|
|||
$loginHistoryPagination = new Pagination($loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp');
|
||||
$accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 10, 'ap');
|
||||
|
||||
$loginHistory = $loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination);
|
||||
$auditLogs = $auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination);
|
||||
$loginHistory = iterator_to_array($loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination));
|
||||
$auditLogs = iterator_to_array($auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination));
|
||||
|
||||
Template::render('settings.logs', [
|
||||
'login_history_list' => $loginHistory,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
): array {
|
||||
): iterable {
|
||||
if($userInfo instanceof UserInfo)
|
||||
$userInfo = $userInfo->getId();
|
||||
if($remoteAddr instanceof IPAddress)
|
||||
$remoteAddr = (string)$remoteAddr;
|
||||
|
||||
$hasUserInfo = $userInfo !== null;
|
||||
$hasRemoteAddr = $remoteAddr !== null;
|
||||
|
@ -98,26 +91,19 @@ class AuditLog {
|
|||
}
|
||||
|
||||
$stmt->execute();
|
||||
$result = $stmt->getResult();
|
||||
$logs = [];
|
||||
|
||||
while($result->next())
|
||||
$logs[] = new AuditLogInfo($result);
|
||||
|
||||
return $logs;
|
||||
return $stmt->getResult()->getIterator(AuditLogInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function createLog(
|
||||
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
|
||||
|
|
|
@ -2,25 +2,28 @@
|
|||
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 {
|
||||
private ?string $userId;
|
||||
private string $action;
|
||||
private array $params;
|
||||
private int $created;
|
||||
private string $address;
|
||||
private string $country;
|
||||
public function __construct(
|
||||
private ?string $userId,
|
||||
private string $action,
|
||||
private array $params,
|
||||
private int $created,
|
||||
private string $address,
|
||||
private string $country,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0);
|
||||
$this->action = $result->getString(1);
|
||||
$this->params = json_decode($result->getString(2));
|
||||
$this->created = $result->getInteger(3);
|
||||
$this->address = $result->isNull(4) ? '::1' : $result->getString(4); // apparently this being NULL is possible?
|
||||
$this->country = $result->getString(5);
|
||||
public static function fromResult(DbResult $result): AuditLogInfo {
|
||||
return new AuditLogInfo(
|
||||
userId: $result->getStringOrNull(0),
|
||||
action: $result->getString(1),
|
||||
params: json_decode($result->getString(2)),
|
||||
created: $result->getInteger(3),
|
||||
address: $result->isNull(4) ? '::1' : $result->getString(4), // apparently this being NULL is possible?
|
||||
country: $result->getString(5),
|
||||
);
|
||||
}
|
||||
|
||||
public function hasUserId(): bool {
|
||||
|
@ -43,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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
namespace Misuzu\Auth;
|
||||
|
||||
use Index\XArray;
|
||||
use Misuzu\Auth\SessionInfo;
|
||||
use Misuzu\Forum\ForumCategoryInfo;
|
||||
use Misuzu\Perms\IPermissionResult;
|
||||
|
|
65
src/Auth/AuthRpcHandler.php
Normal file
65
src/Auth/AuthRpcHandler.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
namespace Misuzu\Auth;
|
||||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\{UsersContext,UserInfo};
|
||||
use RPCii\Server\{RpcHandler,RpcHandlerCommon,RpcAction};
|
||||
use Index\Config\Config;
|
||||
|
||||
final class AuthRpcHandler implements RpcHandler {
|
||||
use RpcHandlerCommon;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[RpcAction('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(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
<?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 {
|
||||
private ?string $userId;
|
||||
private bool $success;
|
||||
private string $remoteAddr;
|
||||
private string $countryCode;
|
||||
private int $created;
|
||||
private string $userAgent;
|
||||
private string $clientInfo;
|
||||
public function __construct(
|
||||
private ?string $userId,
|
||||
private bool $success,
|
||||
private string $remoteAddr,
|
||||
private string $countryCode,
|
||||
private int $created,
|
||||
private string $userAgent,
|
||||
private string $clientInfo,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->userId = $result->isNull(0) ? null : (string)$result->getInteger(0);
|
||||
$this->success = $result->getInteger(1) !== 0;
|
||||
$this->remoteAddr = $result->getString(2);
|
||||
$this->countryCode = $result->getString(3);
|
||||
$this->created = $result->getInteger(4);
|
||||
$this->userAgent = $result->getString(5);
|
||||
$this->clientInfo = $result->getString(6);
|
||||
public static function fromResult(DbResult $result): LoginAttemptInfo {
|
||||
return new LoginAttemptInfo(
|
||||
userId: $result->getStringOrNull(0),
|
||||
success: $result->getBoolean(1),
|
||||
remoteAddr: $result->getString(2),
|
||||
countryCode: $result->getString(3),
|
||||
created: $result->getInteger(4),
|
||||
userAgent: $result->getString(5),
|
||||
clientInfo: $result->getString(6),
|
||||
);
|
||||
}
|
||||
|
||||
public function hasUserId(): bool {
|
||||
|
@ -37,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;
|
||||
}
|
||||
|
@ -53,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 {
|
||||
|
|
|
@ -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
|
||||
): array {
|
||||
): 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;
|
||||
|
@ -127,25 +116,17 @@ class LoginAttempts {
|
|||
}
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$attempts = [];
|
||||
|
||||
while($result->next())
|
||||
$attempts[] = new LoginAttemptInfo($result);
|
||||
|
||||
return $attempts;
|
||||
return $stmt->getResult()->getIterator(LoginAttemptInfo::fromResult(...));
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(?), ?)');
|
||||
|
|
|
@ -1,38 +1,41 @@
|
|||
<?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 {
|
||||
private string $id;
|
||||
private string $userId;
|
||||
private string $token;
|
||||
private string $firstRemoteAddr;
|
||||
private ?string $lastRemoteAddr;
|
||||
private string $userAgent;
|
||||
private string $clientInfo;
|
||||
private string $countryCode;
|
||||
private int $expires;
|
||||
private bool $bumpExpires;
|
||||
private int $created;
|
||||
private ?int $lastActive;
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $userId,
|
||||
private string $token,
|
||||
private string $firstRemoteAddr,
|
||||
private ?string $lastRemoteAddr,
|
||||
private string $userAgent,
|
||||
private string $clientInfo,
|
||||
private string $countryCode,
|
||||
private int $expires,
|
||||
private bool $bumpExpires,
|
||||
private int $created,
|
||||
private ?int $lastActive,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->userId = (string)$result->getInteger(1);
|
||||
$this->token = $result->getString(2);
|
||||
$this->firstRemoteAddr = $result->getString(3);
|
||||
$this->lastRemoteAddr = $result->isNull(4) ? null : $result->getString(4);
|
||||
$this->userAgent = $result->getString(5);
|
||||
$this->clientInfo = $result->getString(6);
|
||||
$this->countryCode = $result->getString(7);
|
||||
$this->expires = $result->getInteger(8);
|
||||
$this->bumpExpires = $result->getInteger(9) !== 0;
|
||||
$this->created = $result->getInteger(10);
|
||||
$this->lastActive = $result->isNull(11) ? null : $result->getInteger(11);
|
||||
public static function fromResult(DbResult $result): SessionInfo {
|
||||
return new SessionInfo(
|
||||
id: $result->getString(0),
|
||||
userId: $result->getString(1),
|
||||
token: $result->getString(2),
|
||||
firstRemoteAddr: $result->getString(3),
|
||||
lastRemoteAddr: $result->getStringOrNull(4),
|
||||
userAgent: $result->getString(5),
|
||||
clientInfo: $result->getString(6),
|
||||
countryCode: $result->getString(7),
|
||||
expires: $result->getInteger(8),
|
||||
bumpExpires: $result->getBoolean(9),
|
||||
created: $result->getInteger(10),
|
||||
lastActive: $result->getIntegerOrNull(11),
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
@ -47,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;
|
||||
}
|
||||
|
@ -87,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 {
|
||||
|
@ -103,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 {
|
||||
|
@ -115,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -58,7 +55,7 @@ class Sessions {
|
|||
public function getSessions(
|
||||
UserInfo|string|null $userInfo = null,
|
||||
?Pagination $pagination = null
|
||||
): array {
|
||||
): iterable {
|
||||
if($userInfo instanceof UserInfo)
|
||||
$userInfo = $userInfo->getId();
|
||||
|
||||
|
@ -85,13 +82,7 @@ class Sessions {
|
|||
}
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$sessions = [];
|
||||
|
||||
while($result->next())
|
||||
$sessions[] = new SessionInfo($result);
|
||||
|
||||
return $sessions;
|
||||
return $stmt->getResult()->getIterator(SessionInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function getSession(
|
||||
|
@ -124,20 +115,18 @@ class Sessions {
|
|||
if(!$result->next())
|
||||
throw new RuntimeException('Session not found.');
|
||||
|
||||
return new SessionInfo($result);
|
||||
return SessionInfo::fromResult($result);
|
||||
}
|
||||
|
||||
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));
|
||||
|
@ -249,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.');
|
||||
|
@ -257,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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
16
src/CSRF.php
16
src/CSRF.php
|
@ -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 {
|
||||
|
|
|
@ -1,24 +1,28 @@
|
|||
<?php
|
||||
namespace Misuzu\Changelog;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class ChangeInfo {
|
||||
private string $id;
|
||||
private ?string $userId;
|
||||
private int $action;
|
||||
private int $created;
|
||||
private string $summary;
|
||||
private string $body;
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private ?string $userId,
|
||||
private int $action,
|
||||
private int $created,
|
||||
private string $summary,
|
||||
private string $body,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1);
|
||||
$this->action = $result->getInteger(2);
|
||||
$this->created = $result->getInteger(3);
|
||||
$this->summary = $result->getString(4);
|
||||
$this->body = $result->getString(5);
|
||||
public static function fromResult(DbResult $result): ChangeInfo {
|
||||
return new ChangeInfo(
|
||||
id: $result->getString(0),
|
||||
userId: $result->getStringOrNull(1),
|
||||
action: $result->getInteger(2),
|
||||
created: $result->getInteger(3),
|
||||
summary: $result->getString(4),
|
||||
body: $result->getString(5),
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
@ -49,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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
): array {
|
||||
): 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;
|
||||
|
@ -169,13 +166,7 @@ class Changelog {
|
|||
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$changes = [];
|
||||
|
||||
while($result->next())
|
||||
$changes[] = new ChangeInfo($result);
|
||||
|
||||
return $changes;
|
||||
return $stmt->getResult()->getIterator(ChangeInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function getChange(string $changeId): ChangeInfo {
|
||||
|
@ -187,7 +178,7 @@ class Changelog {
|
|||
if(!$result->next())
|
||||
throw new RuntimeException('No tag with that ID exists.');
|
||||
|
||||
return new ChangeInfo($result);
|
||||
return ChangeInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public function createChange(
|
||||
|
@ -195,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))
|
||||
|
@ -239,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();
|
||||
|
@ -248,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);
|
||||
|
|
|
@ -3,26 +3,20 @@ namespace Misuzu\Changelog;
|
|||
|
||||
use ErrorException;
|
||||
use RuntimeException;
|
||||
use Index\Routing\Route;
|
||||
use Index\Routing\RouteHandler;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\SiteInfo;
|
||||
use Misuzu\Template;
|
||||
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;
|
||||
use Misuzu\Comments\CommentsEx;
|
||||
use Misuzu\Feeds\Feed;
|
||||
use Misuzu\Feeds\FeedItem;
|
||||
use Misuzu\Feeds\AtomFeedSerializer;
|
||||
use Misuzu\Feeds\RssFeedSerializer;
|
||||
use Misuzu\URLs\URLInfo;
|
||||
use Misuzu\URLs\URLRegistry;
|
||||
use Misuzu\Comments\{Comments,CommentsEx};
|
||||
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,
|
||||
|
@ -34,8 +28,8 @@ final class ChangelogRoutes extends RouteHandler {
|
|||
return $comments->getCommentsForLayout($categoryName);
|
||||
}
|
||||
|
||||
#[Route('GET', '/changelog')]
|
||||
#[URLInfo('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
|
||||
#[HttpGet('/changelog')]
|
||||
#[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);
|
||||
|
@ -73,15 +67,16 @@ final class ChangelogRoutes extends RouteHandler {
|
|||
if(!$pagination->hasValidOffset())
|
||||
return 404;
|
||||
|
||||
$changeInfos = $this->changelog->getChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
|
||||
if(empty($changeInfos))
|
||||
return 404;
|
||||
|
||||
$changes = [];
|
||||
$changeInfos = $this->changelog->getChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
|
||||
$commentsCategoryName = null;
|
||||
|
||||
foreach($changeInfos as $changeInfo) {
|
||||
$userInfo = $changeInfo->hasUserId() ? $this->usersCtx->getUserInfo($changeInfo->getUserId()) : null;
|
||||
|
||||
if($commentsCategoryName === null)
|
||||
$commentsCategoryName = $changeInfo->getCommentsCategoryName();
|
||||
|
||||
$changes[] = [
|
||||
'change' => $changeInfo,
|
||||
'user' => $userInfo,
|
||||
|
@ -89,19 +84,22 @@ final class ChangelogRoutes extends RouteHandler {
|
|||
];
|
||||
}
|
||||
|
||||
if(empty($changes))
|
||||
return 404;
|
||||
|
||||
return Template::renderRaw('changelog.index', [
|
||||
'changelog_infos' => $changes,
|
||||
'changelog_date' => $filterDate,
|
||||
'changelog_user' => $filterUser,
|
||||
'changelog_tags' => $filterTags,
|
||||
'changelog_pagination' => $pagination,
|
||||
'comments_info' => empty($filterDate) ? null : $this->getCommentsInfo($changeInfos[0]->getCommentsCategoryName()),
|
||||
'comments_info' => empty($filterDate) && $commentsCategoryName !== null ? null : $this->getCommentsInfo($commentsCategoryName),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('GET', '/changelog/change/:id')]
|
||||
#[URLInfo('changelog-change', '/changelog/change/<change>')]
|
||||
#[URLInfo('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
|
||||
#[HttpGet('/changelog/change/([0-9]+)')]
|
||||
#[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);
|
||||
|
@ -121,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;
|
||||
}
|
||||
|
||||
#[Route('GET', '/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'));
|
||||
}
|
||||
|
||||
#[Route('GET', '/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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -46,7 +44,7 @@ class Comments {
|
|||
public function getCategories(
|
||||
UserInfo|string|null $owner = null,
|
||||
?Pagination $pagination = null
|
||||
): array {
|
||||
): iterable {
|
||||
if($owner instanceof UserInfo)
|
||||
$owner = $owner->getId();
|
||||
|
||||
|
@ -72,13 +70,7 @@ class Comments {
|
|||
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$categories = [];
|
||||
|
||||
while($result->next())
|
||||
$categories[] = new CommentsCategoryInfo($result);
|
||||
|
||||
return $categories;
|
||||
return $stmt->getResult()->getIterator(CommentsCategoryInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function getCategory(
|
||||
|
@ -124,7 +116,7 @@ class Comments {
|
|||
if(!$result->next())
|
||||
throw new RuntimeException('Comments category not found.');
|
||||
|
||||
return new CommentsCategoryInfo($result);
|
||||
return CommentsCategoryInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public function checkCategoryNameExists(string $name): bool {
|
||||
|
@ -276,7 +268,7 @@ class Comments {
|
|||
?bool $deleted = null,
|
||||
bool $includeRepliesCount = false,
|
||||
bool $includeVotesCount = false
|
||||
): array {
|
||||
): iterable {
|
||||
if($categoryInfo instanceof CommentsCategoryInfo)
|
||||
$categoryInfo = $categoryInfo->getId();
|
||||
if($parentInfo instanceof CommentsPostInfo)
|
||||
|
@ -330,13 +322,7 @@ class Comments {
|
|||
$stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
|
||||
$stmt->execute();
|
||||
|
||||
$posts = [];
|
||||
$result = $stmt->getResult();
|
||||
|
||||
while($result->next())
|
||||
$posts[] = new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount);
|
||||
|
||||
return $posts;
|
||||
return $stmt->getResult()->getIterator(fn($result) => CommentsPostInfo::fromResult($result, $includeRepliesCount, $includeVotesCount));
|
||||
}
|
||||
|
||||
public function getPost(
|
||||
|
@ -362,7 +348,7 @@ class Comments {
|
|||
if(!$result->next())
|
||||
throw new RuntimeException('No comment with that ID exists.');
|
||||
|
||||
return new CommentsPostInfo($result, $includeRepliesCount, $includeVotesCount);
|
||||
return CommentsPostInfo::fromResult($result, $includeRepliesCount, $includeVotesCount);
|
||||
}
|
||||
|
||||
public function createPost(
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
<?php
|
||||
namespace Misuzu\Comments;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Misuzu\Users\UserInfo;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class CommentsCategoryInfo {
|
||||
private string $id;
|
||||
private string $name;
|
||||
private ?string $ownerId;
|
||||
private int $created;
|
||||
private ?int $locked;
|
||||
private int $comments;
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $name,
|
||||
private ?string $ownerId,
|
||||
private int $created,
|
||||
private ?int $locked,
|
||||
private int $comments,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->name = $result->getString(1);
|
||||
$this->ownerId = $result->isNull(2) ? null : (string)$result->getInteger(2);
|
||||
$this->created = $result->getInteger(3);
|
||||
$this->locked = $result->isNull(4) ? null : $result->getInteger(4);
|
||||
$this->comments = $result->getInteger(5);
|
||||
public static function fromResult(DbResult $result): CommentsCategoryInfo {
|
||||
return new CommentsCategoryInfo(
|
||||
id: $result->getString(0),
|
||||
name: $result->getString(1),
|
||||
ownerId: $result->getStringOrNull(2),
|
||||
created: $result->getInteger(3),
|
||||
locked: $result->getIntegerOrNull(4),
|
||||
comments: $result->getInteger(5),
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
@ -50,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 {
|
||||
|
|
|
@ -1,51 +1,57 @@
|
|||
<?php
|
||||
namespace Misuzu\Comments;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class CommentsPostInfo {
|
||||
private string $id;
|
||||
private string $categoryId;
|
||||
private ?string $userId;
|
||||
private ?string $replyingTo;
|
||||
private string $body;
|
||||
private int $created;
|
||||
private ?int $pinned;
|
||||
private ?int $updated;
|
||||
private ?int $deleted;
|
||||
private int $replies;
|
||||
private int $votesTotal;
|
||||
private int $votesPositive;
|
||||
private int $votesNegative;
|
||||
|
||||
public function __construct(
|
||||
IDbResult $result,
|
||||
private string $id,
|
||||
private string $categoryId,
|
||||
private ?string $userId,
|
||||
private ?string $replyingTo,
|
||||
private string $body,
|
||||
private int $created,
|
||||
private ?int $pinned,
|
||||
private ?int $updated,
|
||||
private ?int $deleted,
|
||||
private int $replies,
|
||||
private int $votesTotal,
|
||||
private int $votesPositive,
|
||||
private int $votesNegative,
|
||||
) {}
|
||||
|
||||
public static function fromResult(
|
||||
DbResult $result,
|
||||
bool $includeRepliesCount = false,
|
||||
bool $includeVotesCount = false
|
||||
) {
|
||||
$args = 0;
|
||||
$this->id = (string)$result->getInteger($args);
|
||||
$this->categoryId = (string)$result->getInteger(++$args);
|
||||
$this->userId = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
|
||||
$this->replyingTo = $result->isNull(++$args) ? null : (string)$result->getInteger($args);
|
||||
$this->body = $result->getString(++$args);
|
||||
$this->created = $result->getInteger(++$args);
|
||||
$this->pinned = $result->isNull(++$args) ? null : $result->getInteger($args);
|
||||
$this->updated = $result->isNull(++$args) ? null : $result->getInteger($args);
|
||||
$this->deleted = $result->isNull(++$args) ? null : $result->getInteger($args);
|
||||
): CommentsPostInfo {
|
||||
$args = [];
|
||||
$count = 0;
|
||||
|
||||
$this->replies = $includeRepliesCount ? $result->getInteger(++$args) : 0;
|
||||
$args[] = $result->getString($count); // id
|
||||
$args[] = $result->getString(++$count); // categoryId
|
||||
$args[] = $result->getStringOrNull(++$count); // userId
|
||||
$args[] = $result->getStringOrNull(++$count); // replyingTo
|
||||
$args[] = $result->getString(++$count); // body
|
||||
$args[] = $result->getInteger(++$count); // created
|
||||
$args[] = $result->getIntegerOrNull(++$count); // pinned
|
||||
$args[] = $result->getIntegerOrNull(++$count); // updated
|
||||
$args[] = $result->getIntegerOrNull(++$count); // deleted
|
||||
|
||||
$args[] = $includeRepliesCount ? $result->getInteger(++$count) : 0;
|
||||
|
||||
if($includeVotesCount) {
|
||||
$this->votesTotal = $result->getInteger(++$args);
|
||||
$this->votesPositive = $result->getInteger(++$args);
|
||||
$this->votesNegative = $result->getInteger(++$args);
|
||||
$args[] = $result->getInteger(++$count); // votesTotal
|
||||
$args[] = $result->getInteger(++$count); // votesPositive
|
||||
$args[] = $result->getInteger(++$count); // votesNegative
|
||||
} else {
|
||||
$this->votesTotal = 0;
|
||||
$this->votesPositive = 0;
|
||||
$this->votesNegative = 0;
|
||||
$args[] = 0;
|
||||
$args[] = 0;
|
||||
$args[] = 0;
|
||||
}
|
||||
|
||||
return new CommentsPostInfo(...$args);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
@ -80,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 {
|
||||
|
@ -100,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 {
|
||||
|
@ -112,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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
<?php
|
||||
namespace Misuzu\Counters;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class CounterInfo {
|
||||
private string $name;
|
||||
private int $value;
|
||||
private int $updated;
|
||||
public function __construct(
|
||||
private string $name,
|
||||
private int $value,
|
||||
private int $updated,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->name = $result->getString(0);
|
||||
$this->value = $result->getInteger(1);
|
||||
$this->updated = $result->getInteger(2);
|
||||
public static function fromResult(DbResult $result): CounterInfo {
|
||||
return new CounterInfo(
|
||||
name: $result->getString(0),
|
||||
value: $result->getInteger(1),
|
||||
updated: $result->getInteger(2),
|
||||
);
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
|
@ -27,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -25,7 +23,7 @@ class Counters {
|
|||
public function getCounters(
|
||||
?string $orderBy = null,
|
||||
?Pagination $pagination = null
|
||||
): array {
|
||||
): iterable {
|
||||
$hasOrderBy = $orderBy !== null;
|
||||
$hasPagination = $pagination !== null;
|
||||
|
||||
|
@ -47,13 +45,7 @@ class Counters {
|
|||
}
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$counters = [];
|
||||
|
||||
while($result->next())
|
||||
$counters[] = new CounterInfo($result);
|
||||
|
||||
return $counters;
|
||||
return $stmt->getResult()->getIterator(CounterInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function get(array|string $names): array|int {
|
||||
|
|
|
@ -2,19 +2,23 @@
|
|||
namespace Misuzu\Emoticons;
|
||||
|
||||
use Stringable;
|
||||
use Index\Data\IDbResult;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class EmoteInfo implements Stringable {
|
||||
private string $id;
|
||||
private int $order;
|
||||
private int $rank;
|
||||
private string $url;
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private int $order,
|
||||
private int $rank,
|
||||
private string $url,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->order = $result->getInteger(1);
|
||||
$this->rank = $result->getInteger(2);
|
||||
$this->url = $result->getString(3);
|
||||
public static function fromResult(DbResult $result): EmoteInfo {
|
||||
return new EmoteInfo(
|
||||
id: $result->getString(0),
|
||||
order: $result->getInteger(1),
|
||||
rank: $result->getInteger(2),
|
||||
url: $result->getString(3),
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
|
|
@ -2,17 +2,21 @@
|
|||
namespace Misuzu\Emoticons;
|
||||
|
||||
use Stringable;
|
||||
use Index\Data\IDbResult;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class EmoteStringInfo implements Stringable {
|
||||
private string $emoteId;
|
||||
private int $order;
|
||||
private string $string;
|
||||
public function __construct(
|
||||
private string $emoteId,
|
||||
private int $order,
|
||||
private string $string,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->emoteId = (string)$result->getInteger(0);
|
||||
$this->order = $result->getInteger(1);
|
||||
$this->string = $result->getString(2);
|
||||
public static function fromResult(DbResult $result): EmoteStringInfo {
|
||||
return new EmoteStringInfo(
|
||||
emoteId: $result->getString(0),
|
||||
order: $result->getInteger(1),
|
||||
string: $result->getString(2),
|
||||
);
|
||||
}
|
||||
|
||||
public function getEmoteId(): string {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -31,7 +30,7 @@ class Emotes {
|
|||
if(!$result->next())
|
||||
throw new RuntimeException('No emoticon with that ID exists.');
|
||||
|
||||
return new EmoteInfo($result);
|
||||
return EmoteInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public static function emoteOrderOptions(): array {
|
||||
|
@ -43,7 +42,7 @@ class Emotes {
|
|||
?int $minRank = null,
|
||||
?string $orderBy = null,
|
||||
?bool $reverse = null
|
||||
): array {
|
||||
): iterable {
|
||||
$hasMinRank = $minRank !== null;
|
||||
$hasOrderBy = $orderBy !== null;
|
||||
$hasReverse = $reverse !== null;
|
||||
|
@ -65,13 +64,7 @@ class Emotes {
|
|||
$stmt->addParameter(1, $minRank);
|
||||
$stmt->execute();
|
||||
|
||||
$emotes = [];
|
||||
$result = $stmt->getResult();
|
||||
|
||||
while($result->next())
|
||||
$emotes[] = new EmoteInfo($result);
|
||||
|
||||
return $emotes;
|
||||
return $stmt->getResult()->getIterator(EmoteInfo::fromResult(...));
|
||||
}
|
||||
|
||||
private static function checkEmoteUrlInternal(string $url): string {
|
||||
|
@ -156,7 +149,7 @@ class Emotes {
|
|||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function getEmoteStrings(EmoteInfo|string $infoOrId): array {
|
||||
public function getEmoteStrings(EmoteInfo|string $infoOrId): iterable {
|
||||
if($infoOrId instanceof EmoteInfo)
|
||||
$infoOrId = $infoOrId->getId();
|
||||
|
||||
|
@ -164,13 +157,7 @@ class Emotes {
|
|||
$stmt->addParameter(1, $infoOrId);
|
||||
$stmt->execute();
|
||||
|
||||
$strings = [];
|
||||
$result = $stmt->getResult();
|
||||
|
||||
while($result->next())
|
||||
$strings[] = new EmoteStringInfo($result);
|
||||
|
||||
return $strings;
|
||||
return $stmt->getResult()->getIterator(EmoteStringInfo::fromResult(...));
|
||||
}
|
||||
|
||||
private static function checkEmoteStringInternal(string $string): string {
|
||||
|
|
40
src/Emoticons/EmotesRpcHandler.php
Normal file
40
src/Emoticons/EmotesRpcHandler.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
namespace Misuzu\Emoticons;
|
||||
|
||||
use Index\XArray;
|
||||
use RPCii\Server\{RpcHandler,RpcHandlerCommon,RpcQuery};
|
||||
|
||||
final class EmotesRpcHandler implements RpcHandler {
|
||||
use RpcHandlerCommon;
|
||||
|
||||
public function __construct(
|
||||
private Emotes $emotes
|
||||
) {}
|
||||
|
||||
#[RpcQuery('misuzu:emotes:all')]
|
||||
public function queryAll(bool $includeId = false, bool $includeOrder = false): array {
|
||||
return XArray::select(
|
||||
$this->emotes->getEmotes(orderBy: 'order'),
|
||||
function($emote) use ($includeId, $includeOrder) {
|
||||
$info = [
|
||||
'url' => $emote->getUrl(),
|
||||
'strings' => XArray::select(
|
||||
$this->emotes->getEmoteStrings($emote),
|
||||
fn($string) => $string->getString()
|
||||
),
|
||||
];
|
||||
|
||||
if($includeId)
|
||||
$info['id'] = $emote->getId();
|
||||
if($includeOrder)
|
||||
$info['order'] = $emote->getOrder();
|
||||
|
||||
$rank = $emote->getMinRank();
|
||||
if($rank != 0)
|
||||
$info['min_rank'] = $rank;
|
||||
|
||||
return $info;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu\Feeds;
|
||||
|
||||
abstract class FeedSerializer {
|
||||
abstract public function serializeFeed(Feed $feed): string;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -5,26 +5,25 @@ 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 IDbConnection $dbConn;
|
||||
private DbStatementCache $cache;
|
||||
|
||||
public function __construct(IDbConnection $dbConn) {
|
||||
$this->dbConn = $dbConn;
|
||||
public function __construct(DbConnection $dbConn) {
|
||||
$this->cache = new DbStatementCache($dbConn);
|
||||
}
|
||||
|
||||
public static function convertCategoryListToTree(
|
||||
array $catInfos,
|
||||
iterable $catInfos,
|
||||
ForumCategoryInfo|string|null $parentInfo = null,
|
||||
?Colour $colour = null
|
||||
): array {
|
||||
if(!is_array($catInfos))
|
||||
$catInfos = iterator_to_array($catInfos);
|
||||
|
||||
$colour ??= Colour::none();
|
||||
$tree = [];
|
||||
$predicate = $parentInfo
|
||||
|
@ -103,7 +102,7 @@ class ForumCategories {
|
|||
?bool $hidden = null,
|
||||
bool $asTree = false,
|
||||
?Pagination $pagination = null
|
||||
): array {
|
||||
): iterable {
|
||||
$isRootParent = false;
|
||||
$hasParentInfo = $parentInfo !== false;
|
||||
$hasType = $type !== null;
|
||||
|
@ -155,11 +154,7 @@ class ForumCategories {
|
|||
}
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$cats = [];
|
||||
|
||||
while($result->next())
|
||||
$cats[] = new ForumCategoryInfo($result);
|
||||
$cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
|
||||
|
||||
if($asTree)
|
||||
$cats = self::convertCategoryListToTree($cats);
|
||||
|
@ -216,7 +211,7 @@ class ForumCategories {
|
|||
if(!$result->next())
|
||||
throw new RuntimeException('Forum category info not found.');
|
||||
|
||||
return new ForumCategoryInfo($result);
|
||||
return ForumCategoryInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public function updateCategory(
|
||||
|
@ -261,7 +256,7 @@ class ForumCategories {
|
|||
|
||||
public function getCategoryAncestry(
|
||||
ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
|
||||
): array {
|
||||
): iterable {
|
||||
if($categoryInfo instanceof ForumCategoryInfo)
|
||||
$categoryInfo = $categoryInfo->getId();
|
||||
elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo)
|
||||
|
@ -277,13 +272,7 @@ class ForumCategories {
|
|||
$stmt->addParameter(1, $categoryInfo);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$cats = [];
|
||||
|
||||
while($result->next())
|
||||
$cats[] = new ForumCategoryInfo($result);
|
||||
|
||||
return $cats;
|
||||
return $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function getCategoryChildren(
|
||||
|
@ -291,7 +280,7 @@ class ForumCategories {
|
|||
bool $includeSelf = false,
|
||||
?bool $hidden = null,
|
||||
bool $asTree = false
|
||||
): array {
|
||||
): iterable {
|
||||
if($parentInfo instanceof ForumCategoryInfo)
|
||||
$parentInfo = $parentInfo->getId();
|
||||
|
||||
|
@ -319,11 +308,7 @@ class ForumCategories {
|
|||
$stmt->addParameter(++$args, $parentInfo);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$cats = [];
|
||||
|
||||
while($result->next())
|
||||
$cats[] = new ForumCategoryInfo($result);
|
||||
$cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...));
|
||||
|
||||
if($asTree)
|
||||
$cats = self::convertCategoryListToTree($cats, $parentInfo);
|
||||
|
|
|
@ -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?
|
||||
|
@ -26,38 +26,42 @@ class ForumCategoryInfo {
|
|||
self::TYPE_DISCUSSION,
|
||||
];
|
||||
|
||||
private string $id;
|
||||
private int $order;
|
||||
private ?string $parentId;
|
||||
private string $name;
|
||||
private int $type;
|
||||
private ?string $desc;
|
||||
private ?string $icon;
|
||||
private ?int $colour;
|
||||
private ?string $link;
|
||||
private ?int $clicks;
|
||||
private int $created;
|
||||
private bool $archived;
|
||||
private bool $hidden;
|
||||
private int $topicsCount;
|
||||
private int $postsCount;
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private int $order,
|
||||
private ?string $parentId,
|
||||
private string $name,
|
||||
private int $type,
|
||||
private ?string $desc,
|
||||
private ?string $icon,
|
||||
private ?int $colour,
|
||||
private ?string $link,
|
||||
private ?int $clicks,
|
||||
private int $created,
|
||||
private bool $archived,
|
||||
private bool $hidden,
|
||||
private int $topicsCount,
|
||||
private int $postsCount,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->order = $result->getInteger(1);
|
||||
$this->parentId = $result->isNull(2) ? null : (string)$result->getInteger(2);
|
||||
$this->name = $result->getString(3);
|
||||
$this->type = $result->getInteger(4);
|
||||
$this->desc = $result->isNull(5) ? null : $result->getString(5);
|
||||
$this->icon = $result->isNull(6) ? null : $result->getString(6);
|
||||
$this->colour = $result->isNull(7) ? null : $result->getInteger(7);
|
||||
$this->link = $result->isNull(8) ? null : $result->getString(8);
|
||||
$this->clicks = $result->isNull(9) ? null : $result->getInteger(9);
|
||||
$this->created = $result->getInteger(10);
|
||||
$this->archived = $result->getInteger(11) !== 0;
|
||||
$this->hidden = $result->getInteger(12) !== 0;
|
||||
$this->topicsCount = $result->getInteger(13);
|
||||
$this->postsCount = $result->getInteger(14);
|
||||
public static function fromResult(DbResult $result): ForumCategoryInfo {
|
||||
return new ForumCategoryInfo(
|
||||
id: $result->getString(0),
|
||||
order: $result->getInteger(1),
|
||||
parentId: $result->getStringOrNull(2),
|
||||
name: $result->getString(3),
|
||||
type: $result->getInteger(4),
|
||||
desc: $result->getStringOrNull(5),
|
||||
icon: $result->getStringOrNull(6),
|
||||
colour: $result->getIntegerOrNull(7),
|
||||
link: $result->getStringOrNull(8),
|
||||
clicks: $result->getIntegerOrNull(9),
|
||||
created: $result->getInteger(10),
|
||||
archived: $result->getBoolean(11),
|
||||
hidden: $result->getBoolean(12),
|
||||
topicsCount: $result->getInteger(13),
|
||||
postsCount: $result->getInteger(14),
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
@ -172,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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,36 +1,40 @@
|
|||
<?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 {
|
||||
private string $id;
|
||||
private string $topicId;
|
||||
private string $categoryId;
|
||||
private ?string $userId;
|
||||
private string $remoteAddr;
|
||||
private string $body;
|
||||
private int $parser;
|
||||
private bool $displaySignature;
|
||||
private int $created;
|
||||
private ?int $edited;
|
||||
private ?int $deleted;
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $topicId,
|
||||
private string $categoryId,
|
||||
private ?string $userId,
|
||||
private string $remoteAddr,
|
||||
private string $body,
|
||||
private int $parser,
|
||||
private bool $displaySignature,
|
||||
private int $created,
|
||||
private ?int $edited,
|
||||
private ?int $deleted,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->topicId = (string)$result->getInteger(1);
|
||||
$this->categoryId = (string)$result->getInteger(2);
|
||||
$this->userId = $result->isNull(3) ? null : (string)$result->getInteger(3);
|
||||
$this->remoteAddr = $result->getString(4);
|
||||
$this->body = $result->getString(5);
|
||||
$this->parser = $result->getInteger(6);
|
||||
$this->displaySignature = $result->getInteger(7) !== 0;
|
||||
$this->created = $result->getInteger(8);
|
||||
$this->edited = $result->isNull(9) ? null : $result->getInteger(9);
|
||||
$this->deleted = $result->isNull(10) ? null : $result->getInteger(10);
|
||||
public static function fromResult(DbResult $result): ForumPostInfo {
|
||||
return new ForumPostInfo(
|
||||
id: $result->getString(0),
|
||||
topicId: $result->getString(1),
|
||||
categoryId: $result->getString(2),
|
||||
userId: $result->getStringOrNull(3),
|
||||
remoteAddr: $result->getString(4),
|
||||
body: $result->getString(5),
|
||||
parser: $result->getInteger(6),
|
||||
displaySignature: $result->getBoolean(7),
|
||||
created: $result->getInteger(8),
|
||||
edited: $result->getIntegerOrNull(9),
|
||||
deleted: $result->getIntegerOrNull(10),
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
@ -53,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;
|
||||
}
|
||||
|
@ -89,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 {
|
||||
|
@ -109,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 {
|
||||
|
@ -129,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -84,7 +81,7 @@ class ForumPosts {
|
|||
?array $searchQuery = null,
|
||||
?bool $deleted = null,
|
||||
?Pagination $pagination = null
|
||||
): array {
|
||||
): iterable {
|
||||
// remove this hack when search server
|
||||
$hasSearchQuery = $searchQuery !== null;
|
||||
$doSearchOrder = false;
|
||||
|
@ -189,13 +186,7 @@ class ForumPosts {
|
|||
}
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$posts = [];
|
||||
|
||||
while($result->next())
|
||||
$posts[] = new ForumPostInfo($result);
|
||||
|
||||
return $posts;
|
||||
return $stmt->getResult()->getIterator(ForumPostInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function getPost(
|
||||
|
@ -267,13 +258,13 @@ class ForumPosts {
|
|||
if(!$result->next())
|
||||
throw new RuntimeException('Forum post not found.');
|
||||
|
||||
return new ForumPostInfo($result);
|
||||
return ForumPostInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public function createPost(
|
||||
ForumTopicInfo|string $topicInfo,
|
||||
UserInfo|string|null $userInfo,
|
||||
IPAddress|string $remoteAddr,
|
||||
string $remoteAddr,
|
||||
string $body,
|
||||
int $bodyParser,
|
||||
bool $displaySignature,
|
||||
|
@ -291,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);
|
||||
|
@ -309,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,
|
||||
|
@ -322,9 +310,6 @@ class ForumPosts {
|
|||
$values = [];
|
||||
|
||||
if($remoteAddr !== null) {
|
||||
if($remoteAddr instanceof IPAddress)
|
||||
$remoteAddr = (string)$remoteAddr;
|
||||
|
||||
$fields[] = 'post_ip = INET6_ATON(?)';
|
||||
$values[] = $remoteAddr;
|
||||
}
|
||||
|
@ -401,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(
|
||||
|
|
|
@ -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;
|
||||
|
@ -17,32 +18,36 @@ class ForumTopicInfo {
|
|||
'global' => self::TYPE_GLOBAL,
|
||||
];
|
||||
|
||||
private string $id;
|
||||
private string $categoryId;
|
||||
private ?string $userId;
|
||||
private int $type;
|
||||
private string $title;
|
||||
private int $postsCount;
|
||||
private int $deletedPostsCount;
|
||||
private int $viewsCount;
|
||||
private int $created;
|
||||
private int $bumped;
|
||||
private ?int $deleted;
|
||||
private ?int $locked;
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $categoryId,
|
||||
private ?string $userId,
|
||||
private int $type,
|
||||
private string $title,
|
||||
private int $postsCount,
|
||||
private int $deletedPostsCount,
|
||||
private int $viewsCount,
|
||||
private int $created,
|
||||
private int $bumped,
|
||||
private ?int $deleted,
|
||||
private ?int $locked,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->categoryId = (string)$result->getInteger(1);
|
||||
$this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2);
|
||||
$this->type = $result->getInteger(3);
|
||||
$this->title = $result->getString(4);
|
||||
$this->viewsCount = $result->getInteger(5);
|
||||
$this->created = $result->getInteger(6);
|
||||
$this->bumped = $result->getInteger(7);
|
||||
$this->deleted = $result->isNull(8) ? null : $result->getInteger(8);
|
||||
$this->locked = $result->isNull(9) ? null : $result->getInteger(9);
|
||||
$this->postsCount = $result->getInteger(10);
|
||||
$this->deletedPostsCount = $result->getInteger(11);
|
||||
public static function fromResult(DbResult $result): ForumTopicInfo {
|
||||
return new ForumTopicInfo(
|
||||
id: $result->getString(0),
|
||||
categoryId: $result->getString(1),
|
||||
userId: $result->getStringOrNull(2),
|
||||
type: $result->getInteger(3),
|
||||
title: $result->getString(4),
|
||||
viewsCount: $result->getInteger(5),
|
||||
created: $result->getInteger(6),
|
||||
bumped: $result->getInteger(7),
|
||||
deleted: $result->getIntegerOrNull(8),
|
||||
locked: $result->getIntegerOrNull(9),
|
||||
postsCount: $result->getInteger(10),
|
||||
deletedPostsCount: $result->getInteger(11),
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
@ -132,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 {
|
||||
|
@ -161,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 {
|
||||
|
@ -173,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
<?php
|
||||
namespace Misuzu\Forum;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class ForumTopicRedirectInfo {
|
||||
private string $topicId;
|
||||
private ?string $userId;
|
||||
private string $link;
|
||||
private int $created;
|
||||
public function __construct(
|
||||
private string $topicId,
|
||||
private ?string $userId,
|
||||
private string $link,
|
||||
private int $created,
|
||||
) {}
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->topicId = (string)$result->getInteger(0);
|
||||
$this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1);
|
||||
$this->link = $result->getString(2);
|
||||
$this->created = $result->getInteger(3);
|
||||
public static function fromResult(DbResult $result): ForumTopicRedirectInfo {
|
||||
return new ForumTopicRedirectInfo(
|
||||
topicId: $result->getString(0),
|
||||
userId: $result->getStringOrNull(1),
|
||||
link: $result->getString(2),
|
||||
created: $result->getInteger(3),
|
||||
);
|
||||
}
|
||||
|
||||
public function getTopicId(): string {
|
||||
|
@ -37,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);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue