From 372797c5646185ae53cfdb9df5503aa48267693c Mon Sep 17 00:00:00 2001 From: flashwave <me@flash.moe> Date: Sat, 8 Feb 2025 23:20:53 +0000 Subject: [PATCH] Moved profile about sections into their own table. --- ...moved_profile_about_to_dedicated_table.php | 36 ++++++++ public-legacy/profile.php | 36 ++++---- public-legacy/settings/data.php | 3 +- src/Profile/ProfileAboutData.php | 84 +++++++++++++++++++ src/Profile/ProfileAboutInfo.php | 40 +++++++++ src/Profile/ProfileContext.php | 2 + src/Users/UserBirthdatesData.php | 25 ++++++ src/Users/UserInfo.php | 19 +---- src/Users/UsersContext.php | 16 ---- src/Users/UsersData.php | 55 +----------- templates/profile/index.twig | 10 +-- 11 files changed, 218 insertions(+), 108 deletions(-) create mode 100644 database/2025_02_08_225249_moved_profile_about_to_dedicated_table.php create mode 100644 src/Profile/ProfileAboutData.php create mode 100644 src/Profile/ProfileAboutInfo.php diff --git a/database/2025_02_08_225249_moved_profile_about_to_dedicated_table.php b/database/2025_02_08_225249_moved_profile_about_to_dedicated_table.php new file mode 100644 index 00000000..702216fa --- /dev/null +++ b/database/2025_02_08_225249_moved_profile_about_to_dedicated_table.php @@ -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); + } +} diff --git a/public-legacy/profile.php b/public-legacy/profile.php index 84975433..329e6c78 100644 --- a/public-legacy/profile.php +++ b/public-legacy/profile.php @@ -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, ]); diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php index a991f5c8..d8ffc717 100644 --- a/public-legacy/settings/data.php +++ b/public-legacy/settings/data.php @@ -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']); diff --git a/src/Profile/ProfileAboutData.php b/src/Profile/ProfileAboutData.php new file mode 100644 index 00000000..b9ee9a7b --- /dev/null +++ b/src/Profile/ProfileAboutData.php @@ -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.', + }; + } +} diff --git a/src/Profile/ProfileAboutInfo.php b/src/Profile/ProfileAboutInfo.php new file mode 100644 index 00000000..8cc994c4 --- /dev/null +++ b/src/Profile/ProfileAboutInfo.php @@ -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; + } +} diff --git a/src/Profile/ProfileContext.php b/src/Profile/ProfileContext.php index ed0ed3a1..36c54aa8 100644 --- a/src/Profile/ProfileContext.php +++ b/src/Profile/ProfileContext.php @@ -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); } diff --git a/src/Users/UserBirthdatesData.php b/src/Users/UserBirthdatesData.php index a20e20d6..27a54478 100644 --- a/src/Users/UserBirthdatesData.php +++ b/src/Users/UserBirthdatesData.php @@ -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.', + }; + } } diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php index ea95ab57..998b4c69 100644 --- a/src/Users/UserInfo.php +++ b/src/Users/UserInfo.php @@ -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; - } } diff --git a/src/Users/UsersContext.php b/src/Users/UsersContext.php index bd5741e9..69cbfc14 100644 --- a/src/Users/UsersContext.php +++ b/src/Users/UsersContext.php @@ -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 ''; - } } diff --git a/src/Users/UsersData.php b/src/Users/UsersData.php index 649ff7a0..2193a4d3 100644 --- a/src/Users/UsersData.php +++ b/src/Users/UsersData.php @@ -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.', - }; - } } diff --git a/templates/profile/index.twig b/templates/profile/index.twig index a5b0dc9b..800304ff 100644 --- a/templates/profile/index.twig +++ b/templates/profile/index.twig @@ -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>