Introduce support for Translatable disconnection messages

this allows localizing disconnection screens (at least, once #4512 has been addressed) and the disconnect reasons shown on the console.

We already had disconnect messages implicitly localized in a few places, so this is just formalizing it.
This does break BC with any code that previously passed translation keys as the disconnect screen message, because they'll no longer be translated (only Translatables will be translatated now).
This commit is contained in:
Dylan K. Taylor 2022-12-27 18:36:07 +00:00
parent 9796dfd4d9
commit f173b91ca1
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
11 changed files with 86 additions and 55 deletions

View File

@ -26,6 +26,7 @@ namespace pocketmine\event\player;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\event\Event;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\NetworkSession;
/**
@ -35,7 +36,7 @@ use pocketmine\network\mcpe\NetworkSession;
class PlayerDuplicateLoginEvent extends Event implements Cancellable{
use CancellableTrait;
private string $disconnectMessage = "Logged in from another location";
private Translatable|string $disconnectMessage = "Logged in from another location";
public function __construct(
private NetworkSession $connectingSession,
@ -53,11 +54,11 @@ class PlayerDuplicateLoginEvent extends Event implements Cancellable{
/**
* Returns the message shown to the session which gets disconnected.
*/
public function getDisconnectMessage() : string{
public function getDisconnectMessage() : Translatable|string{
return $this->disconnectMessage;
}
public function setDisconnectMessage(string $message) : void{
public function setDisconnectMessage(Translatable|string $message) : void{
$this->disconnectMessage = $message;
}
}

View File

@ -36,7 +36,7 @@ class PlayerKickEvent extends PlayerEvent implements Cancellable{
public function __construct(
Player $player,
protected string $reason,
protected Translatable|string $reason,
protected Translatable|string $quitMessage
){
$this->player = $player;
@ -46,7 +46,7 @@ class PlayerKickEvent extends PlayerEvent implements Cancellable{
* Sets the message shown on the kicked player's disconnection screen.
* This message is also displayed in the console and server log.
*/
public function setReason(string $reason) : void{
public function setReason(Translatable|string $reason) : void{
$this->reason = $reason;
}
@ -55,7 +55,7 @@ class PlayerKickEvent extends PlayerEvent implements Cancellable{
* This message is also displayed in the console and server log.
* When kicked by the /kick command, the default is something like "Kicked by admin.".
*/
public function getReason() : string{
public function getReason() : Translatable|string{
return $this->reason;
}

View File

@ -25,6 +25,7 @@ namespace pocketmine\event\player;
use pocketmine\event\Cancellable;
use pocketmine\event\Event;
use pocketmine\lang\Translatable;
use pocketmine\player\PlayerInfo;
use function array_keys;
use function count;
@ -52,7 +53,7 @@ class PlayerPreLoginEvent extends Event implements Cancellable{
self::KICK_REASON_BANNED
];
/** @var string[] reason const => associated message */
/** @var Translatable[]|string[] reason const => associated message */
protected array $kickReasons = [];
public function __construct(
@ -107,7 +108,7 @@ class PlayerPreLoginEvent extends Event implements Cancellable{
* Sets a reason to disallow the player to continue continue authenticating, with a message.
* This can also be used to change kick messages for already-set flags.
*/
public function setKickReason(int $flag, string $message) : void{
public function setKickReason(int $flag, Translatable|string $message) : void{
$this->kickReasons[$flag] = $message;
}
@ -138,7 +139,7 @@ class PlayerPreLoginEvent extends Event implements Cancellable{
/**
* Returns the kick message provided for the given kick flag, or null if not set.
*/
public function getKickMessage(int $flag) : ?string{
public function getKickMessage(int $flag) : Translatable|string|null{
return $this->kickReasons[$flag] ?? null;
}
@ -150,7 +151,7 @@ class PlayerPreLoginEvent extends Event implements Cancellable{
*
* @see PlayerPreLoginEvent::KICK_REASON_PRIORITY
*/
public function getFinalKickMessage() : string{
public function getFinalKickMessage() : Translatable|string{
foreach(self::KICK_REASON_PRIORITY as $p){
if(isset($this->kickReasons[$p])){
return $this->kickReasons[$p];

View File

@ -40,7 +40,7 @@ class PlayerQuitEvent extends PlayerEvent{
public function __construct(
Player $player,
protected Translatable|string $quitMessage,
protected string $quitReason
protected Translatable|string $quitReason
){
$this->player = $player;
}
@ -62,7 +62,7 @@ class PlayerQuitEvent extends PlayerEvent{
/**
* Returns the disconnect reason shown in the server log and on the console.
*/
public function getQuitReason() : string{
public function getQuitReason() : Translatable|string{
return $this->quitReason;
}
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\NetworkSession;
use function count;
use function spl_object_id;
@ -74,7 +75,7 @@ class NetworkSessionManager{
/**
* Terminates all connected sessions with the given reason.
*/
public function close(string $reason = "") : void{
public function close(Translatable|string $reason = "") : void{
foreach($this->sessions as $session){
$session->disconnect($reason);
}

View File

@ -34,7 +34,6 @@ use pocketmine\event\server\DataPacketReceiveEvent;
use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\form\Form;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\Translatable;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
@ -230,9 +229,7 @@ class NetworkSession{
$this->logger->info("Player: " . TextFormat::AQUA . $info->getUsername() . TextFormat::RESET);
$this->logger->setPrefix($this->getLogPrefix());
},
function(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
$this->setAuthenticationStatus($isAuthenticated, $authRequired, $error, $clientPubKey);
}
\Closure::fromCallable([$this, "setAuthenticationStatus"])
));
}
@ -542,7 +539,7 @@ class NetworkSession{
/**
* @phpstan-param \Closure() : void $func
*/
private function tryDisconnect(\Closure $func, string $reason) : void{
private function tryDisconnect(\Closure $func, Translatable|string $reason) : void{
if($this->connected && !$this->disconnectGuard){
$this->disconnectGuard = true;
$func();
@ -555,7 +552,14 @@ class NetworkSession{
$this->disposeHooks->clear();
$this->setHandler(null);
$this->connected = false;
$this->logger->info("Session closed due to $reason");
if($reason instanceof Translatable){
$translated = $this->server->getLanguage()->translate($reason);
}else{
$translated = $reason;
}
//TODO: l10n
$this->logger->info("Session closed due to $translated");
}
}
@ -567,13 +571,22 @@ class NetworkSession{
$this->invManager = null;
}
private function sendDisconnectPacket(Translatable|string $reason) : void{
if($reason instanceof Translatable){
$translated = $this->server->getLanguage()->translate($reason);
}else{
$translated = $reason;
}
$this->sendDataPacket(DisconnectPacket::create($translated));
}
/**
* Disconnects the session, destroying the associated player (if it exists).
*/
public function disconnect(string $reason, bool $notify = true) : void{
public function disconnect(Translatable|string $reason, bool $notify = true) : void{
$this->tryDisconnect(function() use ($reason, $notify) : void{
if($notify){
$this->sendDataPacket(DisconnectPacket::create($reason));
$this->sendDisconnectPacket($reason);
}
if($this->player !== null){
$this->player->onPostDisconnect($reason, null);
@ -593,7 +606,7 @@ class NetworkSession{
/**
* Instructs the remote client to connect to a different server.
*/
public function transfer(string $ip, int $port, string $reason = "transfer") : void{
public function transfer(string $ip, int $port, Translatable|string $reason = "transfer") : void{
$this->tryDisconnect(function() use ($ip, $port, $reason) : void{
$this->sendDataPacket(TransferPacket::create($ip, $port), true);
if($this->player !== null){
@ -605,9 +618,9 @@ class NetworkSession{
/**
* Called by the Player when it is closed (for example due to getting kicked).
*/
public function onPlayerDestroyed(string $reason) : void{
public function onPlayerDestroyed(Translatable|string $reason) : void{
$this->tryDisconnect(function() use ($reason) : void{
$this->sendDataPacket(DisconnectPacket::create($reason));
$this->sendDisconnectPacket($reason);
}, $reason);
}
@ -623,7 +636,7 @@ class NetworkSession{
}, $reason);
}
private function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
private function setAuthenticationStatus(bool $authenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPubKey) : void{
if(!$this->connected){
return;
}
@ -636,7 +649,7 @@ class NetworkSession{
}
if($error !== null){
$this->disconnect($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_invalidSession($this->server->getLanguage()->translateString($error))));
$this->disconnect(KnownTranslationFactory::pocketmine_disconnect_invalidSession($error));
return;
}
@ -645,7 +658,7 @@ class NetworkSession{
if(!$this->authenticated){
if($authRequired){
$this->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_NOTAUTHENTICATED);
$this->disconnect(KnownTranslationFactory::disconnectionScreen_notAuthenticated());
return;
}
if($this->info instanceof XboxLivePlayerInfo){

View File

@ -23,7 +23,8 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\JwtChainLinkBody;
@ -49,7 +50,7 @@ class ProcessLoginTask extends AsyncTask{
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*/
private ?string $error = "Unknown";
private Translatable|string|null $error = "Unknown";
/**
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
* root public key.
@ -59,7 +60,7 @@ class ProcessLoginTask extends AsyncTask{
/**
* @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPublicKey) : void $onCompletion
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
array $chainJwts,
@ -76,7 +77,7 @@ class ProcessLoginTask extends AsyncTask{
$this->clientPublicKey = $this->validateChain();
$this->error = null;
}catch(VerifyLoginException $e){
$this->error = $e->getMessage();
$this->error = $e->getDisconnectMessage();
}
}
@ -109,7 +110,8 @@ class ProcessLoginTask extends AsyncTask{
try{
[$headersArray, $claimsArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), 0, $e);
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
@ -121,21 +123,23 @@ class ProcessLoginTask extends AsyncTask{
/** @var JwtHeader $headers */
$headers = $mapper->map($headersArray, new JwtHeader());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), 0, $e);
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
}
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($currentPublicKey === null){
if(!$first){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY);
throw new VerifyLoginException("Missing JWT public key", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
}
}elseif($headerDerKey !== $currentPublicKey){
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
try{
@ -145,10 +149,10 @@ class ProcessLoginTask extends AsyncTask{
}
try{
if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), 0, $e);
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
@ -164,16 +168,16 @@ class ProcessLoginTask extends AsyncTask{
/** @var JwtChainLinkBody $claims */
$claims = $mapper->map($claimsArray, new JwtChainLinkBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), 0, $e);
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
$time = time();
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOEARLY);
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
}
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE);
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
}
if(isset($claims->identityPublicKey)){
@ -188,7 +192,7 @@ class ProcessLoginTask extends AsyncTask{
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, ?string, ?string) : void $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error, $this->clientPublicKey);

View File

@ -23,6 +23,16 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\Translatable;
class VerifyLoginException extends \RuntimeException{
private Translatable|string $disconnectMessage;
public function __construct(string $message, Translatable|string|null $disconnectMessage = null, int $code = 0, ?\Throwable $previous = null){
parent::__construct($message, $code, $previous);
$this->disconnectMessage = $disconnectMessage ?? $message;
}
public function getDisconnectMessage() : Translatable|string{ return $this->disconnectMessage; }
}

View File

@ -25,7 +25,8 @@ namespace pocketmine\network\mcpe\handler;
use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerPreLoginEvent;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\auth\ProcessLoginTask;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
use pocketmine\network\mcpe\JwtException;
@ -51,7 +52,7 @@ use function is_array;
class LoginPacketHandler extends PacketHandler{
/**
* @phpstan-param \Closure(PlayerInfo) : void $playerInfoConsumer
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void $authCallback
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPubKey) : void $authCallback
*/
public function __construct(
private Server $server,
@ -70,7 +71,7 @@ class LoginPacketHandler extends PacketHandler{
$extraData = $this->fetchAuthData($packet->chainDataJwt);
if(!Player::isValidUserName($extraData->displayName)){
$this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDNAME);
$this->session->disconnect(KnownTranslationFactory::disconnectionScreen_invalidName());
return true;
}
@ -80,7 +81,7 @@ class LoginPacketHandler extends PacketHandler{
$skin = SkinAdapterSingleton::get()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData));
}catch(\InvalidArgumentException | InvalidSkinException $e){
$this->session->getLogger()->debug("Invalid skin: " . $e->getMessage());
$this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDSKIN);
$this->session->disconnect(KnownTranslationFactory::disconnectionScreen_invalidSkin());
return true;
}
@ -116,12 +117,14 @@ class LoginPacketHandler extends PacketHandler{
$this->server->requiresAuthentication()
);
if($this->server->getNetwork()->getConnectionCount() > $this->server->getMaxPlayers()){
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_FULL, KnownTranslationKeys::DISCONNECTIONSCREEN_SERVERFULL);
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_FULL, KnownTranslationFactory::disconnectionScreen_serverFull());
}
if(!$this->server->isWhitelisted($playerInfo->getUsername())){
//TODO: l10n
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_WHITELISTED, "Server is whitelisted");
}
if($this->server->getNameBans()->isBanned($playerInfo->getUsername()) || $this->server->getIPBans()->isBanned($this->session->getIp())){
//TODO: l10n
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_BANNED, "You are banned");
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\ResourcePackChunkDataPacket;
@ -86,7 +86,7 @@ class ResourcePacksPacketHandler extends PacketHandler{
private function disconnectWithError(string $error) : void{
$this->session->getLogger()->error("Error downloading resource packs: " . $error);
$this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_RESOURCEPACK);
$this->session->disconnect(KnownTranslationFactory::disconnectionScreen_resourcePack());
}
public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{

View File

@ -96,7 +96,6 @@ use pocketmine\item\Item;
use pocketmine\item\ItemUseResult;
use pocketmine\item\Releasable;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\Language;
use pocketmine\lang\Translatable;
use pocketmine\math\Vector3;
@ -2102,13 +2101,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
/**
* Kicks a player from the server
*/
public function kick(string $reason = "", Translatable|string|null $quitMessage = null) : bool{
public function kick(Translatable|string $reason = "", Translatable|string|null $quitMessage = null) : bool{
$ev = new PlayerKickEvent($this, $reason, $quitMessage ?? $this->getLeaveMessage());
$ev->call();
if(!$ev->isCancelled()){
$reason = $ev->getReason();
if($reason === ""){
$reason = KnownTranslationKeys::DISCONNECTIONSCREEN_NOREASON;
$reason = KnownTranslationFactory::disconnectionScreen_noReason();
}
$this->disconnect($reason, $ev->getQuitMessage());
@ -2127,10 +2126,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
*
* Note for internals developers: Do not call this from network sessions. It will cause a feedback loop.
*
* @param string $reason Shown to the player, usually this will appear on their disconnect screen.
* @param Translatable|string $reason Shown on the disconnect screen, and in the server log
* @param Translatable|string|null $quitMessage Message to broadcast to online players (null will use default)
*/
public function disconnect(string $reason, Translatable|string|null $quitMessage = null) : void{
public function disconnect(Translatable|string $reason, Translatable|string|null $quitMessage = null) : void{
if(!$this->isConnected()){
return;
}
@ -2143,10 +2142,9 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @internal
* This method executes post-disconnect actions and cleanups.
*
* @param string $reason Shown to the player, usually this will appear on their disconnect screen.
* @param Translatable|string|null $quitMessage Message to broadcast to online players (null will use default)
*/
public function onPostDisconnect(string $reason, Translatable|string|null $quitMessage) : void{
public function onPostDisconnect(Translatable|string $reason, Translatable|string|null $quitMessage) : void{
if($this->isConnected()){
throw new \LogicException("Player is still connected");
}