diff --git a/database/2025_02_08_175705_create_profile_background_settings_table.php b/database/2025_02_08_175705_create_profile_background_settings_table.php new file mode 100644 index 00000000..6e071de4 --- /dev/null +++ b/database/2025_02_08_175705_create_profile_background_settings_table.php @@ -0,0 +1,42 @@ +<?php +use Index\Db\DbConnection; +use Index\Db\Migration\DbMigration; + +final class CreateProfileBackgroundSettingsTable_20250208_175705 implements DbMigration { + public function migrate(DbConnection $conn): void { + $conn->execute(<<<SQL + CREATE TABLE msz_profile_backgrounds ( + user_id INT UNSIGNED NOT NULL AUTO_INCREMENT, + bg_attach ENUM('cover','stretch','tile','contain') NOT NULL, + bg_blend TINYINT UNSIGNED NOT NULL, + bg_slide TINYINT UNSIGNED NOT NULL, + PRIMARY KEY (user_id), + CONSTRAINT profile_backgrounds_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_backgrounds + SELECT user_id, + CASE (user_background_settings & 0x0F) + WHEN 1 THEN 'cover' + WHEN 2 THEN 'stretch' + WHEN 3 THEN 'tile' + WHEN 4 THEN 'contain' + END, + IF(user_background_settings & 0x10, 1, 0), + IF(user_background_settings & 0x20, 1, 0) + FROM msz_users + WHERE (user_background_settings & 0x0F) BETWEEN 1 AND 4; + SQL); + + $conn->execute(<<<SQL + ALTER TABLE msz_users + DROP COLUMN user_background_settings; + SQL); + } +} diff --git a/public-legacy/profile.php b/public-legacy/profile.php index 13a6695d..d81427be 100644 --- a/public-legacy/profile.php +++ b/public-legacy/profile.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use RuntimeException; use Index\ByteFormat; use Misuzu\Parsers\TextFormat; +use Misuzu\Profile\ProfileBackgroundAttach; use Misuzu\Users\{User,UsersContext}; use Misuzu\Users\Assets\UserAvatarAsset; use Misuzu\Users\Assets\UserBackgroundAsset; @@ -74,8 +75,9 @@ $canManageWarnings = $viewerPermsUser->check(Perm::U_WARNINGS_MANAGE); $canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $viewerInfo->super || ( $viewerPermsUser->check(Perm::U_USERS_MANAGE) && ($viewingOwnProfile || $viewerRank > $userRank) )); -$avatarInfo = new UserAvatarAsset($userInfo); -$backgroundInfo = new UserBackgroundAsset($userInfo); +$avatarAsset = new UserAvatarAsset($userInfo); +$backgroundInfo = $msz->profileCtx->backgrounds->getProfileBackground($userInfo); +$backgroundAsset = new UserBackgroundAsset($userInfo, $backgroundInfo); if($isEditing) { if(!$canEdit) @@ -92,7 +94,6 @@ if($isEditing) { Template::set([ 'perms' => $perms, - 'background_attachments' => UserBackgroundAsset::getAttachmentStringOptions(), 'birthdate_info' => $msz->usersCtx->birthdates->getUserBirthdate($userInfo), ]); @@ -106,7 +107,7 @@ if($isEditing) { if(!$perms->edit_profile) { $notices[] = 'You\'re not allowed to edit your profile'; } else { - $profileFieldInfos = iterator_to_array($msz->profileFields->getFields()); + $profileFieldInfos = iterator_to_array($msz->profileCtx->fields->getFields()); $profileFieldsSetInfos = []; $profileFieldsSetValues = []; $profileFieldsRemove = []; @@ -130,9 +131,9 @@ if($isEditing) { } if(!empty($profileFieldsRemove)) - $msz->profileFields->removeFieldValues($userInfo, $profileFieldsRemove); + $msz->profileCtx->fields->removeFieldValues($userInfo, $profileFieldsRemove); if(!empty($profileFieldsSetInfos)) - $msz->profileFields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues); + $msz->profileCtx->fields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues); } } @@ -187,7 +188,7 @@ if($isEditing) { if(!empty($_FILES['avatar'])) { if(!empty($_POST['avatar']['delete'])) { - $avatarInfo->delete(); + $avatarAsset->delete(); } else { if(!$perms->edit_avatar) { $notices[] = 'You aren\'t allow to change your avatar.'; @@ -204,7 +205,7 @@ if($isEditing) { break; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: - $notices[] = sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarInfo->getMaxBytes())); + $notices[] = sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarAsset->getMaxBytes())); break; default: $notices[] = 'Unable to save your avatar, contact an administator!'; @@ -212,14 +213,14 @@ if($isEditing) { } } else { try { - $avatarInfo->setFromPath($_FILES['avatar']['tmp_name']['file']); + $avatarAsset->setFromPath($_FILES['avatar']['tmp_name']['file']); } catch(InvalidArgumentException $ex) { $exMessage = $ex->getMessage(); $notices[] = match($exMessage) { '$path is not a valid image.' => 'The file you uploaded was not an image!', '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!', - 'Dimensions of $path are too large.' => sprintf('Your avatar can\'t be larger than %dx%d!', $avatarInfo->getMaxWidth(), $avatarInfo->getMaxHeight()), - 'File size of $path is too large.' => sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarInfo->getMaxBytes())), + 'Dimensions of $path are too large.' => sprintf('Your avatar can\'t be larger than %dx%d!', $avatarAsset->getMaxWidth(), $avatarAsset->getMaxHeight()), + 'File size of $path is too large.' => sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarAsset->getMaxBytes())), default => $exMessage, }; } catch(RuntimeException $ex) { @@ -230,16 +231,20 @@ if($isEditing) { } } - if(!empty($_FILES['background'])) { - if((int)($_POST['background']['attach'] ?? -1) === 0) { - $backgroundInfo->delete(); + if(filter_has_var(INPUT_POST, 'bg_attach')) { + $bgFormat = ProfileBackgroundAttach::tryFrom((string)filter_input(INPUT_POST, 'bg_attach')); + + if($bgFormat === null) { + $backgroundAsset->delete(); + $msz->profileCtx->backgrounds->deleteProfileBackground($userInfo); + $backgroundAsset = null; } else { if(!$perms->edit_background) { $notices[] = 'You aren\'t allow to change your background.'; - } elseif(!empty($_FILES['background']) && is_array($_FILES['background'])) { - if(!empty($_FILES['background']['name']['file'])) { - if($_FILES['background']['error']['file'] !== UPLOAD_ERR_OK) { - switch($_FILES['background']['error']['file']) { + } elseif(!empty($_FILES['bg_file']) && is_array($_FILES['bg_file'])) { + if(!empty($_FILES['bg_file']['name'])) { + if($_FILES['bg_file']['error'] !== UPLOAD_ERR_OK) { + switch($_FILES['bg_file']['error']) { case UPLOAD_ERR_NO_FILE: $notices[] = 'Select a file before hitting upload!'; break; @@ -256,14 +261,14 @@ if($isEditing) { } } else { try { - $backgroundInfo->setFromPath($_FILES['background']['tmp_name']['file']); + $backgroundAsset->setFromPath($_FILES['bg_file']['tmp_name']); } catch(InvalidArgumentException $ex) { $exMessage = $ex->getMessage(); $notices[] = match($exMessage) { '$path is not a valid image.' => 'The file you uploaded was not an image!', '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!', - 'Dimensions of $path are too large.' => sprintf('Your background can\'t be larger than %dx%d!', $backgroundInfo->getMaxWidth(), $backgroundInfo->getMaxHeight()), - 'File size of $path is too large.' => sprintf('Your background is not allowed to be larger in file size than %s!', ByteFormat::format($backgroundInfo->getMaxBytes())), + 'Dimensions of $path are too large.' => sprintf('Your background can\'t be larger than %dx%d!', $backgroundAsset->getMaxWidth(), $backgroundAsset->getMaxHeight()), + 'File size of $path is too large.' => sprintf('Your background is not allowed to be larger in file size than %s!', ByteFormat::format($backgroundAsset->getMaxBytes())), default => $exMessage, }; } catch(RuntimeException $ex) { @@ -272,13 +277,16 @@ if($isEditing) { } } - $backgroundInfo->setAttachment((int)($_POST['background']['attach'] ?? 0)) - ->setBlend(!empty($_POST['background']['attr']['blend'])) - ->setSlide(!empty($_POST['background']['attr']['slide'])); + $backgroundInfo = $msz->profileCtx->backgrounds->updateProfileBackground( + $userInfo, + $bgFormat, + filter_has_var(INPUT_POST, 'bg_blend'), + filter_has_var(INPUT_POST, 'bg_slide') + ); } } - $msz->usersCtx->users->updateUser($userInfo, backgroundSettings: $backgroundInfo->getSettings()); + $backgroundAsset = new UserBackgroundAsset($userInfo, $backgroundInfo); } } @@ -319,9 +327,9 @@ if(!$viewingAsGuest) { ); $activeTopicInfo = $activeTopicStats->success ? $msz->forumCtx->topics->getTopic(topicId: $activeTopicStats->topicId) : null; - $profileFieldValues = iterator_to_array($msz->profileFields->getFieldValues($userInfo)); - $profileFieldInfos = $profileFieldInfos ?? iterator_to_array($msz->profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues)); - $profileFieldFormats = iterator_to_array($msz->profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues)); + $profileFieldValues = iterator_to_array($msz->profileCtx->fields->getFieldValues($userInfo)); + $profileFieldInfos = $profileFieldInfos ?? iterator_to_array($msz->profileCtx->fields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues)); + $profileFieldFormats = iterator_to_array($msz->profileCtx->fields->getFieldFormats(fieldValueInfos: $profileFieldValues)); $profileFieldRawValues = []; $profileFieldLinkValues = []; @@ -382,8 +390,9 @@ Template::render('profile.index', [ 'profile_is_guest' => $viewingAsGuest, 'profile_is_deleted' => false, 'profile_ban_info' => $activeBanInfo, - 'profile_avatar_info' => $avatarInfo, + 'profile_avatar_asset' => $avatarAsset, 'profile_background_info' => $backgroundInfo, + 'profile_background_asset' => $backgroundAsset, 'profile_can_send_messages' => $viewerPermsGlobal->check(Perm::G_MESSAGES_SEND), 'profile_age' => $msz->usersCtx->birthdates->getUserAge($userInfo), ]); diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php index 2b4cd89a..fd919016 100644 --- a/public-legacy/settings/data.php +++ b/public-legacy/settings/data.php @@ -146,9 +146,10 @@ 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_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_signature_content:s:n', 'user_signature_content_format:s', 'user_background_settings:i:n', '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_about_content:s:n', 'user_about_content_format:s', 'user_signature_content:s:n', 'user_signature_content_format:s', '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/MisuzuContext.php b/src/MisuzuContext.php index 34504316..735ccb27 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -21,7 +21,7 @@ use Misuzu\Messages\MessagesContext; use Misuzu\News\NewsData; use Misuzu\OAuth2\OAuth2Context; use Misuzu\Perms\PermissionsData; -use Misuzu\Profile\ProfileFieldsData; +use Misuzu\Profile\ProfileContext; use Misuzu\Redirects\RedirectsContext; use Misuzu\Routing\{BackedRoutingContext,RoutingContext}; use Misuzu\Users\{UsersContext,UserInfo}; @@ -52,11 +52,10 @@ class MisuzuContext { public private(set) ForumContext $forumCtx; public private(set) MessagesContext $messagesCtx; public private(set) OAuth2Context $oauth2Ctx; + public private(set) ProfileContext $profileCtx; public private(set) UsersContext $usersCtx; public private(set) RedirectsContext $redirectsCtx; - public private(set) ProfileFieldsData $profileFields; - public private(set) PermissionsData $perms; public private(set) AuthInfo $authInfo; public private(set) SiteInfo $siteInfo; @@ -84,6 +83,7 @@ class MisuzuContext { $this->deps->register($this->forumCtx = $this->deps->constructLazy(ForumContext::class)); $this->deps->register($this->messagesCtx = $this->deps->constructLazy(MessagesContext::class)); $this->deps->register($this->oauth2Ctx = $this->deps->constructLazy(OAuth2Context::class, config: $config->scopeTo('oauth2'))); + $this->deps->register($this->profileCtx = $this->deps->constructLazy(ProfileContext::class)); $this->deps->register($this->usersCtx = $this->deps->constructLazy(UsersContext::class)); $this->deps->register($this->redirectsCtx = $this->deps->constructLazy(RedirectsContext::class, config: $config->scopeTo('redirects'))); @@ -93,7 +93,6 @@ class MisuzuContext { $this->deps->register($this->counters = $this->deps->constructLazy(CountersData::class)); $this->deps->register($this->emotes = $this->deps->constructLazy(EmotesData::class)); $this->deps->register($this->news = $this->deps->constructLazy(NewsData::class)); - $this->deps->register($this->profileFields = $this->deps->constructLazy(ProfileFieldsData::class)); } public function getDbQueryCount(): int { diff --git a/src/Profile/ProfileBackgroundAttach.php b/src/Profile/ProfileBackgroundAttach.php new file mode 100644 index 00000000..0572ef15 --- /dev/null +++ b/src/Profile/ProfileBackgroundAttach.php @@ -0,0 +1,9 @@ +<?php +namespace Misuzu\Profile; + +enum ProfileBackgroundAttach: string { + case Cover = 'cover'; + case Stretch = 'stretch'; + case Tile = 'tile'; + case Contain = 'contain'; +} diff --git a/src/Profile/ProfileBackgroundInfo.php b/src/Profile/ProfileBackgroundInfo.php new file mode 100644 index 00000000..29657e63 --- /dev/null +++ b/src/Profile/ProfileBackgroundInfo.php @@ -0,0 +1,38 @@ +<?php +namespace Misuzu\Profile; + +use Index\Db\DbResult; + +class ProfileBackgroundInfo { + public function __construct( + public private(set) string $userId, + public private(set) ProfileBackgroundAttach $attach, + public private(set) bool $blend, + public private(set) bool $slide, + ) {} + + public static function fromResult(DbResult $result): ProfileBackgroundInfo { + return new ProfileBackgroundInfo( + userId: $result->getString(0), + attach: ProfileBackgroundAttach::tryFrom($result->getString(1)) ?? ProfileBackgroundAttach::Cover, + blend: $result->getBoolean(2), + slide: $result->getBoolean(3), + ); + } + + public bool $cover { + get => $this->attach === ProfileBackgroundAttach::Cover; + } + + public bool $stretch { + get => $this->attach === ProfileBackgroundAttach::Stretch; + } + + public bool $tile { + get => $this->attach === ProfileBackgroundAttach::Tile; + } + + public bool $contain { + get => $this->attach === ProfileBackgroundAttach::Contain; + } +} diff --git a/src/Profile/ProfileBackgroundsData.php b/src/Profile/ProfileBackgroundsData.php new file mode 100644 index 00000000..2fe17be5 --- /dev/null +++ b/src/Profile/ProfileBackgroundsData.php @@ -0,0 +1,63 @@ +<?php +namespace Misuzu\Profile; + +use RuntimeException; +use Index\Db\{DbConnection,DbStatementCache}; +use Misuzu\Users\UserInfo; + +class ProfileBackgroundsData { + private DbStatementCache $cache; + + public function __construct(DbConnection $dbConn) { + $this->cache = new DbStatementCache($dbConn); + } + + public function getProfileBackground(UserInfo|string $userInfo): ?ProfileBackgroundInfo { + $stmt = $this->cache->get(<<<SQL + SELECT user_id, bg_attach, bg_blend, bg_slide + FROM msz_profile_backgrounds + WHERE user_id = ? + SQL); + $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? ProfileBackgroundInfo::fromResult($result) : null; + } + + public function deleteProfileBackground(UserInfo|string $userInfo): void { + $stmt = $this->cache->get(<<<SQL + DELETE FROM msz_profile_backgrounds + WHERE user_id = ? + SQL); + $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo); + $stmt->execute(); + } + + public function updateProfileBackground( + UserInfo|string $userInfo, + ProfileBackgroundAttach|string $attach, + bool $blend, + bool $slide + ): ProfileBackgroundInfo { + if(is_string($attach)) + $attach = ProfileBackgroundAttach::from($attach); + + $stmt = $this->cache->get(<<<SQL + REPLACE INTO msz_profile_backgrounds ( + user_id, bg_attach, bg_blend, bg_slide + ) VALUES (?, ?, ?, ?) + SQL); + $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo); + $stmt->nextParameter($attach->value); + $stmt->nextParameter($blend ? 1 : 0); + $stmt->nextParameter($slide ? 1 : 0); + $stmt->execute(); + + $bgInfo = $this->getProfileBackground($userInfo); + if($bgInfo === null) + throw new RuntimeException('failed to update profile background'); + + return $bgInfo; + } +} diff --git a/src/Profile/ProfileContext.php b/src/Profile/ProfileContext.php new file mode 100644 index 00000000..ed0ed3a1 --- /dev/null +++ b/src/Profile/ProfileContext.php @@ -0,0 +1,14 @@ +<?php +namespace Misuzu\Profile; + +use Index\Db\DbConnection; + +class ProfileContext { + public private(set) ProfileBackgroundsData $backgrounds; + public private(set) ProfileFieldsData $fields; + + public function __construct(DbConnection $dbConn) { + $this->backgrounds = new ProfileBackgroundsData($dbConn); + $this->fields = new ProfileFieldsData($dbConn); + } +} diff --git a/src/Satori/SatoriRoutes.php b/src/Satori/SatoriRoutes.php index 8877fdb7..45667b1b 100644 --- a/src/Satori/SatoriRoutes.php +++ b/src/Satori/SatoriRoutes.php @@ -8,7 +8,7 @@ use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler,RouteHandlerCommon}; use Misuzu\Pagination; use Misuzu\Forum\ForumContext; -use Misuzu\Profile\ProfileFieldsData; +use Misuzu\Profile\ProfileContext; use Misuzu\Users\UsersContext; final class SatoriRoutes implements RouteHandler { @@ -18,7 +18,7 @@ final class SatoriRoutes implements RouteHandler { private Config $config, private UsersContext $usersCtx, private ForumContext $forumCtx, - private ProfileFieldsData $profileFields + private ProfileContext $profileCtx ) {} /** @return void|int */ @@ -49,7 +49,7 @@ final class SatoriRoutes implements RouteHandler { $fieldId = (string)$request->getParam('field', FILTER_SANITIZE_NUMBER_INT); try { - $fieldValue = $this->profileFields->getFieldValue($fieldId, $userId); + $fieldValue = $this->profileCtx->fields->getFieldValue($fieldId, $userId); } catch(RuntimeException $ex) { return ['error' => 105]; } diff --git a/src/Users/Assets/AssetsRoutes.php b/src/Users/Assets/AssetsRoutes.php index 4982e17b..f3292abe 100644 --- a/src/Users/Assets/AssetsRoutes.php +++ b/src/Users/Assets/AssetsRoutes.php @@ -8,6 +8,7 @@ use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Misuzu\Perm; use Misuzu\Auth\AuthInfo; +use Misuzu\Profile\ProfileContext; use Misuzu\Users\{UsersContext,UserInfo}; class AssetsRoutes implements RouteHandler, UrlSource { @@ -16,7 +17,8 @@ class AssetsRoutes implements RouteHandler, UrlSource { public function __construct( private AuthInfo $authInfo, private UrlRegistry $urls, - private UsersContext $usersCtx + private UsersContext $usersCtx, + private ProfileContext $profileCtx ) {} private function canViewAsset(HttpRequest $request, UserInfo $assetUser): bool { @@ -63,7 +65,10 @@ class AssetsRoutes implements RouteHandler, UrlSource { } catch(InvalidArgumentException $ex) {} if(!empty($userInfo)) { - $userAssetInfo = new UserBackgroundAsset($userInfo); + $userAssetInfo = new UserBackgroundAsset( + $userInfo, + $this->profileCtx->backgrounds->getProfileBackground($userInfo) + ); if($userAssetInfo->isPresent() && $this->canViewAsset($request, $userInfo)) $assetInfo = $userAssetInfo; } diff --git a/src/Users/Assets/UserBackgroundAsset.php b/src/Users/Assets/UserBackgroundAsset.php index 29bd45af..1aba37fb 100644 --- a/src/Users/Assets/UserBackgroundAsset.php +++ b/src/Users/Assets/UserBackgroundAsset.php @@ -1,14 +1,9 @@ <?php namespace Misuzu\Users\Assets; -use InvalidArgumentException; +use Misuzu\Profile\ProfileBackgroundInfo; use Misuzu\Users\UserInfo; -// attachment and attributes are to be stored in the same byte -// left half is for attributes, right half is for attachments -// this makes for 16 possible attachments and 4 possible attributes -// since attachments are just an incrementing number and attrs are flags - class UserBackgroundAsset extends UserImageAsset { private const FORMAT = 'backgrounds/original/%d.msz'; @@ -16,46 +11,11 @@ class UserBackgroundAsset extends UserImageAsset { private const MAX_HEIGHT = 2160; private const MAX_BYTES = 1500000; - public const ATTACH_NONE = 0x00; - public const ATTACH_COVER = 0x01; - public const ATTACH_STRETCH = 0x02; - public const ATTACH_TILE = 0x03; - public const ATTACH_CONTAIN = 0x04; - - public const ATTRIB_BLEND = 0x10; - public const ATTRIB_SLIDE = 0x20; - - public const ATTACHMENT_STRINGS = [ - self::ATTACH_COVER => 'cover', - self::ATTACH_STRETCH => 'stretch', - self::ATTACH_TILE => 'tile', - self::ATTACH_CONTAIN => 'contain', - ]; - - public const ATTRIBUTE_STRINGS = [ - self::ATTRIB_BLEND => 'blend', - self::ATTRIB_SLIDE => 'slide', - ]; - - /** @return array<int, string> */ - public static function getAttachmentStringOptions(): array { - return [ - self::ATTACH_COVER => 'Cover', - self::ATTACH_STRETCH => 'Stretch', - self::ATTACH_TILE => 'Tile', - self::ATTACH_CONTAIN => 'Contain', - ]; - } - - private int $settings; - - public function __construct(UserInfo $userInfo) { + public function __construct( + UserInfo $userInfo, + private ?ProfileBackgroundInfo $bgInfo + ) { parent::__construct($userInfo); - $this->settings = (int)$userInfo->backgroundSettings; - } - - public function getSettings(): int { - return $this->settings; } public function getMaxWidth(): int { @@ -78,68 +38,4 @@ class UserBackgroundAsset extends UserImageAsset { public function getRelativePath(): string { return sprintf(self::FORMAT, $this->getUserId()); } - - public function getAttachment(): int { - return $this->settings & 0x0F; - } - public function getAttachmentString(): string { - return self::ATTACHMENT_STRINGS[$this->getAttachment()] ?? ''; - } - public function setAttachment(int $attach): self { - $this->settings = $this->getAttributes() | ($attach & 0x0F); - return $this; - } - public function setAttachmentString(string $attach): self { - if(!in_array($attach, self::ATTACHMENT_STRINGS)) - throw new InvalidArgumentException; - $this->setAttachment(array_flip(self::ATTACHMENT_STRINGS)[$attach]); - return $this; - } - - public function getAttributes(): int { - return $this->settings & 0xF0; - } - public function setAttributes(int $attrib): self { - $this->settings = $this->getAttachment() | ($attrib & 0xF0); - return $this; - } - public function isBlend(): bool { - return ($this->getAttributes() & self::ATTRIB_BLEND) > 0; - } - public function setBlend(bool $blend): self { - $this->settings = $blend - ? ($this->settings | self::ATTRIB_BLEND) - : ($this->settings & ~self::ATTRIB_BLEND); - return $this; - } - public function isSlide(): bool { - return ($this->getAttributes() & self::ATTRIB_SLIDE) > 0; - } - public function setSlide(bool $slide): self { - $this->settings = $slide - ? ($this->settings | self::ATTRIB_SLIDE) - : ($this->settings & ~self::ATTRIB_SLIDE); - return $this; - } - - /** @return string[] */ - public function getClassNames(string $format = '%s'): array { - $names = []; - $attachment = $this->getAttachment(); - $attributes = $this->getAttributes(); - - if(array_key_exists($attachment, self::ATTACHMENT_STRINGS)) - $names[] = sprintf($format, self::ATTACHMENT_STRINGS[$attachment]); - - foreach(self::ATTRIBUTE_STRINGS as $flag => $name) - if(($attributes & $flag) > 0) - $names[] = sprintf($format, $name); - - return $names; - } - - public function delete(): void { - parent::delete(); - $this->settings = 0; - } } diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php index 201d964a..95ecf190 100644 --- a/src/Users/UserInfo.php +++ b/src/Users/UserInfo.php @@ -25,7 +25,6 @@ class UserInfo { public private(set) TextFormat $aboutBodyFormat, public private(set) ?string $signatureBody, public private(set) TextFormat $signatureBodyFormat, - public private(set) ?int $backgroundSettings, public private(set) ?string $title, ) {} @@ -48,8 +47,7 @@ class UserInfo { aboutBodyFormat: TextFormat::tryFrom($result->getString(14)) ?? TextFormat::Plain, signatureBody: $result->getStringOrNull(15), signatureBodyFormat: TextFormat::tryFrom($result->getString(16)) ?? TextFormat::Plain, - backgroundSettings: $result->getIntegerOrNull(17), - title: $result->getString(18), + title: $result->getString(17), ); } diff --git a/src/Users/UsersData.php b/src/Users/UsersData.php index c98d0c41..45f58e7e 100644 --- a/src/Users/UsersData.php +++ b/src/Users/UsersData.php @@ -9,7 +9,7 @@ use Index\Colour\Colour; use Index\Db\{DbConnection,DbStatementCache,DbTools}; use Misuzu\Pagination; use Misuzu\Tools; -use Misuzu\Parsers\Parsers; +use Misuzu\Parsers\TextFormat; class UsersData { private DbStatementCache $cache; @@ -179,7 +179,7 @@ class UsersData { u.user_display_role_id, u.user_about_content, u.user_about_content_format, u.user_signature_content, u.user_signature_content_format, - u.user_background_settings, u.user_title + u.user_title FROM msz_users AS u SQL; if($hasRoleInfo) @@ -288,7 +288,7 @@ class UsersData { user_display_role_id, user_about_content, user_about_content_format, user_signature_content, user_signature_content_format, - user_background_settings, user_title + user_title FROM msz_users SQL; if($selectId) { @@ -358,10 +358,9 @@ class UsersData { ?Colour $colour = null, RoleInfo|string|null $displayRoleInfo = null, ?string $aboutBody = null, - ?int $aboutBodyFormat = null, + TextFormat|string|null $aboutBodyFormat = null, ?string $signatureBody = null, - ?int $signatureBodyFormat = null, - ?int $backgroundSettings = null, + TextFormat|string|null $signatureBodyFormat = null, ?string $title = null ): void { if($userInfo instanceof UserInfo) @@ -409,28 +408,27 @@ class UsersData { } 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; + $values[] = $aboutBodyFormat->value; } if($signatureBody !== null && $signatureBodyFormat !== null) { + if(is_string($signatureBodyFormat)) + $signatureBodyFormat = TextFormat::tryFrom($signatureBodyFormat) ?? null; if(self::validateForumSignature($signatureBodyFormat, $signatureBody) !== '') throw new InvalidArgumentException('$signatureBody and $signatureBodyFormat contain invalid data!'); $fields[] = 'user_signature_content = ?'; $values[] = $signatureBody; $fields[] = 'user_signature_content_format = ?'; - $values[] = $signatureBodyFormat; - } - - if($backgroundSettings !== null) { - $fields[] = 'user_background_settings = ?'; - $values[] = $backgroundSettings; + $values[] = $signatureBodyFormat->value; } if($title !== null) { @@ -709,8 +707,8 @@ class UsersData { }; } - public static function validateProfileAbout(int $parser, string $text): string { - if(!Parsers::isValid($parser)) + public static function validateProfileAbout(?TextFormat $format, string $text): string { + if($format === null) return 'parser'; $length = strlen($text); @@ -729,8 +727,8 @@ class UsersData { }; } - public static function validateForumSignature(int $parser, string $text): string { - if(!Parsers::isValid($parser)) + public static function validateForumSignature(?TextFormat $format, string $text): string { + if($format === null) return 'parser'; $length = strlen($text); diff --git a/templates/_layout/input.twig b/templates/_layout/input.twig index 27f60d27..304b700b 100644 --- a/templates/_layout/input.twig +++ b/templates/_layout/input.twig @@ -30,7 +30,7 @@ {% if name|length > 0 %}name="{{ name }}"{% endif %} {% if checked %}checked{% endif %} {% if disabled %}disabled{% endif %} - {% if value|length > 0 %}value="{{ value }}"{% endif %} + {% if value is defined and value is not null %}value="{{ value }}"{% endif %} {% for name, value in attributes|default([]) %} {{ name }}{% if value|length > 0 %}="{{ value }}"{% endif %} {% endfor %}> @@ -74,9 +74,9 @@ Click here to select a file! </div> <script> - const parent = document.currentScript.parentNode, - input = parent.querySelector('input[type="file"]'), - display = parent.querySelector('.input__upload__selection'); + const parent = document.currentScript.parentNode; + const input = parent.querySelector('input[type="file"]'); + const display = parent.querySelector('.input__upload__selection'); input.addEventListener('change', ev => display.textContent = Array.from(ev.target.files).map(f => f.name).join(', ')); </script> </label> diff --git a/templates/manage/master.twig b/templates/manage/master.twig index 623a1c6a..7f60bd8d 100644 --- a/templates/manage/master.twig +++ b/templates/manage/master.twig @@ -4,7 +4,7 @@ {% set is_in_manage = true %} {% set title = title|default('Broom Closet') %} -{% set site_logo = '/images/logos/imouto-broom.png' %} +{% set main_css_vars = main_css_vars|default([])|merge({'--site-logo': "url('/images/logos/imouto-broom.png')"}) %} {% block content %} <div class="manage"> diff --git a/templates/master.twig b/templates/master.twig index 962c13e0..cadf6249 100644 --- a/templates/master.twig +++ b/templates/master.twig @@ -5,26 +5,19 @@ <link href="/vendor/fontawesome/css/all.min.css" type="text/css" rel="stylesheet"> <link href="{{ asset('common.css') }}" type="text/css" rel="stylesheet"> <link href="{{ asset('misuzu.css') }}" type="text/css" rel="stylesheet"> - {% if site_background is defined %} + {% if main_css_vars is defined and main_css_vars is iterable and main_css_vars is not empty %} <style> :root { - --background-width: {{ site_background.width }}px; - --background-height: {{ site_background.height }}px; - --background-image: url('{{ site_background_url|raw }}'); - } - </style> - {% endif %} - {% if site_logo is defined %} - <style> - :root { - --site-logo: url('{{ site_logo }}'); + {% for name, value in main_css_vars %} + {{ name }}: {{ value|raw }}; + {% endfor %} } </style> {% endif %} {% endblock %} {% set html_body_attrs = { - 'class': 'main' ~ (site_background is defined ? (' ' ~ site_background.classNames('main--bg-%s')|join(' ')) : ''), + 'class': html_classes('main', main_body_classes|default([])), 'style': global_accent_colour is defined ? ('--accent-colour: ' ~ global_accent_colour) : '', } %} diff --git a/templates/profile/index.twig b/templates/profile/index.twig index 889809b3..18cd786d 100644 --- a/templates/profile/index.twig +++ b/templates/profile/index.twig @@ -47,8 +47,8 @@ {% if perms.edit_avatar %} <ul class="profile__guidelines__section"> <li class="profile__guidelines__line profile__guidelines__line--header">Avatar</li> - <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_avatar_info.maxBytes|format_filesize }}</span> file size limit.</li> - <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_avatar_info.maxWidth }}x{{ profile_avatar_info.maxHeight }}</span>.</li> + <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_avatar_asset.maxBytes|format_filesize }}</span> file size limit.</li> + <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_avatar_asset.maxWidth }}x{{ profile_avatar_asset.maxHeight }}</span>.</li> <li class="profile__guidelines__line">Will be centre cropped and scaled to at most <span class="profile__guidelines__emphasis">240x240</span>.</li> <li class="profile__guidelines__line">Animated GIF images are allowed.</li> </ul> @@ -57,8 +57,8 @@ {% if perms.edit_background %} <ul class="profile__guidelines__section"> <li class="profile__guidelines__line profile__guidelines__line--header">Background</li> - <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_background_info.maxBytes|format_filesize }}</span> file size limit.</li> - <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_background_info.maxWidth }}x{{ profile_background_info.maxHeight }}</span>.</li> + <li class="profile__guidelines__line">May not exceed the <span class="profile__guidelines__emphasis">{{ profile_background_asset.maxBytes|format_filesize }}</span> file size limit.</li> + <li class="profile__guidelines__line">May not be larger than <span class="profile__guidelines__emphasis">{{ profile_background_asset.maxWidth }}x{{ profile_background_asset.maxHeight }}</span>.</li> <li class="profile__guidelines__line">GIF images, in general, are only allowed when tiling.</li> </ul> {% endif %} @@ -90,15 +90,14 @@ {{ container_title('Background') }} <div class="profile__background-settings__content"> - {{ input_file('background[file]', '', ['image/png', 'image/jpeg', 'image/gif'], {'id':'background-selection'}) }} + {{ input_file('bg_file', '', ['image/png', 'image/jpeg', 'image/gif'], {'id':'background-selection'}) }} - {{ input_checkbox('background[attach]', 'None', true, '', 0, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }} - {% for key, value in background_attachments %} - {{ input_checkbox('background[attach]', value, key == profile_background_info.attachment, '', key, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }} + {% for key, value in { '': 'None', 'cover': 'Cover', 'stretch': 'Stretch', 'tile': 'Tile', 'contain': 'Contain' } %} + {{ input_checkbox('bg_attach', value, key == profile_background_info.attach.value|default(''), '', key, true, {'onchange':'profileChangeBackgroundAttach(this.value)'}) }} {% endfor %} - {{ input_checkbox('background[attr][blend]', 'Blend', profile_background_info.blend, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'blend\', this.checked)'}) }} - {{ input_checkbox('background[attr][slide]', 'Slide', profile_background_info.slide, '', '', false, {'onchange':'profileToggleBackgroundAttr(\'slide\', this.checked)'}) }} + {{ input_checkbox('bg_blend', 'Blend', profile_background_info.blend|default(false), '', '', false, {'onchange':'profileToggleBackgroundAttr(\'blend\', this.checked)'}) }} + {{ input_checkbox('bg_slide', 'Slide', profile_background_info.slide|default(false), '', '', false, {'onchange':'profileToggleBackgroundAttr(\'slide\', this.checked)'}) }} </div> </div> {% endif %} @@ -304,10 +303,10 @@ function profileToggleBackground(checked) { let currentBg = document.body.style.getPropertyValue('--background-image'); - if(currentBg != 'initial' && checked) { + if(currentBg != '' && checked) { profilePreviousBackground = currentBg; - currentBg = 'initial'; - } else if(currentBg == 'initial' && !checked) { + currentBg = ''; + } else if(currentBg == '' && !checked) { currentBg = profilePreviousBackground; } @@ -315,47 +314,23 @@ } function profileChangeBackgroundAttach(mode) { - const modes = { - 1: 'cover', - 2: 'stretch', - 3: 'tile', - 4: 'contain', - }; + const modes = ['cover', 'stretch', 'tile', 'contain']; - profileToggleBackground(mode == 0); + for(const m of modes) + document.body.classList.remove(`main--bg-${m}`); - for(let i = 1; i <= Object.keys(modes).length; i++) - document.body.classList.remove('main--bg-' + modes[i]); - - if(!modes[mode]) - return; - - document.body.classList.add('main--bg-' + modes[mode]); + const valid = modes.includes(mode); + profileToggleBackground(valid); + if(valid) + document.body.classList.add(`main--bg-${mode}`); } function profileToggleBackgroundAttr(attr, mode) { - let className = ''; - - switch(attr) { - case 'blend': - className = 'main--bg-blend'; - break; - - case 'slide': - className = 'main--bg-slide'; - break; - } - - if(className) { - if(mode) - document.body.classList.add(className); - else - document.body.classList.remove(className); - } + document.body.classList.toggle(`main--bg-${attr}`, mode); } document.getElementById('background-selection').addEventListener('change', ev => { - const image = new Image(); + const image = new Image; image.src = URL.createObjectURL(ev.target.files[0]); image.addEventListener('load', () => { document.body.style.setProperty('--background-image', 'url(%)'.replace('%', image.src)); diff --git a/templates/profile/master.twig b/templates/profile/master.twig index 2523bf5f..85fd69e8 100644 --- a/templates/profile/master.twig +++ b/templates/profile/master.twig @@ -3,9 +3,20 @@ {% if profile_user is defined %} {% set image = url('user-avatar', {'user': profile_user.id, 'res': 200}) %} {% set manage_link = url('manage-user', {'user': profile_user.id}) %} - {% if (not profile_is_banned or profile_can_edit) %} - {% set site_background = profile_background_info %} - {% set site_background_url = url('user-background', {'user': profile_user.id}) %} + {% if (not profile_is_banned or profile_can_edit) and profile_background_info is not null %} + {% set main_body_classes = main_body_classes|default([])|merge({ + 'main--bg-cover': profile_background_info.cover, + 'main--bg-stretch': profile_background_info.stretch, + 'main--bg-tile': profile_background_info.tile, + 'main--bg-contain': profile_background_info.contain, + 'main--bg-blend': profile_background_info.blend, + 'main--bg-slide': profile_background_info.slide, + }) %} + {% set main_css_vars = main_css_vars|default([])|merge({ + '--background-width': '%dpx'|format(profile_background_asset.width), + '--background-height': '%dpx'|format(profile_background_asset.height), + '--background-image': "url('%s')"|format(url('user-background', {'user': profile_user.id})), + }) %} {% endif %} {% set stats = [ {