Template/Update (Part 46 - V)

* account management rework: Avatar functionality
 * show avatar at comments (beckported, because no forums)
This commit is contained in:
Sarjuuk 2025-08-28 17:46:57 +02:00
parent 258ac19f0a
commit 1d5539b362
24 changed files with 839 additions and 58 deletions

View file

@ -14,12 +14,13 @@ class AccountBaseResponse extends TemplateResponse
protected array $scripts = [[SC_JS_FILE, 'js/account.js']];
// display status of executed step (forwarding back to this page)
public ?array $generalMessage = null;
public ?array $emailMessage = null;
public ?array $usernameMessage = null;
public ?array $passwordMessage = null;
public ?array $communityMessage = null;
public ?array $avatarMessage = null;
public ?array $generalMessage = null;
public ?array $emailMessage = null;
public ?array $usernameMessage = null;
public ?array $passwordMessage = null;
public ?array $communityMessage = null;
public ?array $avatarMessage = null;
public ?array $premiumborderMessage = null;
// form fields
public int $modelrace = 0;
@ -36,6 +37,7 @@ class AccountBaseResponse extends TemplateResponse
public int $customicon = 0;
public array $customicons = [];
public bool $premium = false;
public int $reputation = 0;
public ?Listview $avatarManager = null;
public ?array $bans;
@ -130,7 +132,7 @@ class AccountBaseResponse extends TemplateResponse
$this->avMode = $user['avatar'];
// status [reviewing, ok, rejected]? (only 2: rejected processed in js)
if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d AND `status` > 0', User::$id)))
if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d', User::$id)))
{
array_walk($cuAvatars, function (&$x) {
$x['when'] *= 1000; // uploaded timestamp expected as msec for some reason
@ -139,7 +141,7 @@ class AccountBaseResponse extends TemplateResponse
});
foreach ($cuAvatars as $a)
if ($a['status'] != 2)
if ($a['status'] != AvatarMgr::STATUS_REJECTED)
$this->customicons[$a['id']] = $a['name'];
// TODO - replace with array_find in PHP 8.4
@ -154,6 +156,8 @@ class AccountBaseResponse extends TemplateResponse
if (!$this->premium)
return;
$this->reputation = User::getReputation();
// Avatar Manager
$this->avatarManager = new Listview([
'template' => 'avatar',
@ -161,11 +165,12 @@ class AccountBaseResponse extends TemplateResponse
'name' => '$LANG.tab_avatars',
'parent' => 'avatar-manage',
'hideNav' => 1 | 2, // top | bottom
'data' => $cuAvatars ?? []
'data' => $cuAvatars ?? [],
'note' => Lang::account('avatarSlots', [count($this->customicons), Cfg::get('acc_max_avatar_uploads')])
]);
// Premium Border Selector
// ???
// solved by js
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form button on user settings page
*/
class AccountDeleteiconResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
/*
* response not evaluated
*/
protected function generate() : void
{
if (User::isBanned() || !$this->assertPOST('id'))
return;
// non-int > error
$selected = DB::Aowow()->selectCell('SELECT `current` FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id);
if ($selected === null || $selected === false)
return;
DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id);
// if deleted avatar is also currently selected, unset
if ($selected)
DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id);
$path = sprintf('static/uploads/avatars/%d.jpg', $this->_post['id']);
if (!unlink($path))
trigger_error('AccountDeleteiconResponse - failed to delete file: '.$path, E_USER_ERROR);
}
}
?>

View file

@ -0,0 +1,108 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form submit on user settings page
*/
class AccountForumavatarResponse extends TextResponse
{
protected ?string $redirectTo = '?account#community';
protected bool $requiresLogin = true;
// called via form submit
protected array $expectedPOST = array(
'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2 ]],
'wowicon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]], // file name can have \W chars: inv_misc_fork&knife, achievement_dungeon_drak'tharon_heroic
'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]]
);
// called via ajax
protected array $expectedGET = array(
'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 2, 'max_range' => 2]],
'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]]
);
private bool $success = false;
protected function generate() : void
{
if (User::isBanned())
return;
$msg = match ($this->_post['avatar'] ?? $this->_get['avatar'])
{
0 => $this->unset(), // none
1 => $this->fromIcon(), // wow icon
2 => $this->fromUpload(!$this->_get['avatar']), // custom icon (premium feature)
default => Lang::main('genericError')
};
if ($msg)
$_SESSION['msg'] = ['avatar', $this->success, $msg];
}
private function unset() : string
{
$x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id);
if ($x === null || $x === false)
return Lang::main('genericError');
$this->success = true;
return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess');
}
private function fromIcon() : string
{
if (!$this->assertPOST('wowicon'))
return Lang::main('intError');
$icon = strtolower(trim($this->_post['wowicon']));
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_icons WHERE `name` = ?', $icon))
return Lang::account('updateMessage', 'avNotFound');
$x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1, `wowicon` = ? WHERE `id` = ?d', strtolower($icon), User::$id);
if ($x === null || $x === false)
return Lang::main('genericError');
$this->success = true;
$msg = Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess');
if (($qty = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account WHERE `wowicon` = ?', $icon)) > 1)
$msg .= ' '.Lang::account('updateMessage', 'avNthUser', [$qty]);
else
$msg .= ' '.Lang::account('updateMessage', 'av1stUser');
return $msg;
}
protected function fromUpload(bool $viaPOST) : string
{
if (!User::isPremium())
return Lang::main('genericError');
if (($viaPOST && !$this->assertPOST('customicon')) || (!$viaPOST && !$this->assertGET('customicon')))
return Lang::main('intError');
$customIcon = $this->_post['customicon'] ?? $this->_get['customicon'];
$x = DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = IF(`id` = ?d, 1, 0) WHERE `userId` = ?d AND `status` <> ?d', $customIcon, User::$id, AvatarMgr::STATUS_REJECTED);
if (!is_int($x))
return Lang::main('genericError');
if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `avatar` = 2 WHERE `id` = ?d', User::$id)))
return Lang::main('intError');
$this->success = true;
return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess');
}
}
?>

View file

@ -0,0 +1,41 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form submit on user settings page
*/
class AccountPremiumborderResponse extends TextResponse
{
protected ?string $redirectTo = '?account#premium';
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected array $expectedPOST = array(
'avatarborder' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 4]],
);
protected function generate() : void
{
if (User::isBanned())
return;
if (!$this->assertPOST('avatarborder'))
return;
$x = DB::Aowow()->query('UPDATE ?_account SET `avatarborder` = ?d WHERE `id` = ?d', $this->_post['avatarborder'], User::$id);
if (!is_int($x))
$_SESSION['msg'] = ['premiumborder', false, Lang::main('genericError')];
else if (!$x)
$_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avNoChange')];
else
$_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avSuccess')];
}
}
?>

View file

@ -0,0 +1,36 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form button on user settings page
*/
class AccountRenameiconResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'name' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' =>'/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/']]
);
/*
* response not evaluated
*/
protected function generate() : void
{
if (User::isBanned() || !$this->assertPOST('id', 'name'))
return;
// regexp same as in account.js
DB::Aowow()->query('UPDATE ?_account_avatars SET `name` = ? WHERE `id` = ?d AND `userId` = ?d', trim($this->_post['name']), $this->_post['id'], User::$id);
}
}
?>

View file

@ -0,0 +1,88 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class UploadImagecompleteResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected ?string $redirectTo = '?account#community';
protected array $expectedPOST = array(
'coords' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCoords']],
);
public string $imgHash;
public int $newId;
public function __construct(string $pageParam)
{
if (User::isBanned())
$this->generate404();
parent::__construct($pageParam);
if (!preg_match('/^upload=image-complete&(\d+)\.(\w{16})$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL))
$this->generate404();
[, $this->newId, $this->imgHash] = $m;
if (!$this->imgHash || !$this->newId)
$this->generate404();
}
protected function generate() : void
{
if (!$this->handleComplete())
$_SESSION['msg'] = ['avatar', false, AvatarMgr::$error ?: Lang::main('intError')];
}
private function handleComplete() : bool
{
if (!$this->assertPOST('coords'))
return false;
if (!AvatarMgr::init())
return false;
if (!AvatarMgr::loadFile(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original'))
return false;
if (!AvatarMgr::cropImg(...$this->_post['coords']))
return false;
if (!AvatarMgr::createAtlas($this->newId))
return false;
$fSize = filesize(sprintf(AvatarMgr::PATH_AVATARS, $this->newId));
if (!$fSize)
return false;
$newId = DB::Aowow()->query('INSERT INTO ?_account_avatars (`id`, `userId`, `name`, `when`, `size`) VALUES (?d, ?d, ?, ?d, ?d)', $this->newId, User::$id, 'Avatar '.$this->newId, time(), $fSize);
if (!is_int($newId))
{
trigger_error('UploadImagecompleteResponse - avatar query failed', E_USER_ERROR);
return false;
}
// delete temp files
unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original'));
unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash));
return true;
}
protected static function checkCoords(string $val) : ?array
{
if (preg_match('/^[01]\.[0-9]{3}(,[01]\.[0-9]{3}){3}$/', $val))
return explode(',', $val);
return null;
}
}
?>

View file

@ -0,0 +1,84 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class UploadImagecropResponse extends TemplateResponse
{
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected string $template = 'image-crop';
protected string $pageName = 'image-crop';
protected array $scripts = array(
[SC_JS_FILE, 'js/Cropper.js'],
[SC_CSS_FILE, 'css/Cropper.css']
);
public array $cropper = [];
public int $nextId = 0;
public string $imgHash = '';
public function __construct(string $pageParam)
{
if (User::isBanned())
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
if ($err = $this->handleUpload())
{
$_SESSION['msg'] = ['avatar', false, $err];
$this->forward('?account#community');
}
$this->h1 = Lang::account('avatarSubmit');
$fileBase = User::$username.'-avatar-'.$this->nextId.'-'.$this->imgHash;
$dimensions = AvatarMgr::calcImgDimensions();
$this->cropper = $dimensions + array(
'url' => Cfg::get('STATIC_URL').'/uploads/temp/'.$fileBase.'.jpg',
'parent' => 'av-container',
'minCrop' => ICON_SIZE_LARGE, // optional; defaults to 150 - min selection size (a square)
'type' => Type::NPC, // NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)]
'typeId' => 15384, // = arbitrary image upload
'constraint' => [1, 1] // [xMult, yMult] - relative size to each other (here: be square)
);
parent::generate();
}
private function handleUpload() : string
{
if (!AvatarMgr::init())
return Lang::main('intError');
if (!AvatarMgr::validateUpload())
return AvatarMgr::$error;
if (!AvatarMgr::loadUpload())
return Lang::main('intError');
$n = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account_avatars WHERE `userId` = ?d', User::$id);
if ($n && $n > Cfg::get('ACC_MAX_AVATAR_UPLOADS'))
return Lang::main('intError');
// why is ++(<IntExpression>); illegal syntax? WHO KNOWS!?
$this->nextId = (DB::Aowow()->selectCell('SELECT MAX(`id`) FROM ?_account_avatars') ?: 0) + 1;
if (!AvatarMgr::tempSaveUpload(['avatar', $this->nextId], $this->imgHash))
return Lang::main('intError');
return '';
}
}
?>

View file

@ -38,7 +38,7 @@ class UserBaseResponse extends TemplateResponse
if (!$pageParam)
$this->forwardToSignIn('user');
if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam))
if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`avatarborder`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam))
$this->user = $user;
else
$this->generateNotFound(Lang::user('notFound', [$pageParam]));
@ -115,12 +115,15 @@ class UserBaseResponse extends TemplateResponse
default => ''
};
if (!($this->user['userGroups'] & U_GROUP_PREMIUM))
$this->user['avatarborder'] = 2;
$this->userIcon = array( // JS: Icon.createUser()
$this->user['avatar'], // avatar: 1(iconString), 2(customId)
$avatarMore, // avatarMore: iconString or customId
IconElement::SIZE_MEDIUM, // size: (always medium)
null, // url: (always null)
User::isInGroup(U_GROUP_PREMIUM) ? 0 : 2, // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red']
$this->user['avatarborder'], // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red']
false, // noBorder: always false
'$Icon.getPrivilegeBorder('.$this->user['sumRep'].')' // reputationLevel: calculated in js from passed rep points
);

View file

@ -0,0 +1,122 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AvatarMgr extends ImageUpload
{
private const MIN_SIZE = ICON_SIZE_LARGE;
// 4k resolution
private const MAX_W = 4096;
private const MAX_H = 2160;
public const STATUS_PENDING = 0; // guessed
public const STATUS_APPROVED = 1; // guessed
public const STATUS_REJECTED = 2;
protected static string $uploadFormField = 'iconfile';
protected static string $tmpPath = self::PATH_TEMP;
public const PATH_TEMP = 'static/uploads/temp/%s.jpg';
public const PATH_AVATARS = 'static/uploads/avatars/%d.jpg';
public static function init() : bool
{
$dirErr = false;
foreach (['TEMP', 'AVATARS'] as $p)
{
$path = constant('self::PATH_' . $p);
if (!is_writable(substr($path, 0, strrpos($path, '/'))))
{
trigger_error('AvatarMgr::init - directory '.substr($path, 0, strrpos($path, '/')).' not writable', E_USER_ERROR);
$dirErr = true;
}
}
if ($dirErr)
return false;
return parent::init();
}
public static function validateUpload() : bool
{
if (!parent::validateUpload())
return false;
// invalid file
if ($is = getimagesize(self::$fileName))
{
// image size out of bounds
if ($is[0] < ICON_SIZE_LARGE || $is[1] < ICON_SIZE_LARGE)
self::$error = Lang::account('errTooSmall', [ICON_SIZE_LARGE]);
else if ($is[0] > self::MAX_W || $is[1] > self::MAX_H)
self::$error = Lang::account('selectAvatar');
}
else
self::$error = Lang::account('selectAvatar');
if (!self::$error)
return true;
self::$fileName = '';
return false;
}
/* create icon texture atlas
* ******************************
* * LARGE * MEDIUM *
* * * *
* * * *
* * *************
* * * SMOL * *
* * * * *
* * ********* *
* ******************************
*
* as static/uploads/avatars/<avatarIdx>.jpg
*/
public static function createAtlas(string $fileName) : bool
{
if (!self::$img)
return false;
$sizes = [ICON_SIZE_LARGE, ICON_SIZE_MEDIUM, ICON_SIZE_SMALL];
$dest = imagecreatetruecolor(ICON_SIZE_LARGE + ICON_SIZE_MEDIUM, ICON_SIZE_LARGE);
$srcW = imagesx(self::$img);
$srcH = imagesx(self::$img);
$destX = $destY = 0;
foreach ($sizes as $idx => $dim)
{
imagecopyresampled($dest, self::$img, $destX, $destY, 0, 0, $dim, $dim, $srcW, $srcH);
if ($idx % 2)
$destY += $dim;
else
$destX += $dim;
}
if (!imagejpeg($dest, sprintf(self::PATH_AVATARS, $fileName), self::JPEG_QUALITY))
return false;
self::$img = null;
$dest = null;
return true;
}
/*************/
/* Admin Mgr */
/*************/
// unsure yet how that's supposed to work
// for now pending uploads can be used right away
}
?>

View file

@ -26,7 +26,7 @@ class UserList extends DBTypeList
foreach ($this->iterate() as $userId => $__)
{
$data[$this->curTpl['username']] = array(
'border' => 0, // border around avatar (rarityColors)
'border' => $this->getPremiumborder(),
'roles' => $this->curTpl['userGroups'],
'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']),
'posts' => 0, // forum posts
@ -47,22 +47,39 @@ class UserList extends DBTypeList
$data[$this->curTpl['username']]['avatarmore'] = $this->curTpl['wowicon'];
break;
case 2:
if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> 2', $userId))
if ($this->isPremium())
{
$data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar'];
$data[$this->curTpl['username']]['avatarmore'] = $av;
if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> ?d', $userId, AvatarMgr::STATUS_REJECTED))
{
$data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar'];
$data[$this->curTpl['username']]['avatarmore'] = $av;
}
}
break;
}
// more optional data
// sig: markdown formated string (only used in forum?)
// border: seen as null|1|3 .. changes the border around the avatar (i suspect its meaning changed and got decoupled from premium-status with the introduction of patreon-status)
}
return [Type::USER => $data];
}
// seen as null|1|3 .. changes the border around the avatar (chosen from account > premium tab?)
// changed at the end of MoP. No longer a jsBool but index to Icon.premiumBorderClasses
private function getPremiumBorder() : int
{
if (!$this->isPremium() || !$this->curTpl['avatar'])
return 2; // 2 is "none"
return $this->curTpl['avatarborder'];
}
public function isPremium() : bool
{
return $this->curTpl['userGroups'] & U_GROUP_PREMIUM || $this->curTpl['reputation'] >= Cfg::get('REP_REQ_PREMIUM');
}
public function getListviewData() : array { return []; }
public function renderTooltip() : ?string { return null; }

View file

@ -24,6 +24,7 @@ class User
private static int $reputation = 0;
private static string $dataKey = '';
private static int $excludeGroups = 1;
private static int $avatarborder = 2; // 2 is default / reputation colored
private static ?LocalProfileList $profiles = null;
public static function init()
@ -62,7 +63,7 @@ class User
$session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id());
$userData = DB::Aowow()->selectRow(
'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug`
'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug`, a.`avatar`, a.`avatarborder`
FROM ?_account a
LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP()
LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId`
@ -119,6 +120,7 @@ class User
self::$status = $userData['status'];
self::$debug = $userData['debug'];
self::$email = $userData['email'];
self::$avatarborder = $userData['avatarborder'];
if (Cfg::get('PROFILER_ENABLE'))
{
@ -129,6 +131,18 @@ class User
self::$profiles = (new LocalProfileList($conditions));
}
// reset premium options
if (!self::isPremium())
{
if ($userData['avatar'] == 2)
{
DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1 WHERE `id` = ?d', self::$id);
DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = 0 WHERE `userId` = ?d', self::$id);
}
// avatar borders
// do not reset, it's just not sent to the browser
}
// stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much)
// - consecutive visits
@ -482,7 +496,7 @@ class User
public static function isPremium() : bool
{
return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM');
return !self::isBanned() && (self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM'));
}
public static function isLoggedIn() : bool
@ -568,14 +582,14 @@ class User
if (self::$debug)
$gUser['debug'] = true; // csv id-list output option on listviews
if (self::getPremiumBorder())
$gUser['settings'] = ['premiumborder' => 1];
if (self::isPremium())
{
$gUser['premium'] = 1;
$gUser['settings'] = ['premiumborder' => self::$avatarborder];
}
else
$gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ?
if (self::isPremium())
$gUser['premium'] = 1;
if ($_ = self::getProfilerExclusions())
$gUser = array_merge($gUser, $_);
@ -717,12 +731,6 @@ class User
return $data;
}
// not sure what to set .. user selected?
public static function getPremiumBorder() : bool
{
return self::isInGroup(U_GROUP_PREMIUM);
}
}
?>

View file

@ -950,6 +950,19 @@ $lang = array(
'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", // message_newpassdifferent
'newMailDiff' => "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", // message_newemaildifferent
// premium avatar manager
'uploadAvatar' => "Neuen Avatar hochladen",
'goToManager' => "Zur Avatarverwaltung gehen",
'manageAvatars' => "Avatare verwalten",
'avatarSlots' => '<b>%1$d / %2$d</b> Avatarplätze belegt',
'manageBorders' => "Premium Rahmen verwalten",
'selectAvatar' => "Wählt einen Avatar zum hochladen.",
'errTooSmall' => "Euer Avatar muss wenigstens %dpx groß sein.",
'cropAvatar' => "Ihr könnt Euren Avatar zuschneiden.",
'avatarSubmit' => "Avatar-Einsendung",
'reminder' => "Erinnerung",
'avatarCoC' => "Dass Benutzen von Bildern, die gegen die Regeln verstoßen kann zum Verlust Eures Premium-Status führen.",
// settings
'settings' => "Kontoeinstellungen",
'settingsNote' => "Du kannst einfach die unten stehenden Formulare ausfüllen, um deine Kontodaten zu aktualisieren.",

View file

@ -950,6 +950,19 @@ $lang = array(
'newPassDiff' => "Your new password must be different than your previous one.", // message_newpassdifferent
'newMailDiff' => "Your new email address must be different than your previous one.", // message_newemaildifferent
// premium avatar manager
'uploadAvatar' => "Upload new Avatar",
'goToManager' => "Go to Avatar Manager",
'manageAvatars' => "Manage Avatars",
'avatarSlots' => 'Using <b>%1$d / %2$d</b> avatar slots',
'manageBorders' => "Manage Premium Borders",
'selectAvatar' => "Please select the avatar to upload.",
'errTooSmall' => "Your avatar must be at last %dpx in size.",
'cropAvatar' => "You may crop your avatar.",
'avatarSubmit' => "Avatar Submission",
'reminder' => "Reminder",
'avatarCoC' => "Using imagery violating out terms of service may result in revocation of your premium privileges.",
// settings
'settings' => "Account Settings",
'settingsNote' => "Simply use the forms below to update your account information.",

View file

@ -950,6 +950,19 @@ $lang = array(
'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior.",// message_newpassdifferent
'newMailDiff' => "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", // message_newemaildifferent
// premium avatar manager
'uploadAvatar' => "[Upload new Avatar]",
'goToManager' => "[Go to Avatar Manager]",
'manageAvatars' => "[Manage Avatars]",
'avatarSlots' => '[Using <b>%1$d / %2$d</b> avatar slots]',
'manageBorders' => "[Manage Premium Borders]",
'selectAvatar' => "[Please select the avatar to upload.]",
'errTooSmall' => "[Your avatar must be at last %dpx in size.]",
'cropAvatar' => "[You may crop your avatar.]",
'avatarSubmit' => "[Avatar Submission]",
'reminder' => "[Reminder]",
'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]",
// settings
'settings' => "Mi cuenta",
'settingsNote' => "Simplemente usa el siguiente formulario para actualizar la información de tu cuenta.",

View file

@ -950,6 +950,19 @@ $lang = array(
'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien.", // message_newpassdifferent
'newMailDiff' => "Votre nouvelle adresse courriel doit être différente de l'ancienne.", // message_newemaildifferent
// premium avatar manager
'uploadAvatar' => "[Upload new Avatar]",
'goToManager' => "[Go to Avatar Manager]",
'manageAvatars' => "[Manage Avatars]",
'avatarSlots' => '[Using <b>%1$d / %2$d</b> avatar slots]',
'manageBorders' => "[Manage Premium Borders]",
'selectAvatar' => "[Please select the avatar to upload.]",
'errTooSmall' => "[Your avatar must be at last %dpx in size.]",
'cropAvatar' => "[You may crop your avatar.]",
'avatarSubmit' => "[Avatar Submission]",
'reminder' => "[Reminder]",
'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]",
// settings
'settings' => "Mon compte",
'settingsNote' => "Veuillez utiliser les formulaires ci-dessous pour apporter des changements.",

View file

@ -950,6 +950,19 @@ $lang = array(
'newPassDiff' => "Прежний и новый пароли не должны совпадать.", // message_newpassdifferent
'newMailDiff' => "Прежний и новый e-mail адреса не должны совпадать.", // message_newemaildifferent
// premium avatar manager
'uploadAvatar' => "[Upload new Avatar]",
'goToManager' => "[Go to Avatar Manager]",
'manageAvatars' => "[Manage Avatars]",
'avatarSlots' => '[Using <b>%1$d / %2$d</b> avatar slots]',
'manageBorders' => "[Manage Premium Borders]",
'selectAvatar' => "[Please select the avatar to upload.]",
'errTooSmall' => "[Your avatar must be at last %dpx in size.]",
'cropAvatar' => "[You may crop your avatar.]",
'avatarSubmit' => "[Avatar Submission]",
'reminder' => "[Reminder]",
'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]",
// settings
'settings' => "Параметры учетной записи",
'settingsNote' => "Используйте нижеприведённую форму, чтобы обновить информацию о вашей учетной записи.",

View file

@ -950,6 +950,19 @@ $lang = array(
'newPassDiff' => "你的新密码必须与以前的密码不同。", // message_newpassdifferent
'newMailDiff' => "您的新邮箱地址必须不同于旧地址。", // message_newemaildifferent
// premium avatar manager
'uploadAvatar' => "[Upload new Avatar]",
'goToManager' => "[Go to Avatar Manager]",
'manageAvatars' => "[Manage Avatars]",
'avatarSlots' => '[Using <b>%1$d / %2$d</b> avatar slots]',
'manageBorders' => "[Manage Premium Borders]",
'selectAvatar' => "[Please select the avatar to upload.]",
'errTooSmall' => "[Your avatar must be at last %dpx in size.]",
'cropAvatar' => "[You may crop your avatar.]",
'avatarSubmit' => "[Avatar Submission]",
'reminder' => "[Reminder]",
'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]",
// settings
'settings' => "账号设置",
'settingsNote' => "使用下列表格就能升级您的账号信息。",

View file

@ -39,6 +39,7 @@ CLISetup::registerUtility(new class extends UtilityScript
'static/uploads/screenshots/thumb/',
'static/uploads/temp/',
'static/uploads/guide/images/',
'static/uploads/avatars/'
);
public function __construct()

View file

@ -0,0 +1,3 @@
DELETE FROM `aowow_config` WHERE `key` = 'acc_max_avatar_uploads';
INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES
('acc_max_avatar_uploads', 10, 10, 3, 129, 'premium users may upload this many avatars');

View file

@ -0,0 +1,2 @@
ALTER TABLE `aowow_account`
ADD COLUMN `avatarborder` tinyint unsigned NOT NULL DEFAULT 2 AFTER `avatar`;

View file

@ -1161,7 +1161,8 @@ span.iconblizzard {
.iconsmall-premiumred del { background-image:url(../images/Icon/small/border/premiumred.png); }
.iconmedium-premiumred del { background-image:url(../images/Icon/medium/border/premiumred.png); }
.iconlarge-premiumred del {
background-image:url(../images/logos/special/subscribe/patron-icon.png);
background-image:url(../images/Icon/large/border/premiumred.png);
/* background-image:url(../images/logos/special/subscribe/patron-icon.png); aowow - yeah, no */
height:85px;
}

View file

@ -1124,6 +1124,25 @@ function g_GetStaffColorFromRoles(roles) {
return '';
}
// aowow - stand in for WH.User.getCommentRoleLabel
function g_GetCommentRoleLabel(roles, title) {
if (title) {
return title;
}
if (roles & U_GROUP_ADMIN) {
return g_user_roles[2]; // LANG.administrator_abbrev
}
else if (roles & U_GROUP_MOD) {
return g_user_roles[4]; // LANG.moderator
}
else if (roles & U_GROUP_PREMIUMISH) {
return LANG.premiumuser;
}
return null;
};
function g_formatDate(sp, elapsed, theDate, time, alone) {
var today = new Date();
var event_day = new Date();
@ -13957,6 +13976,49 @@ Listview.templates = {
$(div).show();
},
applyAuthorTitle: function (container, title)
{
if (!title.label)
return;
let cssClass = ['comment-reply-author-label'].concat(title.classes);
$WH.ae(container, $WH.ct(' ')); // aowow - LANG.wordspace_punct
if (title.url)
$WH.ae(container, $WH.ce('a', { className: cssClass.join(' '), href: title.url }, $WH.ct(`<${ title.label }>`)));
else
$WH.ae(container, $WH.ce('span', { className: cssClass.join(' ') }, $WH.ct(`<${ title.label }>`)));
},
getAuthorTitle: function (author)
{
let title = {
classes: [],
label: undefined,
url: undefined
};
if (g_pageInfo.author === author) {
title.label = LANG.guideAuthor;
return title;
}
let user = g_users[author];
if (user) {
// aowow - let roleColor = WH.User.getCommentTitleClass(_.roles, _.tierClass, user);
let roleColor = g_GetStaffColorFromRoles(user.roles);
if (roleColor) {
title.classes.push(roleColor);
}
// aowow - title.label = WH.User.getCommentRoleLabel(user.roles, user.title, user.tierTitle);
title.label = g_GetCommentRoleLabel(user.roles, user.title);
title.url = /* user.tierTitle && !user.title ? '/?premium' : */ ''; // aowow - tierTitle being the premium tier ("Rare|Epic|Legendary Premium User")
}
return title;
},
updateReplies: function(comment)
{
this.updateRepliesCell(comment);
@ -14063,7 +14125,13 @@ Listview.templates = {
row.attr('data-replyid', reply.id);
row.attr('data-idx', i);
row.find('.reply-text').addClass(g_GetStaffColorFromRoles(reply.roles));
// aowow - let cssClass = WH.User.getCommentRoleClass(reply.roles, reply.username);
let cssClass = g_GetStaffColorFromRoles(reply.roles);
if (!['comment-blue', 'comment-green'].includes(cssClass) && owner) {
cssClass = 'comment-green'; // comment-guide-author
}
row.find('.reply-text').addClass(cssClass);
var replyWhen = $('<a></a>');
replyWhen.text(g_formatDate(null, elapsed, creationDate));
@ -14074,12 +14142,7 @@ Listview.templates = {
replyByUserLink.attr('href', '?user=' + reply.username);
replyByUserLink.text(reply.username);
replyBy.append(replyByUserLink);
if (owner)
{
$WH.ae(replyBy[0], $WH.ct(' '));
$WH.ae(replyBy[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>')));
}
this.applyAuthorTitle(replyBy[0], this.getAuthorTitle(reply.username))
replyBy.append(' ').append(replyWhen).append(' ').append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(creationDate)));
@ -14170,7 +14233,6 @@ Listview.templates = {
updateCommentAuthor: function(comment, container)
{
var user = g_users[comment.user];
let owner = g_pageInfo.author === comment.user;
var postedOn = new Date(comment.date);
var elapsed = (g_serverTime - postedOn) / 1000;
@ -14178,12 +14240,18 @@ Listview.templates = {
container.append(LANG.lvcomment_by);
container.append($WH.sprintf('<a href="?user=$1">$2</a>', comment.user, comment.user));
if (owner)
{
$WH.ae(container[0], $WH.ct(' '));
$WH.ae(container[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>')));
// aowow - avatar recovered and transplanted from commentsv1 version
if (user != null && user.avatar) {
var icon = Icon.createUser(user.avatar, user.avatarmore, 0, null, (user.roles & U_GROUP_PREMIUM) ? user.border : Icon.STANDARD_BORDER, 0, Icon.getPrivilegeBorder(user.reputation));
icon.style.marginRight = '3px';
icon.style.cssFloat = 'left';
container.css('lineHeight', '25px');
container.append(icon);
}
// aowow - end recover
container.append(g_getReputationPlusAchievementText(user.gold, user.silver, user.copper, user.reputation));
this.applyAuthorTitle(container[0], this.getAuthorTitle(comment.user));
container.append($WH.sprintf(' <a class="q0" id="comments:id=$1" href="#comments:id=$2">$3</a>', comment.id, comment.id, g_formatDate(null, elapsed, postedOn)));
container.append(' ');
container.append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(postedOn)));

View file

@ -146,9 +146,7 @@ if ($this->bans):
<div><?=Lang::account('accDeleteNote');?></div>
</div>
<?php
endif;
?>
<?php endif; ?>
<div id="tab-community" style="display: none">
<h2 class="first"><?=Lang::account('userPage');?></h2>
@ -214,11 +212,11 @@ if ($this->bans):
</div>
<div class="clear"></div>
<?php if ($this->user::isInGroup(U_GROUP_PREMIUM) && 0): ?>
<?php if ($this->user::isInGroup(U_GROUP_PREMIUM)): ?>
<input type="radio" name="avatar" value="2" id="avaOpt2" onclick="faChange(2)"<?=($this->avMode == 2 ? ' checked="checked"' : '');?> /> <label for="avaOpt2"><?=Lang::account('custom');?></label>&nbsp;&nbsp;<span class="premium-feature-icon-small"></span><table id="avaSel2" style="padding: 6px; margin-top: 4px; margin-left: 16px; border-left: 1px solid #404040; height: 85px">
<tr><td style="padding: 5px; vertical-align: top; position: relative;">
<select name="customicon" id="customicon" style="min-width: 150px; margin-right: 5px;" onchange="spawj()">
<option>Upload new Avatar</option>
<option><?=Lang::account('uploadAvatar');?></option>
<?=$this->makeOptionsList($this->customicons, $this->customicon, 40); ?>
</select>
<div id="avaPre2" style="position: absolute; right: -68px; top: 0px"></div>
@ -226,7 +224,7 @@ if ($this->bans):
<div id="iconbrowse">
<input type="file" name="iconfile">
<div class="pad"></div>
Go to <a href="javascript:;" onclick="_.show(3, true);">Avatar Manager</a>
<a href="javascript:;" onclick="_.show(3, true);"><?=Lang::account('goToManager');?></a>
</div>
</td></tr>
</table>
@ -255,18 +253,46 @@ if ($this->bans):
<ul><li><div><?=Lang::account('status').Lang::main('colon').'<b class="q10">'.Lang::account('inactive'); ?></b></div></li></ul>
<?php else: ?>
<ul><li><div><?=Lang::account('status').Lang::main('colon').'<b class="q2">'.Lang::account('active'); ?></b></div></li></ul>
<?php endif; /*
<h2>Manage Avatars</h2>
<h2><?=Lang::account('manageAvatars');?></h2>
<div id="avatar-manage" class="listview" style="margin: 0px 10% 0px 25px;"></div>
<script type="text/javascript">//<![CDATA[
<?=$this->avatarManager; ?>
//]]></script>
<h2>Manage Premium Borders</h2>
<span>Todo</span>
<?php endif; */ ?>
</div>
<h2><?=Lang::account('manageBorders');?></h2>
<?php if ([$type, $msg] = $this->premiumborderMessage): ?>
<div class="box"><div class="msg-<?=($type ? 'success' : 'failure');?>"><?=$msg;?></div></div>
<?php endif; ?>
<form action="?account=premium-border" method="POST">
<div style="width:500px; padding-left:25px;" class="pad2">
<div style="display:flex; justify-content: space-between;" id="ipb-container"></div>
<div style="display:flex; justify-content: space-between;" id="pb-container"></div>
</div>
<input type="submit" value="<?=Lang::main('submit');?>">
<div class="pad2"></div>
</form>
<script type="text/javascript">
[2, 1, 0, 4, 3].forEach((i, k) => {
let icon = Icon.createUser(2, <?=$this->customicon;?>, 2, null, i, null, Icon.getPrivilegeBorder(<?=$this->reputation;?>));
let div = $WH.ce('div', {id: 'pb-' + i, style: 'display:inline-block'}, icon);
let input = $WH.ce('input', {
type:'radio',
name:'avatarborder',
value: i,
id: 'ipb-' + i,
style: 'width:68px; margin:10px 0px;'
});
$WH.ae($WH.ge('pb-container'), div);
$WH.ae($WH.ge('ipb-container'), input);
icon.onclick = ((x, evt) => { $WH.ge('ipb-' + x).click(); }).bind(this, i);
if (g_user?.settings?.premiumborder === i)
icon.click();
});
</script>
</div>
<?php endif; ?>
</div>
</div>
@ -280,9 +306,7 @@ if ($this->bans):
_.add('<?=Lang::account('tabPremium');?>', {id: 'premium'});
_.flush();
</script>
<?php
endif;
?>
<?php endif; ?>
<div class="clear"></div>
</div><!-- main-contents -->

View file

@ -0,0 +1,45 @@
<?php
namespace Aowow\Template;
use \Aowow\Lang;
$this->brick('header');
?>
<div class="main" id="main">
<div class="main-precontents" id="main-precontents"></div>
<div class="main-contents" id="main-contents">
<?php
$this->brick('announcement');
$this->brick('pageTemplate');
?>
<div class="text">
<h1><?=$this->h1; ?></h1>
<span><?=Lang::account('cropAvatar');?></span>
<div class="pad"></div>
<div id="av-container"></div><script type="text/javascript">//<![CDATA[
var myCropper = new Cropper(<?=$this->json($this->cropper); ?>);
//]]></script>
<div class="pad"></div>
<button style="margin:4px 0 0 5px" onclick="myCropper.selectAll()"><?=Lang::screenshot('selectAll'); ?></button>
<div class="clear"></div>
<div class="pad3"></div>
<form action="?upload=image-complete&amp;<?=$this->nextId.'.'.$this->imgHash; ?>" method="post" onsubmit="this.elements['coords'].value = myCropper.getCoords()">
<div class="pad"></div>
<h2><img src="<?=$this->gStaticUrl; ?>/images/icons/bubble-big.gif" width="32" height="29" alt="" style="vertical-align:middle;margin-right:8px"><?=Lang::account('reminder');?></h2>
<div class="pad3"><?=Lang::account('avatarCoC');?></div>
<input type="submit" value="<?=Lang::main('submit'); ?>" />
<input type="hidden" name="coords" />
</form>
</div>
</div><!-- main-contents -->
</div><!-- main -->
<?php $this->brick('footer'); ?>