The user's groups. */ private array $_groups; /** * @var IntegrationUser[] The user's integrations. */ private array $_integrations; /** * @var Group The user's main group. */ private Group $_main_group; /** * @var array The user's placeholders. */ private array $_placeholders; /** * @var string The session name configuration value for remembering the user. */ private string $_sessionName; /** * @var string The cookie name configuration value. */ private string $_cookieName; /** * @var bool Whether this user is logged in or not. */ private bool $_isLoggedIn = false; /** * @var string The session name configuration value for remembering the admin user. */ private string $_admSessionName; /** * @var bool Whether this user is logged in as an admin or not. */ private bool $_isAdmLoggedIn = false; public function __construct(?string $user = null, string $field = 'id') { $this->_db = DB::getInstance(); $this->_sessionName = Config::get('session.session_name'); $this->_cookieName = Config::get('remember.cookie_name'); $this->_admSessionName = Config::get('session.admin_name'); if ($user === null) { if (Session::exists($this->_sessionName)) { $hash = Session::get($this->_sessionName); if ($this->find($hash, 'hash')) { $this->_isLoggedIn = true; $this->_db->update('users_session', ['hash', $hash], ['last_seen' => date('U')]); } } if (Session::exists($this->_admSessionName)) { $hash = Session::get($this->_admSessionName); if ($this->find($hash, 'hash')) { $this->_isAdmLoggedIn = true; $this->_db->update('users_session', ['hash', $hash], ['last_seen' => date('U')]); } } } else { $this->find($user, $field); } } /** * Find a user by unique identifier (username, ID, email, etc). * Loads instance variables for this class. * * @param string $value Unique identifier. * @param string $field What column to check for their unique identifier in. * * @return bool True/false on success or failure respectfully. */ private function find(string $value, string $field = 'id'): bool { if (isset(self::$_user_cache["$value.$field"])) { $this->_data = self::$_user_cache["$value.$field"]; return true; } if ($field !== 'hash') { $data = $this->_db->get('users', [$field, $value]); } else { $data = $this->_db->query( <<<'SQL' SELECT nl2_users.* FROM nl2_users LEFT JOIN nl2_users_session ON nl2_users.id = nl2_users_session.user_id WHERE nl2_users_session.hash = ? AND nl2_users_session.active = 1 AND ( nl2_users_session.expires_at IS NULL OR nl2_users_session.expires_at > ? ) SQL, [ $value, time(), ] ); } if ($data->count()) { $this->_data = new UserData($data->first()); self::$_user_cache["$value.$field"] = $this->_data; return true; } return false; } /** * Add a group to this user. * * @param int $group_id ID of group to give. * @param int $expire Expiry in epoch time. If not supplied, group will never expire. * * @return bool True on success, false if they already have it. */ public function addGroup(int $group_id, int $expire = 0): bool { if (array_key_exists($group_id, $this->getGroups())) { return false; } $group = Group::find($group_id); if (!$group) { ErrorHandler::logWarning('Could not add invalid group ' . $group_id . ' to user ' . $this->data()->id); return false; } $this->_db->query('INSERT INTO `nl2_users_groups` (`user_id`, `group_id`, `received`, `expire`) VALUES (?, ?, ?, ?)', [ $this->data()->id, $group_id, date('U'), $expire, ]); $this->_groups[$group_id] = $group; self::$_group_cache[$this->data()->id][$group_id] = $group; EventHandler::executeEvent(new UserGroupAddedEvent( $this, $this->_groups[$group_id], )); return true; } /** * Get the user's data. * * @return UserData This user's data. */ public function data(): ?UserData { return $this->_data ?? null; } /** * Get this user's main group CSS styling. * * @return string The CSS styling. */ public function getGroupStyle(): string { $group = $this->getMainGroup(); $group_username_color = Output::getClean($group->group_username_color); $group_username_css = Output::getClean($group->group_username_css); $css = ''; if ($group_username_color) { $css .= "color: $group_username_color;"; } if ($group_username_css) { $css .= $group_username_css; } return $css; } /** * Update a user's data in the database. * * @param array $fields Column names and values to update. * @throws Exception */ public function update(array $fields = []): void { if (!$this->_db->update('users', $this->data()->id, $fields)) { throw new RuntimeException('There was a problem updating your details.'); } } /** * Create a new user. * * @param array $fields Column names and values to insert to database. */ public function create(array $fields = []): void { if (!$this->_db->insert('users', $fields)) { throw new RuntimeException('There was a problem creating an account.'); } } /** * Get a user's username from their ID. * * @param int $id Their ID. * * @return ?string Their username, null on failure. */ public function idToName(int $id): ?string { $data = $this->_db->get('users', ['id', $id]); if ($data->count()) { return $data->first()->username; } return null; } /** * Get a user's nickname from their ID. * * @param int $id Their ID. * * @return ?string Their nickname, null on failure. */ public function idToNickname(int $id): ?string { $data = $this->_db->get('users', ['id', $id]); if ($data->count()) { return $data->first()->nickname; } return null; } /** * Log the user in. * * @param string|null $username Their username (or email, depending on $method). * @param string|null $password Their password. * @param bool $remember Whether to keep them logged in or not. * @param string $method What column to check for their details in. Can be either `username` or `email` or `oauth`. * * @return bool True/false on success or failure respectfully. */ public function login(?string $username = null, ?string $password = null, bool $remember = false, string $method = 'email'): bool { return $this->_commonLogin($username, $password, $remember, $method, false); } private function _commonLogin(?string $username, ?string $password, bool $remember, string $method, bool $is_admin): bool { $sessionName = $is_admin ? $this->_admSessionName : $this->_sessionName; if (!$username && $method == 'hash' && $this->exists()) { // Logged in using hash from cookie Session::put($sessionName, $password); if (!$is_admin) { $this->_isLoggedIn = true; } } elseif ($this->checkCredentials($username, $password, $method) === true) { // Valid credentials $hash = SecureRandom::alphanumeric(); $expiresAt = $remember ? time() + Config::get('remember.cookie_expiry') : null; $this->_db->insert('users_session', [ 'user_id' => $this->data()->id, 'hash' => $hash, 'remember_me' => $remember, 'active' => true, 'login_method' => $is_admin ? 'admin' : $method, 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'ip' => HttpUtils::getRemoteAddress(), 'expires_at' => $expiresAt, ]); Session::put($sessionName, $hash); if ($remember) { $cookieName = $is_admin ? ($this->_cookieName . '_adm') : $this->_cookieName; Cookie::put($cookieName, $hash, $expiresAt, HttpUtils::getProtocol() === 'https', true, false); } return true; } return false; } /** * Does this user exist? * * @return bool Whether the user exists (has data) or not. */ public function exists(): bool { return !empty($this->_data); } /** * Check whether given credentials are valid. * * @param string $username Username (or email) to check. * @param string $password Password entered by user. * @param string $method Column to search for user with. Can be `email` or `username` or `oauth`. If it is `oauth`, then the request will be granted. * * @return bool True if correct, false otherwise. */ public function checkCredentials(string $username, string $password, string $method = 'email'): bool { $user = $this->find($username, $method === 'oauth' ? 'id' : $method); if ($method === 'oauth') { return true; } if ($user) { switch ($this->data()->pass_method) { case 'sha256': [$salt, $pass] = explode('$', $this->data()->password); return $salt . hash('sha256', hash('sha256', $password) . $salt) == $salt . $pass; case 'pbkdf2': [$iterations, $salt, $pass] = explode('$', $this->data()->password); $hashed = hash_pbkdf2('sha256', $password, $salt, $iterations, 64, true); return $hashed == hex2bin($pass); case 'modernbb': case 'sha1': return sha1($password) == $this->data()->password; default: // Default to bcrypt return password_verify($password, $this->data()->password); } } return false; } /** * Handle StaffCP logins. * * @param string|null $username Their username (or email, depending on $method). * @param string|null $password Their password. * @param string $method What column to check for their details in. Can be either `username` or `email`. * * @return bool True/false on success or failure respectfully. */ public function adminLogin(?string $username = null, ?string $password = null, string $method = 'email'): bool { return $this->_commonLogin($username, $password, true, $method, true); } /** * Get user's display name. * * @param bool $username If true, will use their username. If false, will use their nickname. * @return string Their display name. */ public function getDisplayname(bool $username = false): string { if ($username) { return Output::getClean($this->data()->username); } return Output::getClean($this->data()->nickname); } /** * Build this user's profile link. * * @return string Compiled profile URL. */ public function getProfileURL(): string { return URL::build('/profile/' . urlencode($this->data()->username)); } /** * Get all of a user's groups id. Logged out/non-existent users will return just `0`. * * @return array Array of all their group IDs. */ public function getAllGroupIds(): array { if (!$this->exists()) { return [0 => 0]; } $groups = []; foreach ($this->getGroups() as $group) { $groups[$group->id] = $group->id; } return $groups; } /** * Get if this user is currently logged in or not. * * @return bool Whether they're logged in. */ public function isLoggedIn(): bool { return $this->_isLoggedIn; } /** * Get all of a user's group HTML display code. * * @return array Array of all their groups HTML. */ public function getAllGroupHtml(): array { $groups = []; foreach ($this->getGroups() as $group) { $groups[] = $group->group_html; } return $groups; } /** * Get this user's signature. * * @return string Their signature. */ public function getSignature(): string { return $this->data()->signature ?? ''; } /** * Get this user's avatar. * * @param int $size Size of image to render in pixels. * @param bool $full Whether to use full site URL or not, for external loading - ie discord webhooks. * * @return string URL to their avatar image. */ public function getAvatar(int $size = 128, bool $full = false): string { $data_obj = new stdClass(); // Convert UserData object to stdClass so we can dynamically add the 'uuid' property foreach (get_object_vars($this->data()) as $key => $value) { $data_obj->{$key} = $value; } $integrationUser = $this->getIntegration('Minecraft'); if ($integrationUser != null) { $data_obj->uuid = $integrationUser->data()->identifier; } else { $data_obj->uuid = ''; } return AvatarSource::getAvatarFromUserData($data_obj, $this->hasPermission('usercp.gif_avatar'), $size, $full); } /** * Does the user have a specific permission in any of their groups? * * @param string $permission Permission node to check recursively for. * * @return bool Whether they inherit this permission or not. */ public function hasPermission(string $permission): bool { if (!$this->exists()) { return false; } foreach ($this->getGroups() as $group) { $permissions = json_decode($group->permissions, true) ?? []; if (isset($permissions['administrator']) && $permissions['administrator'] == 1) { return true; } if (isset($permissions[$permission]) && $permissions[$permission] == 1) { return true; } } return false; } public function getActiveSessions(): array { return DB::getInstance()->query( 'SELECT * FROM nl2_users_session WHERE user_id = ? AND active = 1 ORDER BY last_seen DESC', [ $this->data()->id, ] )->results(); } /** * Log the user out from a selected session. * * @param string $sessionId Selected session ID. * * @return void */ public function logoutSessionById(string $sessionId): void { DB::getInstance()->query('UPDATE nl2_users_session SET `active` = 0 WHERE user_id = ? AND id = ?', [ $this->data()->id, $sessionId, ]); } /** * Log the user out from all other sessions. */ public function logoutAllOtherSessions(): void { DB::getInstance()->query('UPDATE nl2_users_session SET `active` = 0 WHERE user_id = ? AND hash != ?', [ $this->data()->id, Session::get(Config::get('session.session_name')), ]); } /** * Log the user out. * Deletes their cookies, sessions and database session entry. */ public function logout(): void { $this->_db->update('users_session', [['user_id', $this->data()->id], ['hash', Session::get($this->_sessionName)]], [ 'active' => 0, ]); Session::delete($this->_sessionName); Cookie::delete($this->_cookieName); } /** * Process logout if user is admin. */ public function admLogout(): void { $this->_db->update('users_session', [['user_id', $this->data()->id], ['hash', Session::get($this->_admSessionName)]], [ 'active' => 0, ]); Session::delete($this->_admSessionName); Cookie::delete($this->_cookieName . '_adm'); } /** * Get the user's groups. * * @return array Their groups. */ public function getGroups(): array { if (isset($this->_groups)) { return $this->_groups; } if (isset(self::$_group_cache[$this->data()->id])) { $this->_groups = self::$_group_cache[$this->data()->id]; } else { $groups_query = $this->_db->query('SELECT nl2_groups.* FROM nl2_users_groups INNER JOIN nl2_groups ON group_id = nl2_groups.id WHERE user_id = ? AND deleted = 0 ORDER BY `order`', [$this->data()->id]); if ($groups_query->count()) { foreach ($groups_query->results() as $item) { $this->_groups[$item->id] = new Group($item); } } else { $this->_groups = []; } self::$_group_cache[$this->data()->id] = $this->_groups; } if (!count($this->_groups)) { // Get default group $default_group = Group::find(1, 'default_group'); $default_group_id = $default_group->id ?? 1; $this->addGroup($default_group_id); } return $this->_groups; } /** * Get the user's integrations. * * @return IntegrationUser[] Their integrations. */ public function getIntegrations(): array { if (isset($this->_integrations)) { return $this->_integrations; } $integrations = Integrations::getInstance(); if (isset(self::$_integration_cache[$this->data()->id])) { $integrations_query = self::$_integration_cache[$this->data()->id]; } else { $integrations_query = $this->_db->query('SELECT nl2_users_integrations.*, nl2_integrations.name as integration_name FROM nl2_users_integrations LEFT JOIN nl2_integrations ON integration_id = nl2_integrations.id WHERE user_id = ?', [$this->data()->id]); if ($integrations_query->count()) { $integrations_query = $integrations_query->results(); } else { $integrations_query = []; } self::$_integration_cache[$this->data()->id] = $integrations_query; } $integrations_list = []; foreach ($integrations_query as $item) { $integration = $integrations->getIntegration($item->integration_name); if ($integration != null) { $integrationUser = new IntegrationUser($integration, $this->data()->id, 'user_id', $item); $integrations_list[$item->integration_name] = $integrationUser; } } return $this->_integrations = $integrations_list; } /** * Get the user's integration. * * @param string $integrationName Integration name * * @return IntegrationUser|null Their integration user if connected otherwise null. */ public function getIntegration(string $integrationName): ?IntegrationUser { return $this->getIntegrations()[$integrationName] ?? null; } /** * Get the users placeholders. * * @return array Their placeholders. */ public function getPlaceholders(): array { if (isset($this->_placeholders)) { return $this->_placeholders; } $integrationUser = $this->getIntegration('Minecraft'); if ($integrationUser !== null) { return $this->_placeholders = Placeholders::getInstance()->loadUserPlaceholders($integrationUser->data()->identifier); } return []; } /** * Get this user's placeholders to display on their forum posts. * * @return array Forum placeholders. */ public function getForumPlaceholders(): array { return array_filter($this->getPlaceholders(), static function ($placeholder) { return $placeholder->show_on_forum; }); } /** * Get this user's placeholders to display on their profile. * * @return array Profile placeholders. */ public function getProfilePlaceholders(): array { return array_filter($this->getPlaceholders(), static function ($placeholder) { return $placeholder->show_on_profile; }); } /** * Get this user's main group (with highest order). * * @return Group The group */ public function getMainGroup(): Group { return $this->_main_group ??= array_reduce($this->getGroups(), static function (?Group $carry, Group $group) { return $carry === null || $carry->order > $group->order ? $group : $carry; }); } /** * Set a group to user and remove all other groups. * * @param int $group_id ID of group to set as main group. * @param int $expire Expiry in epoch time. If not supplied, group will never expire. * @return false|void */ public function setGroup(int $group_id, int $expire = 0) { $group = Group::find($group_id); if (!$group) { ErrorHandler::logWarning('Could not set invalid group ' . $group_id . ' to user ' . $this->data()->id); return false; } if ($this->data()->id == 1) { return false; } $this->_db->query('DELETE FROM `nl2_users_groups` WHERE `user_id` = ?', [$this->data()->id]); $this->_db->query('INSERT INTO `nl2_users_groups` (`user_id`, `group_id`, `received`, `expire`) VALUES (?, ?, ?, ?)', [ $this->data()->id, $group_id, date('U'), $expire, ]); $this->_groups = []; $this->_groups[$group_id] = $group; self::$_group_cache[$this->data()->id] = $this->_groups; } /** * Remove a group from the user. * * @param int|null $group_id ID of group to remove. * * @return bool Returns false if they did not have this group or the admin group is being removed from root user */ public function removeGroup(?int $group_id): bool { if (!array_key_exists($group_id, $this->getGroups())) { return false; } if ($group_id == 2 && $this->data()->id == 1) { return false; } $this->_db->query('DELETE FROM `nl2_users_groups` WHERE `user_id` = ? AND `group_id` = ?', [ $this->data()->id, $group_id, ]); EventHandler::executeEvent(new UserGroupRemovedEvent( $this, $this->_groups[$group_id], )); unset($this->_groups[$group_id]); unset(self::$_group_cache[$this->data()->id][$group_id]); return true; } /** * Get if this user is active/validated or not. * * @return bool Whether this user has been validated/activated. */ public function isValidated(): bool { return $this->data()->active; } /** * Get a comma separated string of all other users who have not blocked this user. * For the new private message dropdown. * * @return array Array of usernames. */ public function listAllOtherUsers(): array { $data = $this->_db->query( 'SELECT u.username FROM nl2_users u WHERE u.id <> ? AND u.id NOT IN (SELECT user_id FROM nl2_blocked_users bu WHERE bu.user_blocked_id = ?)', [$this->data()->id, $this->data()->id] )->results(); $return = []; foreach ($data as $item) { $return[] = $item->username; } return $return; } /** * Return an ID from a username. * * @param string $username Username to get ID for. * * @return ?int ID on success, null on failure. */ public function nameToId(string $username): ?int { $data = $this->_db->get('users', ['username', $username]); if ($data->count()) { return $data->first()->id; } return null; } /** * Return an ID from an email. * * @param string $email Email to get ID for. * @return ?int ID on success, false on failure. */ public function emailToId(string $email): ?int { $data = $this->_db->get('users', ['email', $email]); if ($data->count()) { return $data->first()->id; } return null; } /** * Get a list of PMs a user has access to. * * @param int $user_id ID of user to get PMs for. * @return array Array of PMs. */ public function listPMs(int $user_id): array { $return = []; // Array to return containing info of PMs // Get a list of PMs which the user is in $data = $this->_db->get('private_messages_users', ['user_id', $user_id]); if ($data->count()) { $data = $data->results(); foreach ($data as $result) { // Get a list of users who are in this conversation and return them as an array $pms = $this->_db->get('private_messages_users', ['pm_id', $result->pm_id])->results(); $users = []; // Array containing users with permission foreach ($pms as $pm) { $users[] = $pm->user_id; } // Get the PM data $pm = $this->_db->get('private_messages', ['id', $result->pm_id])->first(); $return[$pm->id]['id'] = $pm->id; $return[$pm->id]['title'] = $pm->title; $return[$pm->id]['created'] = $pm->created; $return[$pm->id]['updated'] = $pm->last_reply_date; $return[$pm->id]['user_updated'] = $pm->last_reply_user; $return[$pm->id]['users'] = $users; } } // Order the PMs by date updated - most recent first usort($return, static function ($a, $b) { return $b['updated'] - $a['updated']; }); return $return; } /** * Get a specific private message, and see if the user actually has permission to view it. * * @param int $pm_id ID of PM to find. * @param int $user_id ID of user to check permission for. * @return array|null Array of info about PM, null on failure. */ public function getPM(int $pm_id, int $user_id): ?array { // Get the PM - is the user the author? $data = $this->_db->get('private_messages', ['id', $pm_id]); if ($data->count()) { $data = $data->first(); // Does the user have permission to view the PM? $pms = $this->_db->get('private_messages_users', ['pm_id', $pm_id])->results(); foreach ($pms as $pm) { if ($pm->user_id == $user_id) { $has_permission = true; $pm_user_id = $pm->id; break; } } if (!isset($has_permission)) { return null; // User doesn't have permission } // Set message to "read" if ($pm->read == 0) { $this->_db->update('private_messages_users', $pm_user_id, [ 'read' => true, ]); } // User has permission, return the PM information $users = []; // Array to store users foreach ($pms as $pm) { $users[] = $pm->user_id; } return [$data, $users]; } return null; } /** * Check the user's permission to see if they can view this staffCP page or not. * If they cannot, this will handle appropriate redirection. * * @param string|null $permission Permission required for this page. * @return bool */ public function handlePanelPageLoad(?string $permission = null): bool { // Set page user is trying to access in session, to allow for redirection post-auth if (FRIENDLY_URLS === true) { $split = explode('?', $_SERVER['REQUEST_URI']); if ($split != null && count($split) > 1) { $_SESSION['last_page'] = URL::build($split[0], $split[1]); } else { $_SESSION['last_page'] = URL::build($split[0]); } if (defined('CONFIG_PATH')) { $_SESSION['last_page'] = substr($_SESSION['last_page'], strlen(CONFIG_PATH)); } } else { $_SESSION['last_page'] = URL::build($_GET['route']); } if (!$this->isLoggedIn()) { Redirect::to(URL::build('/login')); } if (!$this->canViewStaffCP()) { Redirect::to(URL::build('/')); } if (!$this->isAdmLoggedIn()) { Redirect::to(URL::build('/panel/auth')); } return !($permission != null && !$this->hasPermission($permission)); } /** * Can the specified user view StaffCP? * * @return bool Whether they can view it or not. */ public function canViewStaffCP(): bool { foreach ($this->getGroups() as $group) { if ($group->admin_cp == 1) { return true; } } return false; } /** * Get if the current user is authenticated as an administrator. * * @return bool Whether they're logged in as admin. */ public function isAdmLoggedIn(): bool { return $this->_isAdmLoggedIn; } /** * Get profile fields for this user. * * @param bool $show_private Whether to only return public fields or not (default `true`). * @param bool $only_forum Whether to only return fields which display on forum posts, only if $public is true (default `false`). * * @return array Array of profile fields. */ public function getProfileFields(bool $show_private = false, bool $only_forum = false): array { $rows = DB::getInstance()->query('SELECT pf.*, upf.id as upf_id, upf.value, upf.updated FROM nl2_profile_fields pf LEFT JOIN nl2_users_profile_fields upf ON (pf.id = upf.field_id AND upf.user_id = ?)', [ $this->data()->id, ])->results(); $fields = []; foreach ($rows as $row) { $field = new UserProfileField($row); // Check that the field is public, or they are viewing private fields // also if they're checking forum fields, check that the field is a forum field // TODO: ideally within the query if (($field->public || $show_private) && (!$only_forum || $field->forum_posts)) { $fields[$row->id] = $field; } } return $fields; } /** * Is a user blocked? * * @param int $user ID of first user * @param int $blocked ID of user who may or may not be blocked * * @return bool Whether they are blocked or not. */ public function isBlocked(int $user, int $blocked): bool { return DB::getInstance()->query( 'SELECT 1 FROM nl2_blocked_users WHERE user_id = ? AND user_blocked_id = ?', [$user, $blocked] )->exists(); } /** * Get this user's profile views. * * @return int Numer of profile views they have */ public function getProfileViews(): int { if ($this->exists()) { return $this->data()->profile_views; } return 0; } /** * Is private profile enabled and does he have the permission to use it? * * @return bool Whether profile privatizing is allowed and if they have permission to use it. */ public function canPrivateProfile(): bool { return Settings::get('private_profile') === '1' && $this->hasPermission('usercp.private_profile'); } /** * Can the user bypass private profiles? * * @return bool Whether the user can bypass private profiles */ public function canBypassPrivateProfile(): bool { return Settings::get('private_profile') === '1' && $this->hasPermission('profile.private.bypass'); } /** * Is the profile page set to private? * * @return bool Whether their profile is set to private or not. */ public function isPrivateProfile(): bool { return $this->data()->private_profile ?? false; } /** * Get templates this user's group has access to. * * @return array Templates which the user has access to. */ public function getUserTemplates(): array { $groups = '('; foreach ($this->getGroups() as $group) { $groups .= ((int) $group->id) . ','; } $groups = rtrim($groups, ',') . ')'; return $this->_db->query("SELECT template.id, template.name FROM nl2_templates AS template WHERE template.enabled = 1 AND template.id IN (SELECT template_id FROM nl2_groups_templates WHERE can_use_template = 1 AND group_id IN $groups)")->results(); } /** * Save/update this user's placeholders. * * @param int $server_id Server ID from staffcp -> integrations to assoc these placeholders with. * @param array $placeholders Key/value array of placeholders name/value from API endpoint. */ public function savePlaceholders(int $server_id, array $placeholders): void { $integrationUser = $this->getIntegration('Minecraft'); if ($integrationUser === null || !$integrationUser->getIntegration()->isEnabled()) { return; } $uuid = hex2bin(str_replace('-', '', $integrationUser->data()->identifier)); foreach ($placeholders as $name => $value) { Placeholders::getInstance()->registerPlaceholder($server_id, $name); $last_updated = time(); $this->_db->query('INSERT INTO nl2_users_placeholders (server_id, uuid, name, value, last_updated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = ?, last_updated = ?', [ $server_id, $uuid, $name, $value, $last_updated, $value, $last_updated, ]); } } /** * Get user's notification preferences. * * TODO: return type (PHP 8) * * @param string $type Optional type of notification to filter by * * @return UserNotificationData|UserNotificationData[]|null */ public function getNotificationPreferences(string $type = '') { if ($this->exists()) { $where = 'user_id = ?'; $whereVars = [$this->data()->id]; if (!empty($type)) { $where .= ' AND `type` = ?'; $whereVars[] = $type; } $this->_db->get( <<_db->first(); } return $this->_db->results(); } return null; } }