Moved profile about sections into their own table.

This commit is contained in:
flash 2025-02-08 23:20:53 +00:00
parent 8bb2400d3f
commit 372797c564
11 changed files with 218 additions and 108 deletions

View file

@ -0,0 +1,36 @@
<?php
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class MovedProfileAboutToDedicatedTable_20250208_225249 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute(<<<SQL
CREATE TABLE msz_profile_about (
user_id INT UNSIGNED NOT NULL,
about_body TEXT NOT NULL COLLATE 'utf8mb4_unicode_520_ci',
about_body_format ENUM('','bb','md') NOT NULL COLLATE 'ascii_general_ci',
about_created TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id),
CONSTRAINT profile_about_users_foreign
FOREIGN KEY (user_id)
REFERENCES msz_users (user_id)
ON UPDATE CASCADE
ON DELETE CASCADE
) COLLATE='utf8mb4_bin';
SQL);
$conn->execute(<<<SQL
INSERT INTO msz_profile_about
SELECT user_id, user_about_content, user_about_content_format, NOW()
FROM msz_users
WHERE user_about_content IS NOT NULL
AND TRIM(user_about_content) <> ''
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_users
DROP COLUMN user_about_content,
DROP COLUMN user_about_content_format;
SQL);
}
}

View file

@ -7,8 +7,8 @@ use RuntimeException;
use Index\ByteFormat;
use Misuzu\Forum\ForumSignaturesData;
use Misuzu\Parsers\TextFormat;
use Misuzu\Profile\ProfileBackgroundAttach;
use Misuzu\Users\{User,UsersContext};
use Misuzu\Profile\{ProfileAboutData,ProfileBackgroundAttach};
use Misuzu\Users\{User,UserBirthdatesData};
use Misuzu\Users\Assets\UserAvatarAsset;
use Misuzu\Users\Assets\UserBackgroundAsset;
@ -80,6 +80,7 @@ $avatarAsset = new UserAvatarAsset($userInfo);
$backgroundInfo = $msz->profileCtx->backgrounds->getProfileBackground($userInfo);
$backgroundAsset = new UserBackgroundAsset($userInfo, $backgroundInfo);
$aboutInfo = $msz->profileCtx->about->getProfileAbout($userInfo);
$sigInfo = $msz->forumCtx->signatures->getSignature($userInfo);
if($isEditing) {
@ -140,24 +141,28 @@ if($isEditing) {
}
}
if(!empty($_POST['about']) && is_array($_POST['about'])) {
if(filter_has_var(INPUT_POST, 'about_body')) {
if(!$perms->edit_about) {
$notices[] = 'You\'re not allowed to edit your about page.';
$notices[] = "You're not allowed to edit your about page.";
} else {
$aboutText = (string)($_POST['about']['text'] ?? '');
$aboutParse = TextFormat::tryFrom((string)($_POST['about']['parser'] ?? '')) ?? TextFormat::Plain;
$aboutValid = $msz->usersCtx->users->validateProfileAbout($aboutParse, $aboutText);
if($aboutValid === '')
$msz->usersCtx->users->updateUser($userInfo, aboutBody: $aboutText, aboutBodyFormat: $aboutParse);
else
$notices[] = $msz->usersCtx->users->validateProfileAboutText($aboutValid);
$aboutBody = (string)filter_input(INPUT_POST, 'about_body');
if(trim($aboutBody) === '') {
$msz->profileCtx->about->deleteProfileAbout($userInfo);
$aboutInfo = null;
} else {
$aboutFormat = TextFormat::tryFrom(filter_input(INPUT_POST, 'about_format'));
$aboutValid = ProfileAboutData::validateProfileAbout($aboutFormat, $aboutBody);
if($aboutValid === '')
$aboutInfo = $msz->profileCtx->about->updateProfileAbout($userInfo, $aboutBody, $aboutFormat);
else
$notices[] = ProfileAboutData::validateProfileAboutText($aboutValid);
}
}
}
if(filter_has_var(INPUT_POST, 'sig_body')) {
if(!$perms->edit_signature) {
$notices[] = 'You\'re not allowed to edit your forum signature.';
$notices[] = "You're not allowed to edit your forum signature.";
} else {
$sigBody = (string)filter_input(INPUT_POST, 'sig_body');
if(trim($sigBody) === '') {
@ -181,7 +186,7 @@ if($isEditing) {
$birthYear = (int)($_POST['birthdate']['year'] ?? 0);
$birthMonth = (int)($_POST['birthdate']['month'] ?? 0);
$birthDay = (int)($_POST['birthdate']['day'] ?? 0);
$birthValid = UsersContext::validateBirthdate($birthYear, $birthMonth, $birthDay);
$birthValid = UserBirthdatesData::validateBirthdate($birthYear, $birthMonth, $birthDay);
if($birthValid === '') {
if($birthMonth === 0 && $birthDay === 0)
@ -189,7 +194,7 @@ if($isEditing) {
else
$msz->usersCtx->birthdates->updateUserBirthdate($userInfo, $birthYear === 0 ? null : $birthYear, $birthMonth, $birthDay);
} else
$notices[] = $msz->usersCtx->users->validateBirthdateText($birthValid);
$notices[] = UserBirthdatesData::validateBirthdateText($birthValid);
}
}
@ -402,5 +407,6 @@ Template::render('profile.index', [
'profile_background_asset' => $backgroundAsset,
'profile_can_send_messages' => $viewerPermsGlobal->check(Perm::G_MESSAGES_SEND),
'profile_age' => $msz->usersCtx->birthdates->getUserAge($userInfo),
'profile_about_info' => $aboutInfo,
'profile_forum_signature_info' => $sigInfo,
]);

View file

@ -147,10 +147,11 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
$tmpFiles[] = db_to_zip($archive, $userInfo, 'oauth2_refresh', ['ref_id:s', 'app_id:s', 'user_id:s:n', 'acc_id:s:n', 'ref_token:n', 'ref_scope:s', 'ref_created:t', 'ref_expires:t']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms', ['user_id:s:n', 'role_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_allow:i', 'perms_deny:i']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'perms_calculated', ['user_id:s:n', 'forum_id:s:n', 'perms_category:s', 'perms_calculated:i']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_about', ['user_id:s', 'about_body:s', 'about_body_format:s', 'about_created:t']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_backgrounds', ['user_id:s', 'bg_attach:s', 'bg_blend:i', 'bg_slide:i']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_fields_values', ['field_id:s', 'user_id:s', 'format_id:s', 'field_value:s']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'sessions', ['session_id:s', 'user_id:s', 'session_key:n', 'session_remote_addr_first:a', 'session_remote_addr_last:a:n', 'session_user_agent:s', 'session_country:s', 'session_expires:t', 'session_expires_bump:b', 'session_created:t', 'session_active:t:n']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'users', ['user_id:s', 'user_name:s', 'user_password:n', 'user_email:s', 'user_remote_addr_first:a', 'user_remote_addr_last:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n', 'user_display_role_id:s:n', 'user_about_content:s:n', 'user_about_content_format:s', 'user_title:s:n']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'users', ['user_id:s', 'user_name:s', 'user_password:n', 'user_email:s', 'user_remote_addr_first:a', 'user_remote_addr_last:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n', 'user_display_role_id:s:n', 'user_title:s:n']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'users_bans', ['ban_id:s', 'user_id:s', 'mod_id:n', 'ban_severity:i', 'ban_reason_public:s', 'ban_reason_private:s', 'ban_created:t', 'ban_expires:t:n']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'users_birthdates', ['user_id:s', 'birth_year:i:n', 'birth_month:i', 'birth_day:i']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'users_password_resets', ['reset_id:s', 'user_id:s', 'reset_remote_addr:a', 'reset_requested:t', 'reset_code:n']);

View file

@ -0,0 +1,84 @@
<?php
namespace Misuzu\Profile;
use RuntimeException;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\Parsers\TextFormat;
use Misuzu\Users\UserInfo;
class ProfileAboutData {
public const int BODY_MAX_LENGTH = 50000;
private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
public function getProfileAbout(UserInfo|string $userInfo): ?ProfileAboutInfo {
$stmt = $this->cache->get(<<<SQL
SELECT user_id, about_body, about_body_format, UNIX_TIMESTAMP(about_created)
FROM msz_profile_about
WHERE user_id = ?
SQL);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? ProfileAboutInfo::fromResult($result) : null;
}
public function deleteProfileAbout(UserInfo|string $userInfo): void {
$stmt = $this->cache->get(<<<SQL
DELETE FROM msz_profile_about
WHERE user_id = ?
SQL);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
}
public function updateProfileAbout(
UserInfo|string $userInfo,
string $body,
TextFormat|string $format
): ProfileAboutInfo {
if(is_string($format))
$format = TextFormat::from($format);
$stmt = $this->cache->get(<<<SQL
REPLACE INTO msz_profile_about (
user_id, about_body, about_body_format
) VALUES (?, ?, ?)
SQL);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($body);
$stmt->nextParameter($format->value);
$stmt->execute();
$aboutInfo = $this->getProfileAbout($userInfo);
if($aboutInfo === null)
throw new RuntimeException('failed to update about section');
return $aboutInfo;
}
public static function validateProfileAbout(?TextFormat $format, string $text): string {
if($format === null)
return 'parser';
$length = mb_strlen($text);
if($length > self::BODY_MAX_LENGTH)
return 'long';
return '';
}
public static function validateProfileAboutText(string $error): string {
return match($error) {
'parser' => 'You attempted to select an invalid parser for your profile about section.',
'long' => sprintf('Please keep the length of your profile about section below %d characters.', self::BODY_MAX_LENGTH),
'' => 'Your profile about section is fine, why are you seeing this?',
default => 'Your profile about section is not acceptable.',
};
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Misuzu\Profile;
use Carbon\CarbonImmutable;
use Index\Db\DbResult;
use Misuzu\Parsers\TextFormat;
class ProfileAboutInfo {
public function __construct(
public private(set) string $userId,
public private(set) string $body,
public private(set) TextFormat $bodyFormat,
public private(set) int $createdTime,
) {}
public static function fromResult(DbResult $result): self {
return new ProfileAboutInfo(
userId: $result->getString(0),
body: $result->getString(1),
bodyFormat: TextFormat::tryFrom($result->getString(2)) ?? TextFormat::Plain,
createdTime: $result->getInteger(3),
);
}
public CarbonImmutable $createdAt {
get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
}
public bool $isBodyPlain {
get => $this->bodyFormat === TextFormat::Plain;
}
public bool $isBodyBBCode {
get => $this->bodyFormat === TextFormat::BBCode;
}
public bool $isBodyMarkdown {
get => $this->bodyFormat === TextFormat::Markdown;
}
}

View file

@ -4,10 +4,12 @@ namespace Misuzu\Profile;
use Index\Db\DbConnection;
class ProfileContext {
public private(set) ProfileAboutData $about;
public private(set) ProfileBackgroundsData $backgrounds;
public private(set) ProfileFieldsData $fields;
public function __construct(DbConnection $dbConn) {
$this->about = new ProfileAboutData($dbConn);
$this->backgrounds = new ProfileBackgroundsData($dbConn);
$this->fields = new ProfileFieldsData($dbConn);
}

View file

@ -4,6 +4,7 @@ namespace Misuzu\Users;
use DateTimeInterface;
use RuntimeException;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\Tools;
class UserBirthdatesData {
private DbStatementCache $cache;
@ -65,4 +66,28 @@ class UserBirthdatesData {
$result = $stmt->getResult();
return $result->next() ? $result->getIntegerOrNull(0) : null;
}
public static function validateBirthdate(?int $year, int $month, int $day, int $yearRange = 100): string {
$year ??= 0;
if($day !== 0 && $month !== 0) {
$currentYear = (int)date('Y');
if($year > 0 && ($year < $currentYear - $yearRange || $year > $currentYear))
return 'year';
if(!Tools::isValidDate($year, $month, $day))
return 'date';
}
return '';
}
public static function validateBirthdateText(string $error): string {
return match($error) {
'year' => 'The year in your birthdate is too ridiculous.',
'date' => 'The birthdate you attempted to set is not a valid date.',
'' => 'Your birthdate is fine, why are you seeing this?',
default => 'Your birthdate is not acceptable.',
};
}
}

View file

@ -4,7 +4,6 @@ namespace Misuzu\Users;
use Carbon\CarbonImmutable;
use Index\Colour\Colour;
use Index\Db\DbResult;
use Misuzu\Parsers\TextFormat;
class UserInfo {
public function __construct(
@ -21,8 +20,6 @@ class UserInfo {
public private(set) ?int $lastActiveTime,
public private(set) ?int $deletedTime,
public private(set) ?string $displayRoleId,
public private(set) ?string $aboutBody,
public private(set) TextFormat $aboutBodyFormat,
public private(set) ?string $title,
) {}
@ -41,9 +38,7 @@ class UserInfo {
lastActiveTime: $result->getIntegerOrNull(10),
deletedTime: $result->getIntegerOrNull(11),
displayRoleId: $result->getStringOrNull(12),
aboutBody: $result->getStringOrNull(13),
aboutBodyFormat: TextFormat::tryFrom($result->getString(14)) ?? TextFormat::Plain,
title: $result->getString(15),
title: $result->getString(13),
);
}
@ -82,16 +77,4 @@ class UserInfo {
public ?CarbonImmutable $deletedAt {
get => $this->deletedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->deletedTime);
}
public bool $isAboutBodyPlain {
get => $this->aboutBodyFormat === TextFormat::Plain;
}
public bool $isAboutBodyBBCode {
get => $this->aboutBodyFormat === TextFormat::BBCode;
}
public bool $isAboutBodyMarkdown {
get => $this->aboutBodyFormat === TextFormat::Markdown;
}
}

View file

@ -3,7 +3,6 @@ namespace Misuzu\Users;
use Index\Colour\Colour;
use Index\Db\DbConnection;
use Misuzu\Tools;
class UsersContext {
public private(set) UsersData $users;
@ -91,19 +90,4 @@ class UsersContext {
): bool {
return $this->tryGetActiveBan($userInfo, $minimumSeverity) !== null;
}
public static function validateBirthdate(?int $year, int $month, int $day, int $yearRange = 100): string {
$year ??= 0;
if($day !== 0 && $month !== 0) {
$currentYear = (int)date('Y');
if($year > 0 && ($year < $currentYear - $yearRange || $year > $currentYear))
return 'year';
if(!Tools::isValidDate($year, $month, $day))
return 'date';
}
return '';
}
}

View file

@ -8,8 +8,6 @@ use Index\XString;
use Index\Colour\Colour;
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\Pagination;
use Misuzu\Tools;
use Misuzu\Parsers\TextFormat;
class UsersData {
private DbStatementCache $cache;
@ -25,8 +23,6 @@ class UsersData {
public const PASSWORD_OPTS = [];
public const PASSWORD_UNIQUE = 6;
public const PROFILE_ABOUT_MAX_LENGTH = 50000;
public static function passwordHash(string $password): string {
return password_hash($password, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
}
@ -175,9 +171,7 @@ class UsersData {
UNIX_TIMESTAMP(u.user_created),
UNIX_TIMESTAMP(u.user_active),
UNIX_TIMESTAMP(u.user_deleted),
u.user_display_role_id,
u.user_about_content, u.user_about_content_format,
u.user_title
u.user_display_role_id, u.user_title
FROM msz_users AS u
SQL;
if($hasRoleInfo)
@ -283,9 +277,7 @@ class UsersData {
UNIX_TIMESTAMP(user_created),
UNIX_TIMESTAMP(user_active),
UNIX_TIMESTAMP(user_deleted),
user_display_role_id,
user_about_content, user_about_content_format,
user_title
user_display_role_id, user_title
FROM msz_users
SQL;
if($selectId) {
@ -354,8 +346,6 @@ class UsersData {
?string $countryCode = null,
?Colour $colour = null,
RoleInfo|string|null $displayRoleInfo = null,
?string $aboutBody = null,
TextFormat|string|null $aboutBodyFormat = null,
?string $title = null
): void {
if($userInfo instanceof UserInfo)
@ -402,18 +392,6 @@ class UsersData {
$values[] = $displayRoleInfo;
}
if($aboutBody !== null && $aboutBodyFormat !== null) {
if(is_string($aboutBodyFormat))
$aboutBodyFormat = TextFormat::tryFrom($aboutBodyFormat) ?? null;
if(self::validateProfileAbout($aboutBodyFormat, $aboutBody) !== '')
throw new InvalidArgumentException('$aboutBody and $aboutBodyFormat contain invalid data!');
$fields[] = 'user_about_content = ?';
$values[] = $aboutBody;
$fields[] = 'user_about_content_format = ?';
$values[] = $aboutBodyFormat->value;
}
if($title !== null) {
$fields[] = 'user_title = ?';
$values[] = $title;
@ -680,33 +658,4 @@ class UsersData {
default => 'Your password is not acceptable.',
};
}
public static function validateBirthdateText(string $error): string {
return match($error) {
'year' => 'The year in your birthdate is too ridiculous.',
'date' => 'The birthdate you attempted to set is not a valid date.',
'' => 'Your birthdate is fine, why are you seeing this?',
default => 'Your birthdate is not acceptable.',
};
}
public static function validateProfileAbout(?TextFormat $format, string $text): string {
if($format === null)
return 'parser';
$length = strlen($text);
if($length > self::PROFILE_ABOUT_MAX_LENGTH)
return 'long';
return '';
}
public static function validateProfileAboutText(string $error): string {
return match($error) {
'parser' => 'You attempted to select an invalid parser for your profile about section.',
'long' => sprintf('Please keep the length of your profile about section below %d characters.', self::PROFILE_ABOUT_MAX_LENGTH),
'' => 'Your profile about section is fine, why are you seeing this?',
default => 'Your profile about section is not acceptable.',
};
}
}

View file

@ -259,18 +259,18 @@
{% if profile_user is defined %}
<div class="profile__content__main">
{% if (not profile_is_banned or profile_can_edit) and ((profile_is_editing and perms.edit_about) or profile_user.aboutBody is not empty) %}
{% if (not profile_is_banned or profile_can_edit) and ((profile_is_editing and perms.edit_about) or profile_about_info is not empty) %}
<div class="container profile__container profile__about" id="about">
{{ container_title('About ' ~ profile_user.name) }}
{% if profile_is_editing %}
<div class="profile__signature__editor">
{{ input_select('about[parser]', parser_options(), profile_user.aboutBodyFormat.value, '', '', false, 'profile__about__select') }}
<textarea name="about[text]" class="input__textarea profile__about__text" id="about-textarea">{{ profile_user.aboutBody }}</textarea>
{{ input_select('about_format', parser_options(), profile_about_info.bodyFormat.value|default('bb'), '', '', false, 'profile__about__select') }}
<textarea name="about_body" class="input__textarea profile__about__text" id="about-textarea">{{ profile_about_info.body|default('') }}</textarea>
</div>
{% else %}
<div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_user.isAboutBodyMarkdown %} markdown{% endif %}">
{{ profile_user.aboutBody|escape|parse_text(profile_user.aboutBodyFormat)|raw }}
<div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_about_info.isBodyMarkdown %} markdown{% endif %}">
{{ profile_about_info.body|escape|parse_text(profile_about_info.bodyFormat)|raw }}
</div>
{% endif %}
</div>