* detatch from User and Util and move to its own enum class
 * added definitions for all locales the 12340 client could in theory have
 * this is incompatble with the Intl extension
 * version bump and php requirement bump
This commit is contained in:
Sarjuuk
2025-01-27 07:47:19 +01:00
parent 40c2c63d1b
commit 398b93e9a7
72 changed files with 1007 additions and 924 deletions

View File

@@ -6,28 +6,34 @@ if (!defined('AOWOW_REVISION'))
class User
{
public static $id = 0;
public static $displayName = '';
public static $banStatus = 0x0; // see ACC_BAN_* defines
public static $groups = 0x0;
public static $perms = 0;
public static $localeId = 0;
public static $localeString = 'enus';
public static $avatar = 'inv_misc_questionmark';
public static $dailyVotes = 0;
public static int $id = 0;
public static string $displayName = '';
public static int $banStatus = 0x0; // see ACC_BAN_* defines
public static int $groups = 0x0;
public static int $perms = 0;
public static string $avatar = 'inv_misc_questionmark';
public static int $dailyVotes = 0;
public static $ip = null;
private static $reputation = 0;
private static $dataKey = '';
private static $expires = false;
private static $passHash = '';
private static $excludeGroups = 1;
private static $profiles = null;
private static int $reputation = 0;
private static string $dataKey = '';
private static bool $expires = false;
private static string $passHash = '';
private static int $excludeGroups = 1;
public static Locale $preferedLoc;
private static LocalProfileList $profiles;
public static function init()
{
self::setIP();
self::setLocale();
if (isset($_SESSION['locale']) && $_SESSION['locale'] instanceof Locale)
self::$preferedLoc = $_SESSION['locale']->validate() ?? Locale::getFallback();
else if (!empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]) && ($loc = Locale::tryFromHttpAcceptLanguage($_SERVER["HTTP_ACCEPT_LANGUAGE"])))
self::$preferedLoc = $loc;
else
self::$preferedLoc = Locale::getFallback();
// session have a dataKey to access the JScripts (yes, also the anons)
if (empty($_SESSION['dataKey']))
@@ -39,12 +45,12 @@ class User
return false;
// check IP bans
if ($ipBan = DB::Aowow()->selectRow('SELECT count, unbanDate FROM ?_account_bannedips WHERE ip = ? AND type = 0', self::$ip))
if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = 0', self::$ip))
{
if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['unbanDate'] > time())
return false;
else if ($ipBan['unbanDate'] <= time())
DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE ip = ?', self::$ip);
DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `ip` = ?', self::$ip);
}
// try to restore session
@@ -55,36 +61,39 @@ class User
if (!empty($_SESSION['timeout']) && $_SESSION['timeout'] <= time())
return false;
$query = DB::Aowow()->SelectRow('
SELECT a.id, a.passHash, a.displayName, a.locale, a.userGroups, a.userPerms, a.allowExpire, BIT_OR(ab.typeMask) AS bans, IFNULL(SUM(r.amount), 0) as reputation, a.avatar, a.dailyVotes, a.excludeGroups
$uData = DB::Aowow()->SelectRow(
'SELECT a.`id`, a.`passHash`, a.`displayName`, a.`locale`, a.`userGroups`, a.`userPerms`, a.`allowExpire`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`avatar`, a.`dailyVotes`, a.`excludeGroups`
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
WHERE a.id = ?d
GROUP BY a.id',
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`
WHERE a.`id` = ?d
GROUP BY a.`id`',
$_SESSION['user']
);
if (!$query)
if (!$uData)
return false;
if ($loc = Locale::tryFrom($uData['locale']))
self::$preferedLoc = $loc;
// password changed, terminate session
if (AUTH_MODE_SELF && $query['passHash'] != $_SESSION['hash'])
if (AUTH_MODE_SELF && $uData['passHash'] != $_SESSION['hash'])
{
self::destroy();
return false;
}
self::$id = intval($query['id']);
self::$displayName = $query['displayName'];
self::$passHash = $query['passHash'];
self::$expires = (bool)$query['allowExpire'];
self::$reputation = $query['reputation'];
self::$banStatus = $query['bans'];
self::$groups = $query['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($query['userGroups']);
self::$perms = $query['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($query['userPerms']);
self::$dailyVotes = $query['dailyVotes'];
self::$excludeGroups = $query['excludeGroups'];
self::$id = intVal($uData['id']);
self::$displayName = $uData['displayName'];
self::$passHash = $uData['passHash'];
self::$expires = (bool)$uData['allowExpire'];
self::$reputation = $uData['reputation'];
self::$banStatus = $uData['bans'];
self::$groups = $uData['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($uData['userGroups']);
self::$perms = $uData['bans'] & (ACC_BAN_TEMP | ACC_BAN_PERM) ? 0 : intval($uData['userPerms']);
self::$dailyVotes = $uData['dailyVotes'];
self::$excludeGroups = $uData['excludeGroups'];
$conditions = array(
[['cuFlags', PROFILER_CU_DELETED, '&'], 0],
@@ -96,11 +105,8 @@ class User
self::$profiles = (new LocalProfileList($conditions));
if ($query['avatar'])
self::$avatar = $query['avatar'];
if (self::$localeId != $query['locale']) // reset, if changed
self::setLocale(intVal($query['locale']));
if ($uData['avatar'])
self::$avatar = $uData['avatar'];
// stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much)
// - conscutive visits
@@ -115,10 +121,10 @@ class User
// daily votes (we need to reset this one)
self::$dailyVotes = self::getMaxDailyVotes();
DB::Aowow()->query('
UPDATE ?_account
SET dailyVotes = ?d, prevLogin = curLogin, curLogin = UNIX_TIMESTAMP(), prevIP = curIP, curIP = ?
WHERE id = ?d',
DB::Aowow()->query(
'UPDATE ?_account
SET `dailyVotes` = ?d, `prevLogin` = `curLogin`, `curLogin` = UNIX_TIMESTAMP(), `prevIP` = `curIP`, `curIP` = ?
WHERE `id` = ?d',
self::$dailyVotes,
self::$ip,
self::$id
@@ -131,16 +137,16 @@ class User
// increment consecutive visits (next day or first of new month and not more than 48h)
// i bet my ass i forgott a corner case
if ((date('j', $lastLogin) + 1 == date('j') || (date('j') == 1 && date('n', $lastLogin) != date('n'))) && (time() - $lastLogin) < 2 * DAY)
DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = consecutiveVisits + 1 WHERE id = ?d', self::$id);
DB::Aowow()->query('UPDATE ?_account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = ?d', self::$id);
else
DB::Aowow()->query('UPDATE ?_account SET consecutiveVisits = 0 WHERE id = ?d', self::$id);
DB::Aowow()->query('UPDATE ?_account SET `consecutiveVisits` = 0 WHERE `id` = ?d', self::$id);
}
}
return true;
}
private static function setIP()
private static function setIP() : void
{
$ipAddr = '';
$method = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'];
@@ -165,68 +171,32 @@ class User
self::$ip = $ipAddr ?: null;
}
/****************/
/* set language */
/****************/
// set and save
public static function setLocale($set = -1)
public static function save(bool $toDB = false)
{
$loc = LOCALE_EN;
$_SESSION['user'] = self::$id;
$_SESSION['hash'] = self::$passHash;
$_SESSION['locale'] = self::$preferedLoc;
$_SESSION['timeout'] = self::$expires ? time() + Cfg::get('SESSION_TIMEOUT_DELAY') : 0;
// $_SESSION['dataKey'] does not depend on user login status and is set in User::init()
// get
if ($set != -1 && isset(Util::$localeStrings[$set]))
$loc = $set;
else if (isset($_SESSION['locale']) && isset(Util::$localeStrings[$_SESSION['locale']]))
$loc = $_SESSION['locale'];
else if (!empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
{
$loc = strtolower(substr($_SERVER["HTTP_ACCEPT_LANGUAGE"], 0, 2));
switch ($loc) {
case 'fr': $loc = LOCALE_FR; break;
case 'de': $loc = LOCALE_DE; break;
case 'zh': $loc = LOCALE_CN; break; // may cause issues in future with zh-tw
case 'es': $loc = LOCALE_ES; break;
case 'ru': $loc = LOCALE_RU; break;
default: $loc = LOCALE_EN;
}
}
// check; pick first viable if failed
$allowedLoc = Cfg::get('LOCALES');
if ($allowedLoc && !($allowedLoc & (1 << $loc)))
{
foreach (Util::$localeStrings as $idx => $__)
{
if ($allowedLoc & (1 << $idx))
{
$loc = $idx;
break;
}
}
}
// set
if (self::$id)
DB::Aowow()->query('UPDATE ?_account SET locale = ? WHERE id = ?', $loc, self::$id);
self::useLocale($loc);
if (self::$id && $toDB)
DB::Aowow()->query('UPDATE ?_account SET `locale` = ? WHERE `id` = ?', self::$preferedLoc->value, self::$id);
}
// only use once
public static function useLocale($use)
public static function destroy()
{
self::$localeId = isset(Util::$localeStrings[$use]) ? $use : LOCALE_EN;
self::$localeString = self::localeString(self::$localeId);
session_regenerate_id(true); // session itself is not destroyed; status changed => regenerate id
session_unset();
$_SESSION['locale'] = self::$preferedLoc; // keep locale
$_SESSION['dataKey'] = self::$dataKey; // keep dataKey
self::$id = 0;
self::$displayName = '';
self::$perms = 0;
self::$groups = U_GROUP_NONE;
}
private static function localeString($loc = -1)
{
if (!isset(Util::$localeStrings[$loc]))
$loc = 0;
return Util::$localeStrings[$loc];
}
/*******************/
/* auth mechanisms */
@@ -245,21 +215,21 @@ class User
return AUTH_INTERNAL_ERR;
// handle login try limitation
$ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 0 AND ip = ?', self::$ip);
$ip = DB::Aowow()->selectRow('SELECT `ip`, `count`, `unbanDate` FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip);
if (!$ip || $ip['unbanDate'] < time()) // no entry exists or time expired; set count to 1
DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK'));
DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK'));
else // entry already exists; increment count
DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip);
DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip);
if ($ip && $ip['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ip['unbanDate'] >= time())
return AUTH_IPBANNED;
$query = DB::Aowow()->SelectRow('
SELECT a.id, a.passHash, BIT_OR(ab.typeMask) AS bans, a.status
$query = DB::Aowow()->SelectRow(
'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status`
FROM ?_account a
LEFT JOIN ?_account_banned ab ON a.id = ab.userId AND ab.end > UNIX_TIMESTAMP()
WHERE a.user = ?
GROUP BY a.id',
LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP()
WHERE a.`user` = ?
GROUP BY a.`id`',
$name
);
if (!$query)
@@ -270,7 +240,7 @@ class User
return AUTH_WRONGPASS;
// successfull auth; clear bans for this IP
DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE type = 0 AND ip = ?', self::$ip);
DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip);
if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP))
return AUTH_BANNED;
@@ -304,10 +274,19 @@ class User
case AUTH_MODE_EXTERNAL:
{
if (!file_exists('config/extAuth.php'))
{
trigger_error('config/extAuth.php not found');
return AUTH_INTERNAL_ERR;
}
require 'config/extAuth.php';
if (!function_exists('extAuth'))
{
trigger_error('external auth function extAuth() not defined in config/extAuth.php');
return AUTH_INTERNAL_ERR;
}
$extGroup = -1;
$result = extAuth($name, $pass, $extId, $extGroup);
@@ -335,27 +314,22 @@ class User
return AUTH_OK;
}
// create a linked account for our settings if nessecary
private static function checkOrCreateInDB($extId, $name, $userGroup = -1)
// create a linked account for our settings if necessary
private static function checkOrCreateInDB(int $extId, string $name, int $userGroup = -1) : int
{
if (!intVal($extId))
return 0;
$userGroup = intVal($userGroup);
if ($_ = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE extId = ?d', $extId))
if ($_ = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `extId` = ?d', $extId))
{
if ($userGroup >= U_GROUP_NONE)
DB::Aowow()->query('UPDATE ?_account SET userGroups = ?d WHERE extId = ?d', $userGroup, $extId);
DB::Aowow()->query('UPDATE ?_account SET `userGroups` = ?d WHERE `extId` = ?d', $userGroup, $extId);
return $_;
}
$newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (extId, user, displayName, joinDate, prevIP, prevLogin, locale, status, userGroups) VALUES (?d, ?, ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)',
$newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (`extId`, `user`, `displayName`, `joinDate`, `prevIP`, `prevLogin`, `locale`, `status`, `userGroups`) VALUES (?d, ?, ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)',
$extId,
$name,
Util::ucFirst($name),
isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : '',
User::$localeId,
$_SERVER["REMOTE_ADDR"] ?? '',
self::$preferedLoc->value,
ACC_STATUS_OK,
$userGroup >= U_GROUP_NONE ? $userGroup : U_GROUP_NONE
);
@@ -439,39 +413,17 @@ class User
return $errCode == 0;
}
public static function save()
{
$_SESSION['user'] = self::$id;
$_SESSION['hash'] = self::$passHash;
$_SESSION['locale'] = self::$localeId;
$_SESSION['timeout'] = self::$expires ? time() + Cfg::get('SESSION_TIMEOUT_DELAY') : 0;
// $_SESSION['dataKey'] does not depend on user login status and is set in User::init()
}
public static function destroy()
{
session_regenerate_id(true); // session itself is not destroyed; status changed => regenerate id
session_unset();
$_SESSION['locale'] = self::$localeId; // keep locale
$_SESSION['dataKey'] = self::$dataKey; // keep dataKey
self::$id = 0;
self::$displayName = '';
self::$perms = 0;
self::$groups = U_GROUP_NONE;
}
/*********************/
/* access management */
/*********************/
public static function isInGroup($group)
public static function isInGroup($group) : bool
{
return (self::$groups & $group) != 0;
}
public static function canComment()
public static function canComment() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -479,7 +431,7 @@ class User
return self::$perms || self::$reputation >= Cfg::get('REP_REQ_COMMENT');
}
public static function canReply()
public static function canReply() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -487,7 +439,7 @@ class User
return self::$perms || self::$reputation >= Cfg::get('REP_REQ_REPLY');
}
public static function canUpvote()
public static function canUpvote() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_COMMENT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -495,7 +447,7 @@ class User
return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_UPVOTE') && self::$dailyVotes > 0);
}
public static function canDownvote()
public static function canDownvote() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -503,7 +455,7 @@ class User
return self::$perms || (self::$reputation >= Cfg::get('REP_REQ_DOWNVOTE') && self::$dailyVotes > 0);
}
public static function canSupervote()
public static function canSupervote() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_RATE | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -511,7 +463,7 @@ class User
return self::$reputation >= Cfg::get('REP_REQ_SUPERVOTE');
}
public static function canUploadScreenshot()
public static function canUploadScreenshot() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_SCREENSHOT | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -519,7 +471,7 @@ class User
return true;
}
public static function canWriteGuide()
public static function canWriteGuide() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_GUIDE | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -527,7 +479,7 @@ class User
return true;
}
public static function canSuggestVideo()
public static function canSuggestVideo() : bool
{
if (!self::$id || self::$banStatus & (ACC_BAN_VIDEO | ACC_BAN_PERM | ACC_BAN_TEMP))
return false;
@@ -535,27 +487,28 @@ class User
return true;
}
public static function isPremium()
public static function isPremium() : bool
{
return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM');
}
/**************/
/* js-related */
/**************/
public static function decrementDailyVotes()
public static function decrementDailyVotes() : void
{
self::$dailyVotes--;
DB::Aowow()->query('UPDATE ?_account SET dailyVotes = ?d WHERE id = ?d', self::$dailyVotes, self::$id);
DB::Aowow()->query('UPDATE ?_account SET `dailyVotes` = ?d WHERE `id` = ?d', self::$dailyVotes, self::$id);
}
public static function getCurDailyVotes()
public static function getCurrentDailyVotes() : int
{
return self::$dailyVotes;
}
public static function getMaxDailyVotes()
public static function getMaxDailyVotes() : int
{
if (!self::$id || self::$banStatus & (ACC_BAN_PERM | ACC_BAN_TEMP))
return 0;
@@ -563,12 +516,12 @@ class User
return Cfg::get('USER_MAX_VOTES') + (self::$reputation >= Cfg::get('REP_REQ_VOTEMORE_BASE') ? 1 + intVal((self::$reputation - Cfg::get('REP_REQ_VOTEMORE_BASE')) / Cfg::get('REP_REQ_VOTEMORE_ADD')) : 0);
}
public static function getReputation()
public static function getReputation() : int
{
return self::$reputation;
}
public static function getUserGlobals()
public static function getUserGlobals() : array
{
$gUser = array(
'id' => self::$id,
@@ -613,34 +566,34 @@ class User
return $gUser;
}
public static function getWeightScales()
public static function getWeightScales() : array
{
$result = [];
$res = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, name FROM ?_account_weightscales WHERE userId = ?d', self::$id);
$res = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `name` FROM ?_account_weightscales WHERE `userId` = ?d', self::$id);
if (!$res)
return $result;
$weights = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, `field` AS ARRAY_KEY2, val FROM ?_account_weightscale_data WHERE id IN (?a)', array_keys($res));
$weights = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `field` AS ARRAY_KEY2, `val` FROM ?_account_weightscale_data WHERE `id` IN (?a)', array_keys($res));
foreach ($weights as $id => $data)
$result[] = array_merge(['name' => $res[$id], 'id' => $id], $data);
return $result;
}
public static function getProfilerExclusions()
public static function getProfilerExclusions() : array
{
$result = [];
$modes = [1 => 'excludes', 2 => 'includes'];
foreach ($modes as $mode => $field)
if ($ex = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, typeId AS ARRAY_KEY2, typeId FROM ?_account_excludes WHERE mode = ?d AND userId = ?d', $mode, self::$id))
if ($ex = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ?_account_excludes WHERE `mode` = ?d AND `userId` = ?d', $mode, self::$id))
foreach ($ex as $type => $ids)
$result[$field][$type] = array_values($ids);
return $result;
}
public static function getCharacters()
public static function getCharacters() : array
{
if (!self::$profiles)
return [];
@@ -648,7 +601,7 @@ class User
return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER);
}
public static function getProfiles()
public static function getProfiles() : array
{
if (!self::$profiles)
return [];
@@ -675,7 +628,7 @@ class User
return [];
}
public static function getGuides()
public static function getGuides() : array
{
$result = [];
@@ -689,17 +642,17 @@ class User
return $result;
}
public static function getCookies()
public static function getCookies() : array
{
$data = [];
if (self::$id)
$data = DB::Aowow()->selectCol('SELECT name AS ARRAY_KEY, data FROM ?_account_cookies WHERE userId = ?d', self::$id);
$data = DB::Aowow()->selectCol('SELECT `name` AS ARRAY_KEY, `data` FROM ?_account_cookies WHERE `userId` = ?d', self::$id);
return $data;
}
public static function getFavorites()
public static function getFavorites() : array
{
if (!self::$id)
return [];