Merge pull request #1083 from AuthMe/master

5.3 [W.I.P.]
This commit is contained in:
Gabriele C 2017-03-26 19:52:12 +02:00 committed by GitHub
commit 1f02ef6f6e
372 changed files with 11746 additions and 4085 deletions

185
.checkstyle.xml Normal file
View File

@ -0,0 +1,185 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<module name="SuppressWarningsFilter" />
<module name="TreeWalker">
<module name="SuppressWarningsHolder"/>
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format" value="\\u00(08|09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message" value="Avoid using corresponding octal or Unicode escape."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="LineLength">
<property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="TodoComment">
<!-- Allow comments which have an issue number as they are accounted for in the issue tracker -->
<property name="format" value="TODO(?! #\d+:)|FIXME"/>
</module>
<module name="GenericWhitespace"/>
<module name="AvoidStarImport"/>
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<module name="OneTopLevelClass"/>
<module name="FinalClass"/>
<module name="HideUtilityClassConstructor"/>
<module name="InnerTypeLast"/>
<module name="VisibilityModifier"/>
<module name="AvoidNestedBlocks"/>
<module name="NoLineWrap"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces"/>
<module name="RightCurly"/>
<module name="RightCurly">
<property name="option" value="alone"/>
<property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, STATIC_INIT, INSTANCE_INIT"/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="DefaultComesLast"/>
<module name="FallThrough"/>
<module name="NestedTryDepth"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<property name="allowOneCharVarInForLoop" value="true"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
<module name="DeclarationOrder"/>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="MethodParamPad"/>
<module name="StringLiteralEquality"/>
<module name="BooleanExpressionComplexity">
<property name="max" value="5"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens" value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR "/>
</module>
<module name="CyclomaticComplexity">
<property name="max" value="15"/>
</module>
<module name="JavaNCSS">
<property name="methodMaximum" value="40"/>
<property name="classMaximum" value="1000"/>
</module>
<module name="AnnotationLocation">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="scope" value="package"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="minLineCount" value="4"/>
<property name="allowedAnnotations" value="Override, Test, SectionComments, EventHandler"/>
<property name="tokens" value="METHOD_DEF, ANNOTATION_FIELD_DEF"/> <!-- exclude CTOR_DEF -->
</module>
<module name="JavadocMethod">
<property name="scope" value="private"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="minLineCount" value="16"/>
<property name="allowedAnnotations" value="Override, Test, SectionComments, EventHandler"/>
<property name="tokens" value="METHOD_DEF, ANNOTATION_FIELD_DEF"/> <!-- exclude CTOR_DEF -->
</module>
<!-- TODO Checkstyle/#4089: need "allowedAnnotations" property to skip @Comment fields
<module name="JavadocVariable">
<property name="scope" value="package"/>
<property name="tokens" value="VARIABLE_DEF"/>
</module>
-->
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc">
<property name="ignoredTags" value="@return"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="ignore|ignored"/>
</module>
<module name="MissingOverride"/>
<module name="EqualsHashCode"/>
<module name="EqualsAvoidNull"/>
</module>
<module name="FileTabCharacter"/>
</module>

30
.codeclimate.yml Normal file
View File

@ -0,0 +1,30 @@
engines:
duplication:
enabled: true
config:
languages:
- java
- php
checkstyle:
enabled: true
channel: beta
config: '.checkstyle.xml'
pmd:
enabled: true
channel: beta
checks:
AvoidUsingHardCodedIP:
enabled: false
ratings:
paths:
# Check only production files
- 'src/main/java/**'
exclude_paths:
# Exclude code from third-party sources
- 'src/main/java/fr/xephi/authme/mail/OAuth2Provider.java'
- 'src/main/java/fr/xephi/authme/mail/OAuth2SaslClient.java'
- 'src/main/java/fr/xephi/authme/mail/OAuth2SaslClientFactory.java'
- 'src/main/java/fr/xephi/authme/security/crypts/BCryptService.java'
- 'src/main/java/fr/xephi/authme/security/crypts/PhpBB.java'
- 'src/main/java/fr/xephi/authme/security/crypts/Whirlpool.java'
- 'src/main/java/fr/xephi/authme/security/crypts/Wordpress.java'

4
.gitignore vendored
View File

@ -1,5 +1,6 @@
### Java files ###
*.class
MANIFEST.MF
# Package Files
#*.jar
@ -9,7 +10,8 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Mac OS
.DS_Store
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm

View File

@ -1,4 +1,8 @@
sudo: false
addons:
apt:
packages:
- oracle-java8-installer
language: java
jdk: oraclejdk8

View File

@ -19,20 +19,14 @@
- Project status:
- Dependencies: [![Dependencies status](https://www.versioneye.com/user/projects/57b182e8d6ffcd0032d7cf2d/badge.svg)](https://www.versioneye.com/user/projects/57b182e8d6ffcd0032d7cf2d)
- Test coverage: [![Coverage status](https://coveralls.io/repos/AuthMe-Team/AuthMeReloaded/badge.svg?branch=master&service=github)](https://coveralls.io/github/AuthMe-Team/AuthMeReloaded?branch=master)
- Code climate: [![Code Climate](https://codeclimate.com/github/AuthMe/AuthMeReloaded/badges/gpa.svg)](https://codeclimate.com/github/AuthMe/AuthMeReloaded)
- Development resources:
- <a href="http://ci.xephi.fr/job/AuthMeReloaded/javadoc/">JavaDocs</a>
- <a href="http://ci.xephi.fr/plugin/repository/everything/">Maven Repository</a>
#####Statistics:
McStats: http://mcstats.org/plugin/AuthMe
<img src="http://i.mcstats.org/AuthMe/Global+Statistics.borderless.png">
<img src="http://i.mcstats.org/AuthMe/Rank.borderless.png">
<img src="http://i.mcstats.org/AuthMe/Version+Demographics.borderless.png">
- Statistics:
- bStats: [AuthMe on bstats.org](https://bstats.org/plugin/bukkit/AuthMe)
<hr>
@ -92,7 +86,7 @@ You can also create your own translation file and, if you want, you can share it
<li>DoubleSaltedMD5: SALTED2MD5</li>
<li>WordPress: WORDPRESS</li>
</ul></li>
<li>Custom MySQL tables/columns names (useful with forums databases)</li>
<li>Custom MySQL tables/columns names (useful with forum databases)</li>
<li><strong>Cached database queries!</strong></li>
<li><strong>Fully compatible with Citizens2, CombatTag, CombatTagPlus!</strong></li>
<li>Compatible with Minecraft mods like <strong>BuildCraft or RedstoneCraft</strong></li>

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Sun Oct 23 18:25:12 CEST 2016. See docs/commands/commands.tpl.md -->
<!-- File auto-generated on Wed Mar 22 23:10:32 CET 2017. See docs/commands/commands.tpl.md -->
## AuthMe Commands
You can use the following commands to use the features of AuthMe. Mandatory arguments are marked with `< >`
@ -47,6 +47,8 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
<br />Requires `authme.admin.converter`
- **/authme messages**: Adds missing messages to the current messages file.
<br />Requires `authme.admin.updatemessages`
- **/authme debug** [child] [params]: Allows various operations for debugging.
<br />Requires `authme.debug`
- **/authme help** [query]: View detailed help for /authme commands.
- **/login** &lt;password>: Command to log in using AuthMeReloaded.
<br />Requires `authme.player.login`
@ -69,7 +71,11 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
<br />Requires `authme.player.email.add`
- **/email change** &lt;oldEmail> &lt;newEmail>: Change an email address of your account.
<br />Requires `authme.player.email.change`
- **/email recover** &lt;email> [code]: Recover your account using an Email address by sending a mail containing a new password.
- **/email recover** &lt;email>: Recover your account using an Email address by sending a mail containing a new password.
<br />Requires `authme.player.email.recover`
- **/email code** &lt;code>: Recover your account by submitting a code delivered to your email.
<br />Requires `authme.player.email.recover`
- **/email setpassword** &lt;password>: Set a new password after successfully recovering your account.
<br />Requires `authme.player.email.recover`
- **/email help** [query]: View detailed help for /email commands.
- **/captcha** &lt;captcha>: Captcha command for AuthMeReloaded.
@ -78,4 +84,4 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Oct 23 18:25:12 CEST 2016
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Wed Mar 22 23:10:32 CET 2017

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Sat Jan 14 22:12:16 CET 2017. See docs/config/config.tpl.md -->
<!-- File auto-generated on Wed Mar 22 23:10:33 CET 2017. See docs/config/config.tpl.md -->
## AuthMe Configuration
The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder,
@ -10,7 +10,7 @@ the generated config.yml file.
DataSource:
# What type of database do you want to use?
# Valid values: sqlite, mysql
# Valid values: SQLITE, MYSQL
backend: 'SQLITE'
# Enable database caching, should improve database performance
caching: true
@ -18,9 +18,11 @@ DataSource:
mySQLHost: '127.0.0.1'
# Database port
mySQLPort: '3306'
# Username about Database Connection Infos
# Connect to MySQL database over SSL
mySQLUseSSL: true
# Username to connect to the MySQL database
mySQLUsername: 'authme'
# Password about Database Connection Infos
# Password to connect to the MySQL database
mySQLPassword: '12345'
# Database Name, use with converters or as SQLITE database name
mySQLDatabase: 'authme'
@ -34,8 +36,6 @@ DataSource:
mySQLRealName: 'realname'
# Column for storing players passwords
mySQLColumnPassword: 'password'
# Request mysql over SSL
mySQLUseSSL: true
# Column for storing players emails
mySQLColumnEmail: 'email'
# Column for storing if a player is logged in or not
@ -71,19 +71,14 @@ ExternalBoardOptions:
phpbbTablePrefix: 'phpbb_'
# phpBB activated group ID; 2 is the default registered group defined by phpBB
phpbbActivatedGroupId: 2
# IP Board table prefix defined during the IP Board installation process
IPBTablePrefix: 'ipb_'
# IP Board default group ID; 3 is the default registered group defined by IP Board
IPBActivatedGroupId: 3
# XenForo default group ID; 2 is the default registered group defined by Xenforo
XFActivatedGroupId: 2
# Wordpress prefix defined during WordPress installation
wordpressTablePrefix: 'wp_'
Converter:
Rakamak:
# Rakamak file name
fileName: 'users.rak'
# Rakamak use IP?
useIP: false
# Rakamak IP file name
ipFileName: 'UsersIp.rak'
CrazyLogin:
# CrazyLogin database file name
fileName: 'accounts.db'
settings:
sessions:
# Do you want to enable the session feature?
@ -94,13 +89,8 @@ settings:
# expired, he will not need to authenticate.
enabled: false
# After how many minutes should a session expire?
# Remember that sessions will end only after the timeout, and
# if the player's IP has changed but the timeout hasn't expired,
# the player will be kicked from the server due to invalid session
# A player's session ends after the timeout or if his IP has changed
timeout: 10
# Should the session expire if the player tries to log in with
# another IP address?
sessionExpireOnIpChange: true
# Message language, available languages:
# https://github.com/AuthMe/AuthMeReloaded/blob/master/docs/translations.md
messagesLanguage: 'en'
@ -161,6 +151,8 @@ settings:
# AllowedRestrictedUser:
# - playername;127.0.0.1
AllowedRestrictedUser: []
# Ban unknown IPs trying to log in with a restricted username?
banUnsafedIP: false
# Should unregistered players be kicked immediately?
kickNonRegistered: false
# Should players be kicked on wrong password?
@ -177,7 +169,7 @@ settings:
# After how many seconds should players who fail to login or register
# be kicked? Set to 0 to disable.
timeout: 30
# Regex syntax of allowed characters in the player name.
# Regex pattern of allowed characters in the player name.
allowedNicknameCharacters: '[a-zA-Z0-9_]*'
# How far can unregistered players walk?
# Set to 0 for unlimited radius
@ -189,8 +181,6 @@ settings:
# Should we display all other accounts from a player when he joins?
# permission: /authme.admin.accounts
displayOtherAccounts: true
# Ban ip when the ip is not the ip registered in database
banUnsafedIP: false
# Spawn priority; values: authme, essentials, multiverse, default
spawnPriority: 'authme,essentials,multiverse,default'
# Maximum Login authorized by IP
@ -223,18 +213,6 @@ settings:
minPasswordLength: 5
# Maximum length of password
passwordMaxLength: 30
# This is a very important option: every time a player joins the server,
# if they are registered, AuthMe will switch him to unLoggedInGroup.
# This should prevent all major exploits.
# You can set up your permission plugin with this special group to have no permissions,
# or only permission to chat (or permission to send private messages etc.).
# The better way is to set up this group with few permissions, so if a player
# tries to exploit an account they can do only what you've defined for the group.
# After, a logged in player will be moved to his correct permissions group!
# Please note that the group name is case-sensitive, so 'admin' is different from 'Admin'
# Otherwise your group will be wiped and the player will join in the default group []!
# Example unLoggedinGroup: NotLogged
unLoggedinGroup: 'unLoggedinGroup'
# Possible values: SHA256, BCRYPT, BCRYPT2Y, PBKDF2, SALTEDSHA512, WHIRLPOOL,
# MYBB, IPB3, PHPBB, PHPFUSION, SMF, XENFORO, XAUTH, JOOMLA, WBB3, WBB4, MD5VB,
# PBKDF2DJANGO, WORDPRESS, ROYALAUTH, CUSTOM (for developers only). See full list at
@ -317,17 +295,31 @@ settings:
# Do we need to prevent people to login with another case?
# If Xephi is registered, then Xephi can login, but not XEPHI/xephi/XePhI
preventOtherCase: true
permission:
# Take care with this option; if you want
# to use group switching of AuthMe
# for unloggedIn players, set this setting to true.
# Default is false.
EnablePermissionCheck: false
GroupOptions:
# Enables switching a player to defined permission groups before they log in.
# See below for a detailed explanation.
enablePermissionCheck: false
# This is a very important option: if a registered player joins the server
# AuthMe will switch him to unLoggedInGroup. This should prevent all major exploits.
# You can set up your permission plugin with this special group to have no permissions,
# or only permission to chat (or permission to send private messages etc.).
# The better way is to set up this group with few permissions, so if a player
# tries to exploit an account they can do only what you've defined for the group.
# After login, the player will be moved to his correct permissions group!
# Please note that the group name is case-sensitive, so 'admin' is different from 'Admin'
# Otherwise your group will be wiped and the player will join in the default group []!
# Example: registeredPlayerGroup: 'NotLogged'
registeredPlayerGroup: ''
# Similar to above, unregistered players can be set to the following
# permissions group
unregisteredPlayerGroup: ''
Email:
# Email SMTP server host
mailSMTP: 'smtp.gmail.com'
# Email SMTP server port
mailPort: 465
# Only affects port 25: enable TLS/STARTTLS?
useTls: true
# Email account which sends the mails
mailAccount: ''
# Email account password
@ -366,18 +358,13 @@ Hooks:
disableSocialSpy: false
# Do we need to force /motd Essentials command on join?
useEssentialsMotd: false
GroupOptions:
# Unregistered permission group
UnregisteredPlayerGroup: ''
# Registered permission group
RegisteredPlayerGroup: ''
Protection:
# Enable some servers protection (country based login, antibot)
enableProtection: false
# Apply the protection also to registered usernames
enableProtectionRegistered: true
# Countries allowed to join the server and register. For country codes, see
# http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/
# https://dev.bukkit.org/projects/authme-reloaded/pages/countries-codes
# PLEASE USE QUOTES!
countries:
- 'US'
@ -432,6 +419,8 @@ Security:
maxLoginTry: 5
# Captcha length
captchaLength: 5
# Minutes after which login attempts count is reset for a player
captchaCountReset: 60
tempban:
# Tempban a user's IP address if they enter the wrong password too many times
enableTempban: false
@ -448,6 +437,53 @@ Security:
length: 8
# How many hours is a recovery code valid for?
validForHours: 4
# Max number of tries to enter recovery code
maxTries: 3
# How long a player has after password recovery to change their password
# without logging in. This is in minutes.
# Default: 2 minutes
passwordChangeTimeout: 2
emailRecovery:
# Seconds a user has to wait for before a password recovery mail may be sent again
# This prevents an attacker from abusing AuthMe's email feature.
cooldown: 60
# Before a user logs in, various properties are temporarily removed from the player,
# such as OP status, ability to fly, and walk/fly speed.
# Once the user is logged in, we add back the properties we previously saved.
# In this section, you may define how these properties should be handled.
limbo:
persistence:
# Besides storing the data in memory, you can define if/how the data should be persisted
# on disk. This is useful in case of a server crash, so next time the server starts we can
# properly restore things like OP status, ability to fly, and walk/fly speed.
# DISABLED: no disk storage, INDIVIDUAL_FILES: each player data in its own file,
# SINGLE_FILE: all data in one single file (only if you have a small server!)
# SEGMENT_FILES: distributes players into different buckets based on their UUID. See below.
type: 'INDIVIDUAL_FILES'
# This setting only affects SEGMENT_FILES persistence. The segment file
# persistence attempts to reduce the number of files by distributing players into various
# buckets based on their UUID. This setting defines into how many files the players should
# be distributed. Possible values: ONE, FOUR, EIGHT, SIXTEEN, THIRTY_TWO, SIXTY_FOUR,
# ONE_TWENTY for 128, TWO_FIFTY for 256.
# For example, if you expect 100 non-logged in players, setting to SIXTEEN will average
# 6.25 players per file (100 / 16). If you set to ONE, like persistence SINGLE_FILE, only
# one file will be used. Contrary to SINGLE_FILE, it won't keep the entries in cache, which
# may deliver different results in terms of performance.
# Note: if you change this setting all data will be migrated. If you have a lot of data,
# change this setting only on server restart, not with /authme reload.
segmentDistribution: 'SIXTEEN'
# Whether the player is allowed to fly: RESTORE, ENABLE, DISABLE.
# RESTORE sets back the old property from the player.
restoreAllowFlight: 'RESTORE'
# Restore fly speed: RESTORE, DEFAULT, MAX_RESTORE, RESTORE_NO_ZERO.
# RESTORE: restore the speed the player had;
# DEFAULT: always set to default speed;
# MAX_RESTORE: take the maximum of the player's current speed and the previous one
# RESTORE_NO_ZERO: Like 'restore' but sets speed to default if the player's speed was 0
restoreFlySpeed: 'MAX_RESTORE'
# Restore walk speed: RESTORE, DEFAULT, MAX_RESTORE, RESTORE_NO_ZERO.
# See above for a description of the values.
restoreWalkSpeed: 'MAX_RESTORE'
BackupSystem:
# Enable or disable automatic backup
ActivateBackup: false
@ -457,6 +493,17 @@ BackupSystem:
OnServerStop: true
# Windows only mysql installation Path
MysqlWindowsPath: 'C:\Program Files\MySQL\MySQL Server 5.1\'
Converter:
Rakamak:
# Rakamak file name
fileName: 'users.rak'
# Rakamak use IP?
useIP: false
# Rakamak IP file name
ipFileName: 'UsersIp.rak'
CrazyLogin:
# CrazyLogin database file name
fileName: 'accounts.db'
```
To change settings on a running server, save your changes to config.yml and use
@ -464,4 +511,4 @@ To change settings on a running server, save your changes to config.yml and use
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sat Jan 14 22:12:16 CET 2017
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Wed Mar 22 23:10:33 CET 2017

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Fri Nov 25 15:48:35 CET 2016. See docs/hashmethods/hash_algorithms.tpl.md -->
<!-- File auto-generated on Sat Mar 25 00:15:27 CET 2017. See docs/hashmethods/hash_algorithms.tpl.md -->
## Hash Algorithms
AuthMe supports the following hash algorithms for storing your passwords safely.
@ -10,11 +10,11 @@ Algorithm | Recommendation | Hash length | ASCII | | Salt type | Length | Se
BCRYPT | Recommended | 60 | | | Text | |
BCRYPT2Y | Recommended | 60 | | | Text | 22 |
CRAZYCRYPT1 | Do not use | 128 | | | Username | |
DOUBLEMD5 | Do not use | 32 | | | None | |
DOUBLEMD5 | Deprecated | 32 | | | None | |
IPB3 | Acceptable | 32 | | | Text | 5 | Y
IPB4 | Does not work | 60 | | | Text | 22 | Y
JOOMLA | Acceptable | 65 | | | Text | 32 |
MD5 | Do not use | 32 | | | None | |
MD5 | Deprecated | 32 | | | None | |
MD5VB | Acceptable | 56 | | | Text | 16 |
MYBB | Acceptable | 32 | | | Text | 8 | Y
PBKDF2 | Recommended | 165 | | | Text | 16 |
@ -24,14 +24,14 @@ PHPFUSION | Do not use | 64 | Y | | | | Y
ROYALAUTH | Do not use | 128 | | | None | |
SALTED2MD5 | Acceptable | 32 | | | Text | | Y
SALTEDSHA512 | Recommended | 128 | | | | | Y
SHA1 | Do not use | 40 | | | None | |
SHA1 | Deprecated | 40 | | | None | |
SHA256 | Recommended | 86 | | | Text | 16 |
SHA512 | Do not use | 128 | | | None | |
SHA512 | Deprecated | 128 | | | None | |
SMF | Do not use | 40 | | | Username | |
TWO_FACTOR | Does not work | 16 | | | None | |
WBB3 | Acceptable | 40 | | | Text | 40 | Y
WBB4 | Recommended | 60 | | | Text | 8 |
WHIRLPOOL | Do not use | 128 | | | None | |
WHIRLPOOL | Deprecated | 128 | | | None | |
WORDPRESS | Acceptable | 34 | | | Text | 9 |
XAUTH | Recommended | 140 | | | Text | 12 |
XFBCRYPT | | 60 | | | | |
@ -82,4 +82,4 @@ or bad.
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Nov 25 15:48:35 CET 2016
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sat Mar 25 00:15:27 CET 2017

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Wed Jan 11 21:24:50 CET 2017. See docs/translations/translations.tpl.md -->
<!-- File auto-generated on Wed Mar 22 23:10:32 CET 2017. See docs/translations/translations.tpl.md -->
# AuthMe Translations
The following translations are available in AuthMe. Set `messagesLanguage` to the language code
@ -8,35 +8,34 @@ in your config.yml to use the language, or use another language code to start a
Code | Language | Translated | &nbsp;
---- | -------- | ---------: | ------
[en](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_en.yml) | English | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 69% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=69&h=5&txtpad=1" alt="bar" />
[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 62% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=62&h=5&txtpad=1" alt="bar" />
[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 66% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=66&h=5&txtpad=1" alt="bar" />
[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 70% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=70&h=5&txtpad=1" alt="bar" />
[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 99% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=99&h=5&txtpad=1" alt="bar" />
[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 70% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=70&h=5&txtpad=1" alt="bar" />
[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 72% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=72&h=5&txtpad=1" alt="bar" />
[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 53% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=53&h=5&txtpad=1" alt="bar" />
[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 77% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=77&h=5&txtpad=1" alt="bar" />
[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 86% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=86&h=5&txtpad=1" alt="bar" />
[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 99% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=99&h=5&txtpad=1" alt="bar" />
[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 99% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=99&h=5&txtpad=1" alt="bar" />
[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 46% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=46&h=5&txtpad=1" alt="bar" />
[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 81% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=81&h=5&txtpad=1" alt="bar" />
[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 93% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=93&h=5&txtpad=1" alt="bar" />
[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 81% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=81&h=5&txtpad=1" alt="bar" />
[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 81% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=81&h=5&txtpad=1" alt="bar" />
[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 96% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=96&h=5&txtpad=1" alt="bar" />
[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 81% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=81&h=5&txtpad=1" alt="bar" />
[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 85% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=85&h=5&txtpad=1" alt="bar" />
[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 85% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=85&h=5&txtpad=1" alt="bar" />
[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 85% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=85&h=5&txtpad=1" alt="bar" />
[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 53% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=53&h=5&txtpad=1" alt="bar" />
[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 56% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=56&h=5&txtpad=1" alt="bar" />
[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 60% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=60&h=5&txtpad=1" alt="bar" />
[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 84% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=84&h=5&txtpad=1" alt="bar" />
[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 60% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=60&h=5&txtpad=1" alt="bar" />
[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 61% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=61&h=5&txtpad=1" alt="bar" />
[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 45% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=45&h=5&txtpad=1" alt="bar" />
[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 85% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=85&h=5&txtpad=1" alt="bar" />
[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 84% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=84&h=5&txtpad=1" alt="bar" />
[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 39% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa4400&w=39&h=5&txtpad=1" alt="bar" />
[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 79% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=79&h=5&txtpad=1" alt="bar" />
[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 85% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=85&h=5&txtpad=1" alt="bar" />
[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 95% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=95&h=5&txtpad=1" alt="bar" />
[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 69% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=69&h=5&txtpad=1" alt="bar" />
[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 82% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=82&h=5&txtpad=1" alt="bar" />
[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 69% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=69&h=5&txtpad=1" alt="bar" />
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Wed Jan 11 21:24:50 CET 2017
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Wed Mar 22 23:10:32 CET 2017

58
pom.xml
View File

@ -6,7 +6,7 @@
<groupId>fr.xephi</groupId>
<artifactId>authme</artifactId>
<version>5.2</version>
<version>5.3-SNAPSHOT</version>
<name>AuthMeReloaded</name>
<description>The first authentication plugin for the Bukkit API!</description>
@ -108,10 +108,6 @@
<artifactId>junit</artifactId>
<groupId>junit</groupId>
</exclusion>
<exclusion>
<artifactId>json-simple</artifactId>
<groupId>com.googlecode.json-simple</groupId>
</exclusion>
<exclusion>
<artifactId>persistence-api</artifactId>
<groupId>javax.persistence</groupId>
@ -146,10 +142,6 @@
<artifactId>junit</artifactId>
<groupId>junit</groupId>
</exclusion>
<exclusion>
<artifactId>json-simple</artifactId>
<groupId>com.googlecode.json-simple</groupId>
</exclusion>
<exclusion>
<artifactId>persistence-api</artifactId>
<groupId>javax.persistence</groupId>
@ -249,8 +241,8 @@
<shadedPattern>fr.xephi.authme.libs.jalu.injector</shadedPattern>
</relocation>
<relocation>
<pattern>com.github.authme.configme</pattern>
<shadedPattern>fr.xephi.authme.libs.authme.configme</shadedPattern>
<pattern>ch.jalu.configme</pattern>
<shadedPattern>fr.xephi.authme.libs.jalu.configme</shadedPattern>
</relocation>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
@ -276,10 +268,10 @@
<pattern>javax.inject</pattern>
<shadedPattern>fr.xephi.authme.libs.javax.inject</shadedPattern>
</relocation>
<!-- MCStats.org metrics -->
<!-- bStats metrics class -->
<relocation>
<pattern>org.mcstats</pattern>
<shadedPattern>fr.xephi.authme</shadedPattern>
<pattern>org.bstats</pattern>
<shadedPattern>fr.xephi.authme.libs.org.bstats</shadedPattern>
</relocation>
</relocations>
<outputFile>target/${project.finalName}-spigot.jar</outputFile>
@ -331,10 +323,10 @@
<pattern>javax.inject</pattern>
<shadedPattern>fr.xephi.authme.libs.javax.inject</shadedPattern>
</relocation>
<!-- MCStats.org metrics -->
<!-- bStats metrics class -->
<relocation>
<pattern>org.mcstats</pattern>
<shadedPattern>fr.xephi.authme</shadedPattern>
<pattern>org.bstats</pattern>
<shadedPattern>fr.xephi.authme.libs.org.bstats</shadedPattern>
</relocation>
</relocations>
<outputFile>target/${project.finalName}-legacy.jar</outputFile>
@ -442,6 +434,12 @@
<id>xephi-repo</id>
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
</repository>
<!-- bStats Repo -->
<repository>
<id>bstats-repo</id>
<url>http://repo.bstats.org/content/groups/public</url>
</repository>
</repositories>
<dependencies>
@ -451,7 +449,7 @@
<dependency>
<groupId>ch.jalu</groupId>
<artifactId>injector</artifactId>
<version>0.3</version>
<version>0.4</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -513,7 +511,7 @@
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.5.1</version>
<version>2.6.0</version>
<scope>compile</scope>
<exclusions>
<exclusion>
@ -527,7 +525,7 @@
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
<version>1.7.22</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -544,19 +542,11 @@
<!-- Bukkit Libraries -->
<!-- Metrics API -->
<!-- bStats metrics -->
<dependency>
<groupId>org.mcstats.bukkit</groupId>
<artifactId>metrics</artifactId>
<version>R8-SNAPSHOT</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
</exclusion>
</exclusions>
<optional>true</optional>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>1.0</version>
</dependency>
<!-- ProtocolLib -->
@ -902,7 +892,7 @@
<dependency>
<groupId>ch.jalu</groupId>
<artifactId>configme</artifactId>
<version>0.3</version>
<version>0.4</version>
<scope>compile</scope>
<optional>true</optional>
<exclusions>
@ -933,7 +923,7 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
<version>2.4.1</version>
<version>2.7.9</version>
<exclusions>
<exclusion>
<artifactId>hamcrest-core</artifactId>

View File

@ -14,6 +14,8 @@ import fr.xephi.authme.initialization.OnShutdownPlayerSaver;
import fr.xephi.authme.initialization.OnStartupTasks;
import fr.xephi.authme.initialization.SettingsProvider;
import fr.xephi.authme.initialization.TaskCloser;
import fr.xephi.authme.initialization.factory.FactoryDependencyHandler;
import fr.xephi.authme.initialization.factory.SingletonStoreDependencyHandler;
import fr.xephi.authme.listener.BlockListener;
import fr.xephi.authme.listener.EntityListener;
import fr.xephi.authme.listener.PlayerListener;
@ -24,22 +26,22 @@ import fr.xephi.authme.listener.PlayerListener19;
import fr.xephi.authme.listener.ServerListener;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PermissionsSystemType;
import fr.xephi.authme.security.crypts.SHA256;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.Sha256;
import fr.xephi.authme.service.BackupService;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.GeoIpService;
import fr.xephi.authme.service.MigrationService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.task.CleanupTask;
import fr.xephi.authme.task.purge.PurgeService;
import fr.xephi.authme.util.PlayerUtils;
import org.apache.commons.lang.SystemUtils;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@ -72,8 +74,6 @@ public class AuthMe extends JavaPlugin {
private DataSource database;
private BukkitService bukkitService;
private Injector injector;
private GeoIpService geoIpService;
private PlayerCache playerCache;
/**
* Constructor.
@ -139,6 +139,7 @@ public class AuthMe extends JavaPlugin {
initialize();
} catch (Exception e) {
ConsoleLogger.logException("Aborting initialization of AuthMe:", e);
OnStartupTasks.displayLegacyJarHint(e);
stopOrUnload();
return;
}
@ -148,7 +149,8 @@ public class AuthMe extends JavaPlugin {
// If server is using PermissionsBukkit, print a warning that some features may not be supported
if (PermissionsSystemType.PERMISSIONS_BUKKIT.equals(permsMan.getPermissionSystem())) {
ConsoleLogger.warning("Warning! This server uses PermissionsBukkit for permissions. Some permissions features may not be supported!");
ConsoleLogger.warning("Warning! This server uses PermissionsBukkit for permissions. Some permissions "
+ "features may not be supported!");
}
// Do a backup on start
@ -159,10 +161,12 @@ public class AuthMe extends JavaPlugin {
// Sponsor messages
ConsoleLogger.info("Development builds are available on our jenkins, thanks to f14stelt.");
ConsoleLogger.info("Do you want a good game server? Look at our sponsor GameHosting.it leader in Italy as Game Server Provider!");
ConsoleLogger.info("Do you want a good game server? Look at our sponsor GameHosting.it leader "
+ "in Italy as Game Server Provider!");
// Successful message
ConsoleLogger.info("AuthMe " + getPluginVersion() + " build n." + getPluginBuildNumber() + " correctly enabled!");
ConsoleLogger.info("AuthMe " + getPluginVersion() + " build n." + getPluginBuildNumber()
+ " correctly enabled!");
// Purge on start if enabled
PurgeService purgeService = injector.getSingleton(PurgeService.class);
@ -197,11 +201,19 @@ public class AuthMe extends JavaPlugin {
ConsoleLogger.setLogger(getLogger());
ConsoleLogger.setLogFile(new File(getDataFolder(), LOG_FILENAME));
// Check java version
if(!SystemUtils.isJavaVersionAtLeast(1.8f)) {
throw new IllegalStateException("You need Java 1.8 or above to run this plugin!");
}
// Create plugin folder
getDataFolder().mkdir();
// Create injector, provide elements from the Bukkit environment and register providers
injector = new InjectorBuilder().addDefaultHandlers("fr.xephi.authme").create();
injector = new InjectorBuilder()
.addHandlers(new FactoryDependencyHandler(), new SingletonStoreDependencyHandler())
.addDefaultHandlers("fr.xephi.authme")
.create();
injector.register(AuthMe.class, this);
injector.register(Server.class, getServer());
injector.register(PluginManager.class, getServer().getPluginManager());
@ -219,7 +231,7 @@ public class AuthMe extends JavaPlugin {
instantiateServices(injector);
// Convert deprecated PLAINTEXT hash entries
MigrationService.changePlainTextToSha256(settings, database, new SHA256());
MigrationService.changePlainTextToSha256(settings, database, new Sha256());
// TODO: does this still make sense? -sgdc3
// If the server is empty (fresh start) just set all the players as unlogged
@ -240,16 +252,15 @@ public class AuthMe extends JavaPlugin {
*
* @param injector the injector
*/
protected void instantiateServices(Injector injector) {
void instantiateServices(Injector injector) {
// PlayerCache is still injected statically sometimes
playerCache = PlayerCache.getInstance();
PlayerCache playerCache = PlayerCache.getInstance();
injector.register(PlayerCache.class, playerCache);
database = injector.getSingleton(DataSource.class);
permsMan = injector.getSingleton(PermissionsManager.class);
bukkitService = injector.getSingleton(BukkitService.class);
commandHandler = injector.getSingleton(CommandHandler.class);
geoIpService = injector.getSingleton(GeoIpService.class);
// Trigger construction of API classes; they will keep track of the singleton
injector.getSingleton(NewAPI.class);
@ -270,6 +281,19 @@ public class AuthMe extends JavaPlugin {
&& settings.getProperty(PluginSettings.SESSIONS_ENABLED)) {
ConsoleLogger.warning("WARNING!!! You set session timeout to 0, this may cause security issues!");
}
// Use TLS property only affects port 25
if (!settings.getProperty(EmailSettings.PORT25_USE_TLS)
&& settings.getProperty(EmailSettings.SMTP_PORT) != 25) {
ConsoleLogger.warning("Note: You have set Email.useTls to false but this only affects mail over port 25");
}
// Unsalted hashes will be deprecated in 5.4 (see Github issue #1016)
HashAlgorithm hash = settings.getProperty(SecuritySettings.PASSWORD_HASH);
if (OnStartupTasks.isHashDeprecatedIn54(hash)) {
ConsoleLogger.warning("You are using an unsalted hash (" + hash + "). Support for this will be removed "
+ "in 5.4 -- do you still need it? Comment on https://github.com/Xephi/AuthMeReloaded/issues/1016");
}
}
/**
@ -277,7 +301,7 @@ public class AuthMe extends JavaPlugin {
*
* @param injector the injector
*/
protected void registerEventListeners(Injector injector) {
void registerEventListeners(Injector injector) {
// Get the plugin manager instance
PluginManager pluginManager = getServer().getPluginManager();
@ -344,24 +368,6 @@ public class AuthMe extends JavaPlugin {
ConsoleLogger.close();
}
public String replaceAllInfo(String message, Player player) {
String playersOnline = Integer.toString(bukkitService.getOnlinePlayers().size());
String ipAddress = PlayerUtils.getPlayerIp(player);
Server server = getServer();
return message
.replace("&", "\u00a7")
.replace("{PLAYER}", player.getName())
.replace("{ONLINE}", playersOnline)
.replace("{MAXPLAYERS}", Integer.toString(server.getMaxPlayers()))
.replace("{IP}", ipAddress)
.replace("{LOGINS}", Integer.toString(playerCache.getLogged()))
.replace("{WORLD}", player.getWorld().getName())
.replace("{SERVER}", server.getServerName())
.replace("{VERSION}", server.getBukkitVersion())
// TODO: We should cache info like this, maybe with a class that extends Player?
.replace("{COUNTRY}", geoIpService.getCountryName(ipAddress));
}
/**
* Handle Bukkit commands.
*

View File

@ -12,7 +12,10 @@ import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
@ -89,6 +92,18 @@ public final class ConsoleLogger {
writeLog("[WARN] " + message);
}
/**
* Log a Throwable with the provided message on WARNING level
* and save the stack trace to the log file.
*
* @param message The message to accompany the exception
* @param th The Throwable to log
*/
public static void logException(String message, Throwable th) {
warning(message + " " + StringUtils.formatException(th));
writeLog(Throwables.getStackTraceAsString(th));
}
/**
* Log an INFO message.
*
@ -114,6 +129,10 @@ public final class ConsoleLogger {
}
}
// --------
// Debug log methods
// --------
/**
* Log a DEBUG message if enabled.
* <p>
@ -124,21 +143,78 @@ public final class ConsoleLogger {
*/
public static void debug(String message) {
if (logLevel.includes(LogLevel.DEBUG)) {
logger.info("Debug: " + message);
writeLog("[DEBUG] " + message);
String debugMessage = "[DEBUG] " + message;
logger.info(debugMessage);
writeLog(debugMessage);
}
}
/**
* Log a Throwable with the provided message on WARNING level
* and save the stack trace to the log file.
* Log the DEBUG message from the supplier if enabled.
*
* @param message The message to accompany the exception
* @param th The Throwable to log
* @param msgSupplier the message supplier
*/
public static void logException(String message, Throwable th) {
warning(message + " " + StringUtils.formatException(th));
writeLog(Throwables.getStackTraceAsString(th));
public static void debug(Supplier<String> msgSupplier) {
if (logLevel.includes(LogLevel.DEBUG)) {
String debugMessage = "[DEBUG] " + msgSupplier.get();
logger.info(debugMessage);
writeLog(debugMessage);
}
}
/**
* Log the DEBUG message.
*
* @param message the message
* @param param1 parameter to replace in the message
*/
public static void debug(String message, String param1) {
if (logLevel.includes(LogLevel.DEBUG)) {
String debugMessage = "[DEBUG] " + message;
logger.log(Level.INFO, debugMessage, param1);
writeLog(debugMessage + " {" + param1 + "}");
}
}
/**
* Log the DEBUG message.
*
* @param message the message
* @param param1 first param to replace in message
* @param param2 second param to replace in message
*/
// Avoids array creation if DEBUG level is disabled
public static void debug(String message, String param1, String param2) {
if (logLevel.includes(LogLevel.DEBUG)) {
debug(message, new String[]{param1, param2});
}
}
/**
* Log the DEBUG message.
*
* @param message the message
* @param params the params to replace in the message
*/
// Equivalent to debug(String, Object...) but avoids conversions
public static void debug(String message, String... params) {
if (logLevel.includes(LogLevel.DEBUG)) {
String debugMessage = "[DEBUG] " + message;
logger.log(Level.INFO, debugMessage, params);
writeLog(debugMessage + " {" + String.join(", ", params) + "}");
}
}
/**
* Log the DEBUG message.
*
* @param message the message
* @param params the params to replace in the message
*/
public static void debug(String message, Object... params) {
if (logLevel.includes(LogLevel.DEBUG)) {
debug(message, Arrays.stream(params).map(String::valueOf).toArray(String[]::new));
}
}

View File

@ -23,6 +23,7 @@ import javax.inject.Inject;
* @deprecated Use {@link NewAPI}
*/
@Deprecated
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class API {
private static AuthMe instance;

View File

@ -5,7 +5,8 @@ import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.process.register.executors.RegistrationExecutorProvider;
import fr.xephi.authme.process.register.executors.ApiPasswordRegisterParams;
import fr.xephi.authme.process.register.executors.RegistrationMethod;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.service.PluginHookService;
@ -24,6 +25,7 @@ import java.util.List;
* NewAPI authmeApi = AuthMe.getApi();
* </code>
*/
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class NewAPI {
private static NewAPI singleton;
@ -34,15 +36,13 @@ public class NewAPI {
private final Management management;
private final ValidationService validationService;
private final PlayerCache playerCache;
private final RegistrationExecutorProvider registrationExecutorProvider;
/*
* Constructor for NewAPI.
*/
@Inject
NewAPI(AuthMe plugin, PluginHookService pluginHookService, DataSource dataSource, PasswordSecurity passwordSecurity,
Management management, ValidationService validationService, PlayerCache playerCache,
RegistrationExecutorProvider registrationExecutorProvider) {
Management management, ValidationService validationService, PlayerCache playerCache) {
this.plugin = plugin;
this.pluginHookService = pluginHookService;
this.dataSource = dataSource;
@ -50,7 +50,6 @@ public class NewAPI {
this.management = management;
this.validationService = validationService;
this.playerCache = playerCache;
this.registrationExecutorProvider = registrationExecutorProvider;
NewAPI.singleton = this;
}
@ -128,7 +127,8 @@ public class NewAPI {
public Location getLastLocation(Player player) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth != null) {
return new Location(Bukkit.getWorld(auth.getWorld()), auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ());
return new Location(Bukkit.getWorld(auth.getWorld()),
auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ());
}
return null;
}
@ -203,8 +203,8 @@ public class NewAPI {
* @param autoLogin Should the player be authenticated automatically after the registration?
*/
public void forceRegister(Player player, String password, boolean autoLogin) {
management.performRegister(player,
registrationExecutorProvider.getPasswordRegisterExecutor(player, password, autoLogin));
management.performRegister(RegistrationMethod.API_REGISTRATION,
ApiPasswordRegisterParams.of(player, password, autoLogin));
}
/**

View File

@ -1,8 +1,8 @@
package fr.xephi.authme.command;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.util.CollectionUtils;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.Utils;
import java.util.ArrayList;
import java.util.List;
@ -19,6 +19,7 @@ import static java.util.Arrays.asList;
* {@code /authme} has a child whose label is {@code "register"}, then {@code /authme register} is the command that
* the child defines.
*/
@SuppressWarnings("checkstyle:FinalClass") // Justification: class is mocked in multiple tests
public class CommandDescription {
/**
@ -224,7 +225,7 @@ public class CommandDescription {
* @return The generated CommandDescription object
*/
public CommandDescription build() {
checkArgument(!CollectionUtils.isEmpty(labels), "Labels may not be empty");
checkArgument(!Utils.isCollectionEmpty(labels), "Labels may not be empty");
checkArgument(!StringUtils.isEmpty(description), "Description may not be empty");
checkArgument(!StringUtils.isEmpty(detailedDescription), "Detailed description may not be empty");
checkArgument(executableCommand != null, "Executable command must be set");

View File

@ -1,8 +1,8 @@
package fr.xephi.authme.command;
import ch.jalu.injector.Injector;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.command.help.HelpProvider;
import fr.xephi.authme.initialization.factory.Factory;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.permission.PermissionsManager;
@ -40,13 +40,13 @@ public class CommandHandler {
private Map<Class<? extends ExecutableCommand>, ExecutableCommand> commands = new HashMap<>();
@Inject
CommandHandler(Injector injector, CommandMapper commandMapper, PermissionsManager permissionsManager,
Messages messages, HelpProvider helpProvider) {
CommandHandler(Factory<ExecutableCommand> commandFactory, CommandMapper commandMapper,
PermissionsManager permissionsManager, Messages messages, HelpProvider helpProvider) {
this.commandMapper = commandMapper;
this.permissionsManager = permissionsManager;
this.messages = messages;
this.helpProvider = helpProvider;
initializeCommands(injector, commandMapper.getCommandClasses());
initializeCommands(commandFactory, commandMapper.getCommandClasses());
}
/**
@ -94,13 +94,13 @@ public class CommandHandler {
/**
* Initialize all required ExecutableCommand objects.
*
* @param injector the injector
* @param commandFactory factory to create command objects
* @param commandClasses the classes to instantiate
*/
private void initializeCommands(Injector injector,
private void initializeCommands(Factory<ExecutableCommand> commandFactory,
Set<Class<? extends ExecutableCommand>> commandClasses) {
for (Class<? extends ExecutableCommand> clazz : commandClasses) {
commands.put(clazz, injector.newInstance(clazz));
commands.put(clazz, commandFactory.newInstance(clazz));
}
}

View File

@ -24,12 +24,15 @@ import fr.xephi.authme.command.executable.authme.SpawnCommand;
import fr.xephi.authme.command.executable.authme.SwitchAntiBotCommand;
import fr.xephi.authme.command.executable.authme.UnregisterAdminCommand;
import fr.xephi.authme.command.executable.authme.VersionCommand;
import fr.xephi.authme.command.executable.authme.debug.DebugCommand;
import fr.xephi.authme.command.executable.captcha.CaptchaCommand;
import fr.xephi.authme.command.executable.changepassword.ChangePasswordCommand;
import fr.xephi.authme.command.executable.email.AddEmailCommand;
import fr.xephi.authme.command.executable.email.ChangeEmailCommand;
import fr.xephi.authme.command.executable.email.EmailBaseCommand;
import fr.xephi.authme.command.executable.email.ProcessCodeCommand;
import fr.xephi.authme.command.executable.email.RecoverEmailCommand;
import fr.xephi.authme.command.executable.email.SetPasswordCommand;
import fr.xephi.authme.command.executable.email.ShowEmailCommand;
import fr.xephi.authme.command.executable.login.LoginCommand;
import fr.xephi.authme.command.executable.logout.LogoutCommand;
@ -37,6 +40,7 @@ import fr.xephi.authme.command.executable.register.RegisterCommand;
import fr.xephi.authme.command.executable.unregister.UnregisterCommand;
import fr.xephi.authme.permission.AdminPermission;
import fr.xephi.authme.permission.PlayerPermission;
import fr.xephi.authme.permission.PlayerStatePermission;
import java.util.Arrays;
import java.util.Collection;
@ -62,6 +66,10 @@ public class CommandInitializer {
return commands;
}
/**
* Builds the command description objects for all available AuthMe commands.
*/
@SuppressWarnings({"checkstyle:LocalVariableName", "checkstyle:AbbreviationAsWordInName"})
private void buildCommands() {
// Register the base AuthMe Reloaded command
final CommandDescription AUTHME_BASE = CommandDescription.builder()
@ -283,8 +291,8 @@ public class CommandInitializer {
.labels("converter", "convert", "conv")
.description("Converter command")
.detailedDescription("Converter command for AuthMeReloaded.")
.withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " +
"royalauth / vauth / sqliteToSql / mysqlToSqlite", false)
.withArgument("job", "Conversion job: xauth / crazylogin / rakamak / "
+ "royalauth / vauth / sqliteToSql / mysqlToSqlite", false)
.permission(AdminPermission.CONVERTER)
.executableCommand(ConverterCommand.class)
.register();
@ -298,6 +306,19 @@ public class CommandInitializer {
.executableCommand(MessagesCommand.class)
.register();
CommandDescription.builder()
.parent(AUTHME_BASE)
.labels("debug", "dbg")
.description("Debug features")
.detailedDescription("Allows various operations for debugging.")
.withArgument("child", "The child to execute", true)
.withArgument(".", "meaning varies", true)
.withArgument(".", "meaning varies", true)
.withArgument(".", "meaning varies", true)
.permission(PlayerStatePermission.DEBUG_COMMAND)
.executableCommand(DebugCommand.class)
.register();
// Register the base login command
final CommandDescription LOGIN_BASE = CommandDescription.builder()
.parent(null)
@ -401,14 +422,35 @@ public class CommandInitializer {
.parent(EMAIL_BASE)
.labels("recover", "recovery", "recoveremail", "recovermail")
.description("Recover password using email")
.detailedDescription("Recover your account using an Email address by sending a mail containing " +
"a new password.")
.detailedDescription("Recover your account using an Email address by sending a mail containing "
+ "a new password.")
.withArgument("email", "Email address", false)
.withArgument("code", "Recovery code", true)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(RecoverEmailCommand.class)
.register();
// Register the process recovery code command
CommandDescription.builder()
.parent(EMAIL_BASE)
.labels("code")
.description("Submit code to recover password")
.detailedDescription("Recover your account by submitting a code delivered to your email.")
.withArgument("code", "Recovery code", false)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(ProcessCodeCommand.class)
.register();
// Register the change password after recovery command
CommandDescription.builder()
.parent(EMAIL_BASE)
.labels("setpassword")
.description("Set new password after recovery")
.detailedDescription("Set a new password after successfully recovering your account.")
.withArgument("password", "New password", false)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(SetPasswordCommand.class)
.register();
// Register the base captcha command
CommandDescription CAPTCHA_BASE = CommandDescription.builder()
.parent(null)

View File

@ -2,8 +2,8 @@ package fr.xephi.authme.command;
import fr.xephi.authme.command.executable.HelpCommand;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.util.CollectionUtils;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
@ -45,7 +45,7 @@ public class CommandMapper {
* @return The generated {@link FoundCommandResult}
*/
public FoundCommandResult mapPartsToCommand(CommandSender sender, final List<String> parts) {
if (CollectionUtils.isEmpty(parts)) {
if (Utils.isCollectionEmpty(parts)) {
return new FoundCommandResult(null, parts, null, 0.0, MISSING_BASE_COMMAND);
}
@ -123,6 +123,9 @@ public class CommandMapper {
private CommandDescription getBaseCommand(String label) {
String baseLabel = label.toLowerCase();
if (baseLabel.startsWith("authme:")) {
baseLabel = baseLabel.substring("authme:".length());
}
for (CommandDescription command : baseCommands) {
if (command.hasLabel(baseLabel)) {
return command;
@ -142,7 +145,7 @@ public class CommandMapper {
* @return A command if there was a complete match (including proper argument count), null otherwise
*/
private static CommandDescription getSuitableChild(CommandDescription baseCommand, List<String> parts) {
if (CollectionUtils.isEmpty(parts)) {
if (Utils.isCollectionEmpty(parts)) {
return null;
}

View File

@ -19,7 +19,7 @@ public interface ExecutableCommand {
void executeCommand(CommandSender sender, List<String> arguments);
/**
* Returns the message to show to the user if the command is used with the wrong commands.
* Returns the message to show to the user if the command is used with the wrong arguments.
* If null is returned, the standard help (/<i>command</i> help) output is shown.
*
* @return the message explaining the command's usage, or {@code null} for default behavior

View File

@ -1,6 +1,5 @@
package fr.xephi.authme.command.executable.authme;
import ch.jalu.injector.Injector;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import fr.xephi.authme.ConsoleLogger;
@ -11,8 +10,9 @@ import fr.xephi.authme.datasource.converter.MySqlToSqlite;
import fr.xephi.authme.datasource.converter.RakamakConverter;
import fr.xephi.authme.datasource.converter.RoyalAuthConverter;
import fr.xephi.authme.datasource.converter.SqliteToSql;
import fr.xephi.authme.datasource.converter.vAuthConverter;
import fr.xephi.authme.datasource.converter.xAuthConverter;
import fr.xephi.authme.datasource.converter.VAuthConverter;
import fr.xephi.authme.datasource.converter.XAuthConverter;
import fr.xephi.authme.initialization.factory.Factory;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
@ -37,7 +37,7 @@ public class ConverterCommand implements ExecutableCommand {
private BukkitService bukkitService;
@Inject
private Injector injector;
private Factory<Converter> converterFactory;
@Override
public void executeCommand(final CommandSender sender, List<String> arguments) {
@ -52,7 +52,7 @@ public class ConverterCommand implements ExecutableCommand {
}
// Get the proper converter instance
final Converter converter = injector.newInstance(converterClass);
final Converter converter = converterFactory.newInstance(converterClass);
// Run the convert job
bukkitService.runTaskAsynchronously(new Runnable() {
@ -78,11 +78,11 @@ public class ConverterCommand implements ExecutableCommand {
*/
private static Map<String, Class<? extends Converter>> getConverters() {
return ImmutableMap.<String, Class<? extends Converter>>builder()
.put("xauth", xAuthConverter.class)
.put("xauth", XAuthConverter.class)
.put("crazylogin", CrazyLoginConverter.class)
.put("rakamak", RakamakConverter.class)
.put("royalauth", RoyalAuthConverter.class)
.put("vauth", vAuthConverter.class)
.put("vauth", VAuthConverter.class)
.put("sqlitetosql", SqliteToSql.class)
.put("mysqltosqlite", MySqlToSqlite.class)
.build();

View File

@ -1,9 +1,8 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.limbo.LimboCache;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.security.PasswordSecurity;
@ -38,9 +37,6 @@ public class RegisterAdminCommand implements ExecutableCommand {
@Inject
private ValidationService validationService;
@Inject
private LimboCache limboCache;
@Override
public void executeCommand(final CommandSender sender, List<String> arguments) {
// Get the player name and password
@ -83,7 +79,6 @@ public class RegisterAdminCommand implements ExecutableCommand {
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(new Runnable() {
@Override
public void run() {
limboCache.restoreData(player);
player.kickPlayer(commonService.retrieveSingleMessage(MessageKey.KICK_FOR_ADMIN_REGISTER));
}
});

View File

@ -1,20 +1,20 @@
package fr.xephi.authme.command.executable.authme;
import ch.jalu.injector.Injector;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.initialization.factory.SingletonStore;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Collection;
import java.util.List;
/**
@ -25,9 +25,6 @@ public class ReloadCommand implements ExecutableCommand {
@Inject
private AuthMe plugin;
@Inject
private Injector injector;
@Inject
private Settings settings;
@ -37,6 +34,12 @@ public class ReloadCommand implements ExecutableCommand {
@Inject
private CommonService commonService;
@Inject
private SingletonStore<Reloadable> reloadableStore;
@Inject
private SingletonStore<SettingsDependent> settingsDependentStore;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
try {
@ -44,8 +47,7 @@ public class ReloadCommand implements ExecutableCommand {
ConsoleLogger.setLoggingOptions(settings);
// We do not change database type for consistency issues, but we'll output a note in the logs
if (!settings.getProperty(DatabaseSettings.BACKEND).equals(dataSource.getType())) {
ConsoleLogger.info("Note: cannot change database type during /authme reload");
sender.sendMessage("Note: cannot change database type during /authme reload");
Utils.logAndSendMessage(sender, "Note: cannot change database type during /authme reload");
}
performReloadOnServices();
commonService.send(sender, MessageKey.CONFIG_RELOAD_SUCCESS);
@ -57,14 +59,10 @@ public class ReloadCommand implements ExecutableCommand {
}
private void performReloadOnServices() {
Collection<Reloadable> reloadables = injector.retrieveAllOfType(Reloadable.class);
for (Reloadable reloadable : reloadables) {
reloadable.reload();
}
reloadableStore.retrieveAllOfType()
.forEach(r -> r.reload());
Collection<SettingsDependent> settingsDependents = injector.retrieveAllOfType(SettingsDependent.class);
for (SettingsDependent dependent : settingsDependents) {
dependent.reload(settings);
}
settingsDependentStore.retrieveAllOfType()
.forEach(s -> s.reload(settings));
}
}

View File

@ -31,12 +31,12 @@ public class VersionCommand implements ExecutableCommand {
printDeveloper(sender, "games647", "games647", "Developer", onlinePlayers);
printDeveloper(sender, "Tim Visee", "timvisee", "Developer", onlinePlayers);
printDeveloper(sender, "Gabriele C.", "sgdc3", "Project manager, Contributor", onlinePlayers);
sender.sendMessage(ChatColor.GOLD + "Website: " + ChatColor.WHITE +
"http://dev.bukkit.org/bukkit-plugins/authme-reloaded/");
sender.sendMessage(ChatColor.GOLD + "Website: " + ChatColor.WHITE
+ "http://dev.bukkit.org/bukkit-plugins/authme-reloaded/");
sender.sendMessage(ChatColor.GOLD + "License: " + ChatColor.WHITE + "GNU GPL v3.0"
+ ChatColor.GRAY + ChatColor.ITALIC + " (See LICENSE file)");
sender.sendMessage(ChatColor.GOLD + "Copyright: " + ChatColor.WHITE
+ "Copyright (c) AuthMe-Team 2016. All rights reserved.");
+ "Copyright (c) AuthMe-Team 2017. All rights reserved.");
}
/**

View File

@ -0,0 +1,77 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.service.GeoIpService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.settings.properties.ProtectionSettings;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.regex.Pattern;
/**
* Shows the GeoIP information as returned by the geoIpService.
*/
class CountryLookup implements DebugSection {
private static final Pattern IS_IP_ADDR = Pattern.compile("(\\d{1,3}\\.){3}\\d{1,3}");
@Inject
private GeoIpService geoIpService;
@Inject
private DataSource dataSource;
@Inject
private ValidationService validationService;
@Override
public String getName() {
return "cty";
}
@Override
public String getDescription() {
return "Check country protection / country data";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage("Check player: /authme debug cty Bobby");
sender.sendMessage("Check IP address: /authme debug cty 127.123.45.67");
return;
}
String argument = arguments.get(0);
if (IS_IP_ADDR.matcher(argument).matches()) {
outputInfoForIpAddr(sender, argument);
} else {
outputInfoForPlayer(sender, argument);
}
}
private void outputInfoForIpAddr(CommandSender sender, String ipAddr) {
sender.sendMessage("IP '" + ipAddr + "' maps to country '" + geoIpService.getCountryCode(ipAddr)
+ "' (" + geoIpService.getCountryName(ipAddr) + ")");
if (validationService.isCountryAdmitted(ipAddr)) {
sender.sendMessage(ChatColor.DARK_GREEN + "This IP address' country is not blocked");
} else {
sender.sendMessage(ChatColor.DARK_RED + "This IP address' country is blocked from the server");
}
sender.sendMessage("Note: if " + ProtectionSettings.ENABLE_PROTECTION + " is false no country is blocked");
}
private void outputInfoForPlayer(CommandSender sender, String name) {
PlayerAuth auth = dataSource.getAuth(name);
if (auth == null) {
sender.sendMessage("No player with name '" + name + "'");
} else {
sender.sendMessage("Player '" + name + "' has IP address " + auth.getIp());
outputInfoForIpAddr(sender, auth.getIp());
}
}
}

View File

@ -0,0 +1,72 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.CacheDataSource;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.initialization.factory.SingletonStore;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap;
/**
* Fetches various statistics, particularly regarding in-memory data that is stored.
*/
class DataStatistics implements DebugSection {
@Inject
private PlayerCache playerCache;
@Inject
private LimboService limboService;
@Inject
private DataSource dataSource;
@Inject
private SingletonStore<Object> singletonStore;
@Override
public String getName() {
return "stats";
}
@Override
public String getDescription() {
return "Outputs general data statistics";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
sender.sendMessage("LimboPlayers in memory: " + applyToLimboPlayersMap(limboService, Map::size));
sender.sendMessage("PlayerCache size: " + playerCache.getLogged() + " (= logged in players)");
outputDatabaseStats(sender);
outputInjectorStats(sender);
}
private void outputDatabaseStats(CommandSender sender) {
sender.sendMessage("Total players in DB: " + dataSource.getAccountsRegistered());
sender.sendMessage("Total marked as logged in in DB: " + dataSource.getLoggedPlayers().size());
if (dataSource instanceof CacheDataSource) {
CacheDataSource cacheDataSource = (CacheDataSource) this.dataSource;
sender.sendMessage("Cached PlayerAuth objects: " + cacheDataSource.getCachedAuths().size());
}
}
private void outputInjectorStats(CommandSender sender) {
sender.sendMessage(
String.format("Singleton Java classes: %d (Reloadable: %d / SettingsDependent: %d / HasCleanup: %d)",
singletonStore.retrieveAllOfType().size(),
singletonStore.retrieveAllOfType(Reloadable.class).size(),
singletonStore.retrieveAllOfType(SettingsDependent.class).size(),
singletonStore.retrieveAllOfType(HasCleanup.class).size()));
}
}

View File

@ -0,0 +1,60 @@
package fr.xephi.authme.command.executable.authme.debug;
import com.google.common.collect.ImmutableSet;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.initialization.factory.Factory;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Debug command main.
*/
public class DebugCommand implements ExecutableCommand {
private static final Set<Class<? extends DebugSection>> SECTION_CLASSES = ImmutableSet.of(
PermissionGroups.class, DataStatistics.class, CountryLookup.class, PlayerAuthViewer.class, InputValidator.class,
LimboPlayerViewer.class, CountryLookup.class, HasPermissionChecker.class, TestEmailSender.class,
SpawnLocationViewer.class);
@Inject
private Factory<DebugSection> debugSectionFactory;
private Map<String, DebugSection> sections;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
DebugSection debugSection = getDebugSection(arguments);
if (debugSection == null) {
sender.sendMessage("Available sections:");
getSections().values()
.forEach(e -> sender.sendMessage("- " + e.getName() + ": " + e.getDescription()));
} else {
debugSection.execute(sender, arguments.subList(1, arguments.size()));
}
}
private DebugSection getDebugSection(List<String> arguments) {
if (arguments.isEmpty()) {
return null;
}
return getSections().get(arguments.get(0).toLowerCase());
}
// Lazy getter
private Map<String, DebugSection> getSections() {
if (sections == null) {
Map<String, DebugSection> sections = new TreeMap<>();
for (Class<? extends DebugSection> sectionClass : SECTION_CLASSES) {
DebugSection section = debugSectionFactory.newInstance(sectionClass);
sections.put(section.getName(), section);
}
this.sections = sections;
}
return sections;
}
}

View File

@ -0,0 +1,30 @@
package fr.xephi.authme.command.executable.authme.debug;
import org.bukkit.command.CommandSender;
import java.util.List;
/**
* A debug section: "child" command of the debug command.
*/
interface DebugSection {
/**
* @return the name to get to this child command
*/
String getName();
/**
* @return short description of the child command
*/
String getDescription();
/**
* Executes the debug child command.
*
* @param sender the sender executing the command
* @param arguments the arguments, without the label of the child command
*/
void execute(CommandSender sender, List<String> arguments);
}

View File

@ -0,0 +1,101 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboService;
import org.bukkit.Location;
import java.lang.reflect.Field;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
/**
* Utilities used within the DebugSection implementations.
*/
final class DebugSectionUtils {
private static Field limboEntriesField;
private DebugSectionUtils() {
}
/**
* Formats the given location in a human readable way. Null-safe.
*
* @param location the location to format
* @return the formatted location
*/
static String formatLocation(Location location) {
if (location == null) {
return "null";
}
String worldName = location.getWorld() == null ? "null" : location.getWorld().getName();
return formatLocation(location.getX(), location.getY(), location.getZ(), worldName);
}
/**
* Formats the given location in a human readable way.
*
* @param x the x coordinate
* @param y the y coordinate
* @param z the z coordinate
* @param world the world name
* @return the formatted location
*/
static String formatLocation(double x, double y, double z, String world) {
return "(" + round(x) + ", " + round(y) + ", " + round(z) + ") in '" + world + "'";
}
/**
* Rounds the given number to two decimals.
*
* @param number the number to round
* @return the rounded number
*/
private static String round(double number) {
DecimalFormat df = new DecimalFormat("#.##");
df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.US));
df.setRoundingMode(RoundingMode.HALF_UP);
return df.format(number);
}
private static Field getLimboPlayerEntriesField() {
if (limboEntriesField == null) {
try {
Field field = LimboService.class.getDeclaredField("entries");
field.setAccessible(true);
limboEntriesField = field;
} catch (Exception e) {
ConsoleLogger.logException("Could not retrieve LimboService entries field:", e);
}
}
return limboEntriesField;
}
/**
* Applies the given function to the map in LimboService containing the LimboPlayers.
* As we don't want to expose this information in non-debug settings, this is done with reflection.
* Exceptions are generously caught and {@code null} is returned on failure.
*
* @param limboService the limbo service instance to get the map from
* @param function the function to apply to the map
* @param <U> the result type of the function
*
* @return player names for which there is a LimboPlayer (or error message upon failure)
*/
static <U> U applyToLimboPlayersMap(LimboService limboService, Function<Map, U> function) {
Field limboPlayerEntriesField = getLimboPlayerEntriesField();
if (limboPlayerEntriesField != null) {
try {
return function.apply((Map) limboEntriesField.get(limboService));
} catch (Exception e) {
ConsoleLogger.logException("Could not retrieve LimboService values:", e);
}
}
return null;
}
}

View File

@ -0,0 +1,131 @@
package fr.xephi.authme.command.executable.authme.debug;
import com.google.common.collect.ImmutableList;
import fr.xephi.authme.permission.AdminPermission;
import fr.xephi.authme.permission.DefaultPermission;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerPermission;
import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
/**
* Checks if a player has a given permission, as checked by AuthMe.
*/
class HasPermissionChecker implements DebugSection {
static final List<Class<? extends PermissionNode>> PERMISSION_NODE_CLASSES =
ImmutableList.of(AdminPermission.class, PlayerPermission.class, PlayerStatePermission.class);
@Inject
private PermissionsManager permissionsManager;
@Inject
private BukkitService bukkitService;
@Override
public String getName() {
return "perm";
}
@Override
public String getDescription() {
return "Checks if player has given permission: /authme debug perm bobby my.perm";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.size() < 2) {
sender.sendMessage("Check if a player has permission:");
sender.sendMessage("Example: /authme debug perm bobby my.perm.node");
return;
}
final String playerName = arguments.get(0);
final String permissionNode = arguments.get(1);
Player player = bukkitService.getPlayerExact(playerName);
if (player == null) {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
if (offlinePlayer == null) {
sender.sendMessage(ChatColor.DARK_RED + "Player '" + playerName + "' does not exist");
} else {
sender.sendMessage("Player '" + playerName + "' not online; checking with offline player");
performPermissionCheck(offlinePlayer, permissionNode, permissionsManager::hasPermissionOffline, sender);
}
} else {
performPermissionCheck(player, permissionNode, permissionsManager::hasPermission, sender);
}
}
/**
* Performs a permission check and informs the given sender of the result. {@code permissionChecker} is the
* permission check to perform with the given {@code node} and the {@code player}.
*
* @param player the player to check a permission for
* @param node the node of the permission to check
* @param permissionChecker permission checking function
* @param sender the sender to inform of the result
* @param <P> the player type
*/
private static <P extends OfflinePlayer> void performPermissionCheck(
P player, String node, BiFunction<P, PermissionNode, Boolean> permissionChecker, CommandSender sender) {
PermissionNode permNode = getPermissionNode(sender, node);
if (permissionChecker.apply(player, permNode)) {
sender.sendMessage(ChatColor.DARK_GREEN + "Success: player '" + player.getName()
+ "' has permission '" + node + "'");
} else {
sender.sendMessage(ChatColor.DARK_RED + "Check failed: player '" + player.getName()
+ "' does NOT have permission '" + node + "'");
}
}
/**
* Based on the given permission node (String), tries to find the according AuthMe {@link PermissionNode}
* instance, or creates a new one if not available.
*
* @param sender the sender (used to inform him if no AuthMe PermissionNode can be matched)
* @param node the node to search for
* @return the node as {@link PermissionNode} object
*/
private static PermissionNode getPermissionNode(CommandSender sender, String node) {
Optional<? extends PermissionNode> permNode = PERMISSION_NODE_CLASSES.stream()
.map(Class::getEnumConstants)
.flatMap(Arrays::stream)
.filter(perm -> perm.getNode().equals(node))
.findFirst();
if (permNode.isPresent()) {
return permNode.get();
} else {
sender.sendMessage("Did not detect AuthMe permission; using default permission = DENIED");
return createPermNode(node);
}
}
private static PermissionNode createPermNode(String node) {
return new PermissionNode() {
@Override
public String getNode() {
return node;
}
@Override
public DefaultPermission getDefaultPermission() {
return DefaultPermission.NOT_ALLOWED;
}
};
}
}

View File

@ -0,0 +1,115 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.listener.FailedVerificationException;
import fr.xephi.authme.listener.OnJoinVerifier;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.ValidationService.ValidationResult;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.MAIL;
import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.NAME;
import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.PASS;
/**
* Checks if a sample username, email or password is valid according to the AuthMe settings.
*/
class InputValidator implements DebugSection {
@Inject
private ValidationService validationService;
@Inject
private Messages messages;
@Inject
private OnJoinVerifier onJoinVerifier;
@Override
public String getName() {
return "valid";
}
@Override
public String getDescription() {
return "Check if email / password is valid according to your settings";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.size() < 2 || !ValidationObject.matchesAny(arguments.get(0))) {
displayUsageHint(sender);
} else if (PASS.matches(arguments.get(0))) {
validatePassword(sender, arguments.get(1));
} else if (MAIL.matches(arguments.get(0))) {
validateEmail(sender, arguments.get(1));
} else if (NAME.matches(arguments.get(0))) {
validateUsername(sender, arguments.get(1));
} else {
throw new IllegalStateException("Unexpected validation object with arg[0] = '" + arguments.get(0) + "'");
}
}
private void displayUsageHint(CommandSender sender) {
sender.sendMessage("You can define forbidden emails and passwords in your config.yml");
sender.sendMessage("This command allows you to test some of the values:");
sender.sendMessage("/authme debug valid pass test1234 -- test if 'test1234' is allowed password");
sender.sendMessage("/authme debug valid mail t@t.tld -- test if 't@t.tld' is allowed email");
sender.sendMessage("/authme debug valid name bobby1 -- test if 'bobby1' is allowed username");
}
private void validatePassword(CommandSender sender, String password) {
ValidationResult validationResult = validationService.validatePassword(password, "");
sender.sendMessage("Validation of password '" + password + "' returned:");
if (validationResult.hasError()) {
messages.send(sender, validationResult.getMessageKey(), validationResult.getArgs());
} else {
sender.sendMessage(ChatColor.DARK_GREEN + "Valid password!");
}
}
private void validateEmail(CommandSender sender, String email) {
boolean isValidEmail = validationService.validateEmail(email);
sender.sendMessage("Validation of email '" + email + "' returned:");
if (isValidEmail) {
sender.sendMessage(ChatColor.DARK_GREEN + "Valid email!");
} else {
sender.sendMessage(ChatColor.DARK_RED + "Email is not valid!");
}
}
private void validateUsername(CommandSender sender, String username) {
sender.sendMessage("Validation of username '" + username + "' returned:");
try {
onJoinVerifier.checkIsValidName(username);
sender.sendMessage("Valid username!");
} catch (FailedVerificationException failedVerificationEx) {
messages.send(sender, failedVerificationEx.getReason(), failedVerificationEx.getArgs());
}
}
enum ValidationObject {
PASS, MAIL, NAME;
static boolean matchesAny(String arg) {
return Arrays.stream(values()).anyMatch(vo -> vo.matches(arg));
}
boolean matches(String arg) {
return name().equalsIgnoreCase(arg);
}
}
}

View File

@ -0,0 +1,132 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap;
/**
* Shows the data stored in LimboPlayers and the equivalent properties on online players.
*/
class LimboPlayerViewer implements DebugSection {
@Inject
private LimboService limboService;
@Inject
private LimboPersistence limboPersistence;
@Inject
private BukkitService bukkitService;
@Inject
private PermissionsManager permissionsManager;
@Override
public String getName() {
return "limbo";
}
@Override
public String getDescription() {
return "View LimboPlayers and player's \"limbo stats\"";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage("/authme debug limbo <player>: show a player's limbo info");
sender.sendMessage("Available limbo records: " + applyToLimboPlayersMap(limboService, Map::keySet));
return;
}
LimboPlayer memoryLimbo = limboService.getLimboPlayer(arguments.get(0));
Player player = bukkitService.getPlayerExact(arguments.get(0));
LimboPlayer diskLimbo = player != null ? limboPersistence.getLimboPlayer(player) : null;
if (memoryLimbo == null && player == null) {
sender.sendMessage("No limbo info and no player online with name '" + arguments.get(0) + "'");
return;
}
sender.sendMessage(ChatColor.GOLD + "Showing disk limbo / limbo / player info for '" + arguments.get(0) + "'");
new InfoDisplayer(sender, diskLimbo, memoryLimbo, player)
.sendEntry("Is op", LimboPlayer::isOperator, Player::isOp)
.sendEntry("Walk speed", LimboPlayer::getWalkSpeed, Player::getWalkSpeed)
.sendEntry("Can fly", LimboPlayer::isCanFly, Player::getAllowFlight)
.sendEntry("Fly speed", LimboPlayer::getFlySpeed, Player::getFlySpeed)
.sendEntry("Location", l -> formatLocation(l.getLocation()), p -> formatLocation(p.getLocation()))
.sendEntry("Group", LimboPlayer::getGroup, permissionsManager::getPrimaryGroup);
}
/**
* Displays the info for the given LimboPlayer and Player to the provided CommandSender.
*/
private static final class InfoDisplayer {
private final CommandSender sender;
private final Optional<LimboPlayer> diskLimbo;
private final Optional<LimboPlayer> memoryLimbo;
private final Optional<Player> player;
/**
* Constructor.
*
* @param sender command sender to send the information to
* @param memoryLimbo the limbo player to get data from
* @param player the player to get data from
*/
InfoDisplayer(CommandSender sender, LimboPlayer diskLimbo, LimboPlayer memoryLimbo, Player player) {
this.sender = sender;
this.diskLimbo = Optional.ofNullable(diskLimbo);
this.memoryLimbo = Optional.ofNullable(memoryLimbo);
this.player = Optional.ofNullable(player);
if (memoryLimbo == null) {
sender.sendMessage("Note: no Limbo information available");
}
if (player == null) {
sender.sendMessage("Note: player is not online");
} else if (diskLimbo == null) {
sender.sendMessage("Note: no Limbo on disk available");
}
}
/**
* Displays a piece of information to the command sender.
*
* @param title the designation of the piece of information
* @param limboGetter getter for data retrieval on the LimboPlayer
* @param playerGetter getter for data retrieval on Player
* @param <T> the data type
* @return this instance (for chaining)
*/
<T> InfoDisplayer sendEntry(String title,
Function<LimboPlayer, T> limboGetter,
Function<Player, T> playerGetter) {
sender.sendMessage(
title + ": "
+ getData(diskLimbo, limboGetter)
+ " / "
+ getData(memoryLimbo, limboGetter)
+ " / "
+ getData(player, playerGetter));
return this;
}
static <E, T> String getData(Optional<E> entity, Function<E, T> getter) {
return entity.map(getter).map(String::valueOf).orElse(" -- ");
}
}
}

View File

@ -0,0 +1,41 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.permission.PermissionsManager;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Outputs the permission groups of a player.
*/
class PermissionGroups implements DebugSection {
@Inject
private PermissionsManager permissionsManager;
@Override
public String getName() {
return "groups";
}
@Override
public String getDescription() {
return "Show permission groups a player belongs to";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
String name = arguments.isEmpty() ? sender.getName() : arguments.get(0);
Player player = Bukkit.getPlayer(name);
if (player == null) {
sender.sendMessage("Player " + name + " could not be found");
} else {
sender.sendMessage("Player " + name + " has permission groups: "
+ String.join(", ", permissionsManager.getGroups(player)));
sender.sendMessage("Primary group is: " + permissionsManager.getGroups(player));
}
}
}

View File

@ -0,0 +1,104 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
/**
* Allows to view the data of a PlayerAuth in the database.
*/
class PlayerAuthViewer implements DebugSection {
@Inject
private DataSource dataSource;
@Override
public String getName() {
return "db";
}
@Override
public String getDescription() {
return "View player's data in the database";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage("Enter player name to view his data in the database.");
sender.sendMessage("Example: /authme debug db Bobby");
return;
}
PlayerAuth auth = dataSource.getAuth(arguments.get(0));
if (auth == null) {
sender.sendMessage("No record exists for '" + arguments.get(0) + "'");
} else {
displayAuthToSender(auth, sender);
}
}
/**
* Outputs the PlayerAuth information to the given sender.
*
* @param auth the PlayerAuth to display
* @param sender the sender to send the messages to
*/
private void displayAuthToSender(PlayerAuth auth, CommandSender sender) {
sender.sendMessage(ChatColor.GOLD + "[AuthMe] Player " + auth.getNickname() + " / " + auth.getRealName());
sender.sendMessage("Email: " + auth.getEmail() + ". IP: " + auth.getIp() + ". Group: " + auth.getGroupId());
sender.sendMessage("Quit location: "
+ formatLocation(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld()));
sender.sendMessage("Last login: " + formatLastLogin(auth));
HashedPassword hashedPass = auth.getPassword();
sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6)
+ "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'");
}
/**
* Fail-safe substring method. Guarantees not to show the entire String.
*
* @param str the string to transform
* @param length number of characters to show from the start of the String
* @return the first <code>length</code> characters of the string, or half of the string if it is shorter,
* or empty string if the string is null or empty
*/
private static String safeSubstring(String str, int length) {
if (StringUtils.isEmpty(str)) {
return "";
} else if (str.length() < length) {
return str.substring(0, str.length() / 2) + "...";
} else {
return str.substring(0, length) + "...";
}
}
/**
* Formats the last login date from the given PlayerAuth.
*
* @param auth the auth object
* @return the last login as human readable date
*/
private static String formatLastLogin(PlayerAuth auth) {
long lastLogin = auth.getLastLogin();
if (lastLogin == 0) {
return "Never (0)";
} else {
LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastLogin), ZoneId.systemDefault());
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date);
}
}
}

View File

@ -0,0 +1,78 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
/**
* Shows the spawn location that AuthMe is configured to use.
*/
class SpawnLocationViewer implements DebugSection {
@Inject
private SpawnLoader spawnLoader;
@Inject
private Settings settings;
@Inject
private BukkitService bukkitService;
@Override
public String getName() {
return "spawn";
}
@Override
public String getDescription() {
return "Shows the spawn location that AuthMe will use";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
showGeneralInfo(sender);
} else if ("?".equals(arguments.get(0))) {
showHelp(sender);
} else {
showPlayerSpawn(sender, arguments.get(0));
}
}
private void showGeneralInfo(CommandSender sender) {
sender.sendMessage("Spawn priority: "
+ String.join(", ", settings.getProperty(RestrictionSettings.SPAWN_PRIORITY)));
sender.sendMessage("AuthMe spawn location: " + formatLocation(spawnLoader.getSpawn()));
sender.sendMessage("AuthMe first spawn location: " + formatLocation(spawnLoader.getFirstSpawn()));
sender.sendMessage("AuthMe (first)spawn are only used depending on the configured priority!");
sender.sendMessage("Use '/authme debug spawn ?' for further help");
}
private void showHelp(CommandSender sender) {
sender.sendMessage("Use /authme spawn and /authme firstspawn to teleport to the spawns.");
sender.sendMessage("/authme set(first)spawn sets the (first) spawn to your current location.");
sender.sendMessage("Use /authme debug spawn <player> to view where a player would be teleported to.");
sender.sendMessage("Read more at https://github.com/AuthMe/AuthMeReloaded/wiki/Spawn-Handling");
}
private void showPlayerSpawn(CommandSender sender, String playerName) {
Player player = bukkitService.getPlayerExact(playerName);
if (player == null) {
sender.sendMessage("Player '" + playerName + "' is not online!");
} else {
Location spawn = spawnLoader.getSpawnLocation(player);
sender.sendMessage("Player '" + playerName + "' has spawn location: " + formatLocation(spawn));
sender.sendMessage("Note: this check excludes the AuthMe firstspawn.");
}
}
}

View File

@ -0,0 +1,102 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.mail.SendMailSsl;
import fr.xephi.authme.util.StringUtils;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.bukkit.ChatColor;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Sends out a test email.
*/
class TestEmailSender implements DebugSection {
@Inject
private DataSource dataSource;
@Inject
private SendMailSsl sendMailSsl;
@Inject
private Server server;
@Override
public String getName() {
return "mail";
}
@Override
public String getDescription() {
return "Sends out a test email";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (!sendMailSsl.hasAllInformation()) {
sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml "
+ "for sending emails. Please check your config.yml");
return;
}
String email = getEmail(sender, arguments);
// getEmail() takes care of informing the sender of the error if email == null
if (email != null) {
boolean sendMail = sendTestEmail(email);
if (sendMail) {
sender.sendMessage("Test email sent to " + email + " with success");
} else {
sender.sendMessage(ChatColor.RED + "Failed to send test mail to " + email + "; please check your logs");
}
}
}
private String getEmail(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
PlayerAuth auth = dataSource.getAuth(sender.getName());
if (auth == null) {
sender.sendMessage(ChatColor.RED + "Please provide an email address, "
+ "e.g. /authme debug mail test@example.com");
return null;
}
String email = auth.getEmail();
if (email == null || "your@email.com".equals(email)) {
sender.sendMessage(ChatColor.RED + "No email set for your account!"
+ " Please use /authme debug mail <email>");
return null;
}
return email;
} else {
String email = arguments.get(0);
if (StringUtils.isInsideString('@', email)) {
return email;
}
sender.sendMessage(ChatColor.RED + "Invalid email! Usage: /authme debug mail test@example.com");
return null;
}
}
private boolean sendTestEmail(String email) {
HtmlEmail htmlEmail;
try {
htmlEmail = sendMailSsl.initializeMail(email);
} catch (EmailException e) {
ConsoleLogger.logException("Failed to create email for sample email:", e);
return false;
}
htmlEmail.setSubject("AuthMe test email");
String message = "Hello there!<br />This is a sample email sent to you from a Minecraft server ("
+ server.getName() + ") via /authme debug mail. If you're seeing this, sending emails should be fine.";
return sendMailSsl.sendEmail(message, htmlEmail);
}
}

View File

@ -3,8 +3,7 @@ package fr.xephi.authme.command.executable.captcha;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.CaptchaManager;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.limbo.LimboCache;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.entity.Player;
@ -24,7 +23,7 @@ public class CaptchaCommand extends PlayerCommand {
private CommonService commonService;
@Inject
private LimboCache limboCache;
private LimboService limboService;
@Override
public void runCommand(Player player, List<String> arguments) {
@ -44,7 +43,7 @@ public class CaptchaCommand extends PlayerCommand {
if (isCorrectCode) {
commonService.send(player, MessageKey.CAPTCHA_SUCCESS);
commonService.send(player, MessageKey.LOGIN_MESSAGE);
limboCache.getPlayerData(player.getName()).getMessageTask().setMuted(false);
limboService.unmuteMessageTask(player);
} else {
String newCode = captchaManager.generateCode(player.getName());
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);

View File

@ -54,4 +54,9 @@ public class ChangePasswordCommand extends PlayerCommand {
protected String getAlternativeCommand() {
return "/authme password <playername> <password>";
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_CHANGE_PASSWORD;
}
}

View File

@ -32,4 +32,9 @@ public class AddEmailCommand extends PlayerCommand {
commonService.send(player, MessageKey.CONFIRM_EMAIL_MESSAGE);
}
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_ADD_EMAIL;
}
}

View File

@ -1,6 +1,7 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import org.bukkit.entity.Player;
@ -22,4 +23,9 @@ public class ChangeEmailCommand extends PlayerCommand {
management.performChangeEmail(player, playerMailOld, playerMailNew);
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_CHANGE_EMAIL;
}
}

View File

@ -0,0 +1,46 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.PasswordRecoveryService;
import fr.xephi.authme.service.RecoveryCodeService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for submitting email recovery code.
*/
public class ProcessCodeCommand extends PlayerCommand {
@Inject
private CommonService commonService;
@Inject
private RecoveryCodeService codeService;
@Inject
private PasswordRecoveryService recoveryService;
@Override
protected void runCommand(Player player, List<String> arguments) {
String name = player.getName();
String code = arguments.get(0);
if (codeService.hasTriesLeft(name)) {
if (codeService.isCodeValid(name, code)) {
commonService.send(player, MessageKey.RECOVERY_CODE_CORRECT);
recoveryService.addSuccessfulRecovery(player);
codeService.removeCode(name);
} else {
commonService.send(player, MessageKey.INCORRECT_RECOVERY_CODE,
Integer.toString(codeService.getTriesLeft(name)));
}
} else {
codeService.removeCode(name);
commonService.send(player, MessageKey.RECOVERY_TRIES_EXCEEDED);
}
}
}

View File

@ -5,28 +5,21 @@ import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.mail.SendMailSSL;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.PasswordRecoveryService;
import fr.xephi.authme.service.RecoveryCodeService;
import fr.xephi.authme.util.RandomStringUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH;
/**
* Command for password recovery by email.
*/
public class RecoverEmailCommand extends PlayerCommand {
@Inject
private PasswordSecurity passwordSecurity;
@Inject
private CommonService commonService;
@ -37,17 +30,20 @@ public class RecoverEmailCommand extends PlayerCommand {
private PlayerCache playerCache;
@Inject
private SendMailSSL sendMailSsl;
private EmailService emailService;
@Inject
private PasswordRecoveryService recoveryService;
@Inject
private RecoveryCodeService recoveryCodeService;
@Override
public void runCommand(Player player, List<String> arguments) {
protected void runCommand(Player player, List<String> arguments) {
final String playerMail = arguments.get(0);
final String playerName = player.getName();
if (!sendMailSsl.hasAllInformation()) {
if (!emailService.hasAllInformation()) {
ConsoleLogger.warning("Mail API is not set");
commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS);
return;
@ -57,7 +53,7 @@ public class RecoverEmailCommand extends PlayerCommand {
return;
}
PlayerAuth auth = dataSource.getAuth(playerName); // TODO: Create method to get email only
PlayerAuth auth = dataSource.getAuth(playerName); // TODO #1127: Create method to get email only
if (auth == null) {
commonService.send(player, MessageKey.USAGE_REGISTER);
return;
@ -70,49 +66,16 @@ public class RecoverEmailCommand extends PlayerCommand {
}
if (recoveryCodeService.isRecoveryCodeNeeded()) {
// Process /email recovery addr@example.com
if (arguments.size() == 1) {
createAndSendRecoveryCode(player, email);
// Recovery code is needed; generate and send one
recoveryService.createAndSendRecoveryCode(player, email);
} else {
// Process /email recovery addr@example.com 12394
processRecoveryCode(player, arguments.get(1), email);
}
} else {
generateAndSendNewPassword(player, email);
// Code not needed, just send them a new password
recoveryService.generateAndSendNewPassword(player, email);
}
}
private void createAndSendRecoveryCode(Player player, String email) {
String recoveryCode = recoveryCodeService.generateCode(player.getName());
boolean couldSendMail = sendMailSsl.sendRecoveryCode(player.getName(), email, recoveryCode);
if (couldSendMail) {
commonService.send(player, MessageKey.RECOVERY_CODE_SENT);
} else {
commonService.send(player, MessageKey.EMAIL_SEND_FAILURE);
}
}
private void processRecoveryCode(Player player, String code, String email) {
final String name = player.getName();
if (recoveryCodeService.isCodeValid(name, code)) {
generateAndSendNewPassword(player, email);
recoveryCodeService.removeCode(name);
} else {
commonService.send(player, MessageKey.INCORRECT_RECOVERY_CODE);
}
}
private void generateAndSendNewPassword(Player player, String email) {
String name = player.getName();
String thePass = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH));
HashedPassword hashNew = passwordSecurity.computeHash(thePass, name);
dataSource.updatePassword(name, hashNew);
boolean couldSendMail = sendMailSsl.sendPasswordMail(name, email, thePass);
if (couldSendMail) {
commonService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE);
} else {
commonService.send(player, MessageKey.EMAIL_SEND_FAILURE);
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_RECOVER_EMAIL;
}
}

View File

@ -0,0 +1,55 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.PasswordRecoveryService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.ValidationService.ValidationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for changing password following successful recovery.
*/
public class SetPasswordCommand extends PlayerCommand {
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Inject
private PasswordRecoveryService recoveryService;
@Inject
private PasswordSecurity passwordSecurity;
@Inject
private ValidationService validationService;
@Override
protected void runCommand(Player player, List<String> arguments) {
if (recoveryService.canChangePassword(player)) {
String name = player.getName();
String password = arguments.get(0);
ValidationResult result = validationService.validatePassword(password, name);
if (!result.hasError()) {
HashedPassword hashedPassword = passwordSecurity.computeHash(password, name);
dataSource.updatePassword(name, hashedPassword);
ConsoleLogger.info("Player '" + name + "' has changed their password from recovery");
commonService.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS);
} else {
commonService.send(player, result.getMessageKey(), result.getArgs());
}
}
}
}

View File

@ -24,7 +24,7 @@ public class ShowEmailCommand extends PlayerCommand {
@Override
public void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth.getEmail() != null && !"your@email.com".equalsIgnoreCase(auth.getEmail())) {
if (auth != null && auth.getEmail() != null && !"your@email.com".equalsIgnoreCase(auth.getEmail())) {
commonService.send(player, MessageKey.EMAIL_SHOW, auth.getEmail());
} else {
commonService.send(player, MessageKey.SHOW_NO_EMAIL);

View File

@ -2,12 +2,15 @@ package fr.xephi.authme.command.executable.register;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.mail.SendMailSSL;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.process.register.RegisterSecondaryArgument;
import fr.xephi.authme.process.register.RegistrationType;
import fr.xephi.authme.process.register.executors.RegistrationExecutorProvider;
import fr.xephi.authme.process.register.executors.EmailRegisterParams;
import fr.xephi.authme.process.register.executors.PasswordRegisterParams;
import fr.xephi.authme.process.register.executors.RegistrationMethod;
import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
@ -37,20 +40,17 @@ public class RegisterCommand extends PlayerCommand {
private CommonService commonService;
@Inject
private SendMailSSL sendMailSsl;
private EmailService emailService;
@Inject
private ValidationService validationService;
@Inject
private RegistrationExecutorProvider registrationExecutorProvider;
@Override
public void runCommand(Player player, List<String> arguments) {
if (commonService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) {
//for two factor auth we don't need to check the usage
management.performRegister(player,
registrationExecutorProvider.getTwoFactorRegisterExecutor(player));
management.performRegister(RegistrationMethod.TWO_FACTOR_REGISTRATION,
TwoFactorRegisterParams.of(player));
return;
} else if (arguments.size() < 1) {
commonService.send(player, MessageKey.USAGE_REGISTER);
@ -82,8 +82,8 @@ public class RegisterCommand extends PlayerCommand {
final String password = arguments.get(0);
final String email = getEmailIfAvailable(arguments);
management.performRegister(
player, registrationExecutorProvider.getPasswordRegisterExecutor(player, password, email));
management.performRegister(RegistrationMethod.PASSWORD_REGISTRATION,
PasswordRegisterParams.of(player, password, email));
}
}
@ -127,7 +127,7 @@ public class RegisterCommand extends PlayerCommand {
}
private void handleEmailRegistration(Player player, List<String> arguments) {
if (!sendMailSsl.hasAllInformation()) {
if (!emailService.hasAllInformation()) {
commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS);
ConsoleLogger.warning("Cannot register player '" + player.getName() + "': no email or password is set "
+ "to send emails from. Please adjust your config at " + EmailSettings.MAIL_ACCOUNT.getPath());
@ -138,7 +138,8 @@ public class RegisterCommand extends PlayerCommand {
if (!validationService.validateEmail(email)) {
commonService.send(player, MessageKey.INVALID_EMAIL);
} else if (isSecondArgValidForEmailRegistration(player, arguments)) {
management.performRegister(player, registrationExecutorProvider.getEmailRegisterExecutor(player, email));
management.performRegister(RegistrationMethod.EMAIL_REGISTRATION,
EmailRegisterParams.of(player, email));
}
}

View File

@ -38,4 +38,14 @@ public class UnregisterCommand extends PlayerCommand {
// Unregister the player
management.performUnregister(player, playerPass);
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_UNREGISTER;
}
@Override
protected String getAlternativeCommand() {
return "/authme unregister <player>";
}
}

View File

@ -81,8 +81,8 @@ public class HelpMessagesService implements Reloadable {
public String getMessage(DefaultPermission defaultPermission) {
// e.g. {default_permissions_path}.opOnly for DefaultPermission.OP_ONLY
String path = DEFAULT_PERMISSIONS_PATH +
CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, defaultPermission.name());
String path = DEFAULT_PERMISSIONS_PATH
+ CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, defaultPermission.name());
return messageFileHandler.getMessage(path);
}

View File

@ -1,19 +1,22 @@
package fr.xephi.authme.data;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.util.expiring.TimedCounter;
import javax.inject.Inject;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* Manager for the handling of captchas.
*/
public class CaptchaManager implements SettingsDependent {
public class CaptchaManager implements SettingsDependent, HasCleanup {
private final ConcurrentHashMap<String, Integer> playerCounts;
private final TimedCounter<String> playerCounts;
private final ConcurrentHashMap<String, String> captchaCodes;
private boolean isEnabled;
@ -22,8 +25,9 @@ public class CaptchaManager implements SettingsDependent {
@Inject
CaptchaManager(Settings settings) {
this.playerCounts = new ConcurrentHashMap<>();
this.captchaCodes = new ConcurrentHashMap<>();
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
this.playerCounts = new TimedCounter<>(countTimeout, TimeUnit.MINUTES);
reload(settings);
}
@ -35,12 +39,7 @@ public class CaptchaManager implements SettingsDependent {
public void increaseCount(String name) {
if (isEnabled) {
String playerLower = name.toLowerCase();
Integer currentCount = playerCounts.get(playerLower);
if (currentCount == null) {
playerCounts.put(playerLower, 1);
} else {
playerCounts.put(playerLower, currentCount + 1);
}
playerCounts.increment(playerLower);
}
}
@ -51,21 +50,7 @@ public class CaptchaManager implements SettingsDependent {
* @return true if the player has to solve a captcha, false otherwise
*/
public boolean isCaptchaRequired(String name) {
if (isEnabled) {
Integer count = playerCounts.get(name.toLowerCase());
return count != null && count >= threshold;
}
return false;
}
/**
* Returns the stored captcha code for the player.
*
* @param name the player's name
* @return the code the player is required to enter, or null if none registered
*/
public String getCaptchaCode(String name) {
return captchaCodes.get(name.toLowerCase());
return isEnabled && playerCounts.get(name.toLowerCase()) >= threshold;
}
/**
@ -75,7 +60,7 @@ public class CaptchaManager implements SettingsDependent {
* @return the code the player is required to enter
*/
public String getCaptchaCodeOrGenerateNew(String name) {
String code = getCaptchaCode(name);
String code = captchaCodes.get(name.toLowerCase());
return code == null ? generateCode(name) : code;
}
@ -127,6 +112,13 @@ public class CaptchaManager implements SettingsDependent {
this.isEnabled = settings.getProperty(SecuritySettings.USE_CAPTCHA);
this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA);
this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
playerCounts.setExpiration(countTimeout, TimeUnit.MINUTES);
}
@Override
public void performCleanup() {
playerCounts.removeExpiredEntries();
}
}

View File

@ -5,13 +5,10 @@ import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.util.expiring.ExpiringSet;
import javax.inject.Inject;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
import java.util.concurrent.TimeUnit;
/**
* Manages sessions, allowing players to be automatically logged in if they join again
@ -19,15 +16,14 @@ import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
*/
public class SessionManager implements SettingsDependent, HasCleanup {
// Player -> expiration of session in milliseconds
private final Map<String, Long> sessions = new ConcurrentHashMap<>();
private final ExpiringSet<String> sessions;
private boolean enabled;
private int timeoutInMinutes;
@Inject
SessionManager(Settings settings) {
reload(settings);
long timeout = settings.getProperty(PluginSettings.SESSIONS_TIMEOUT);
sessions = new ExpiringSet<>(timeout, TimeUnit.MINUTES);
enabled = timeout > 0 && settings.getProperty(PluginSettings.SESSIONS_ENABLED);
}
/**
@ -37,13 +33,7 @@ public class SessionManager implements SettingsDependent, HasCleanup {
* @return True if a session is found.
*/
public boolean hasSession(String name) {
if (enabled) {
Long timeout = sessions.get(name.toLowerCase());
if (timeout != null) {
return System.currentTimeMillis() <= timeout;
}
}
return false;
return enabled && sessions.contains(name.toLowerCase());
}
/**
@ -53,8 +43,7 @@ public class SessionManager implements SettingsDependent, HasCleanup {
*/
public void addSession(String name) {
if (enabled) {
long timeout = System.currentTimeMillis() + timeoutInMinutes * MILLIS_PER_MINUTE;
sessions.put(name.toLowerCase(), timeout);
sessions.add(name.toLowerCase());
}
}
@ -64,12 +53,13 @@ public class SessionManager implements SettingsDependent, HasCleanup {
* @param name The name of the player.
*/
public void removeSession(String name) {
this.sessions.remove(name.toLowerCase());
sessions.remove(name.toLowerCase());
}
@Override
public void reload(Settings settings) {
timeoutInMinutes = settings.getProperty(PluginSettings.SESSIONS_TIMEOUT);
long timeoutInMinutes = settings.getProperty(PluginSettings.SESSIONS_TIMEOUT);
sessions.setExpiration(timeoutInMinutes, TimeUnit.MINUTES);
boolean oldEnabled = enabled;
enabled = timeoutInMinutes > 0 && settings.getProperty(PluginSettings.SESSIONS_ENABLED);
@ -82,16 +72,8 @@ public class SessionManager implements SettingsDependent, HasCleanup {
@Override
public void performCleanup() {
if (!enabled) {
return;
}
final long currentTime = System.currentTimeMillis();
Iterator<Map.Entry<String, Long>> iterator = sessions.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Long> entry = iterator.next();
if (entry.getValue() < currentTime) {
iterator.remove();
}
if (enabled) {
sessions.removeExpiredEntries();
}
}
}

View File

@ -1,21 +1,21 @@
package fr.xephi.authme.data;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.PlayerUtils;
import fr.xephi.authme.util.expiring.TimedCounter;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static fr.xephi.authme.settings.properties.SecuritySettings.TEMPBAN_MINUTES_BEFORE_RESET;
import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
@ -25,7 +25,7 @@ import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
*/
public class TempbanManager implements SettingsDependent, HasCleanup {
private final Map<String, Map<String, TimedCounter>> ipLoginFailureCounts;
private final Map<String, TimedCounter<String>> ipLoginFailureCounts;
private final BukkitService bukkitService;
private final Messages messages;
@ -50,18 +50,9 @@ public class TempbanManager implements SettingsDependent, HasCleanup {
*/
public void increaseCount(String address, String name) {
if (isEnabled) {
Map<String, TimedCounter> countsByName = ipLoginFailureCounts.get(address);
if (countsByName == null) {
countsByName = new ConcurrentHashMap<>();
ipLoginFailureCounts.put(address, countsByName);
}
TimedCounter counter = countsByName.get(name);
if (counter == null) {
countsByName.put(name, new TimedCounter(1));
} else {
counter.increment(resetThreshold);
}
TimedCounter<String> countsByName = ipLoginFailureCounts.computeIfAbsent(
address, k -> new TimedCounter<>(resetThreshold, TimeUnit.MINUTES));
countsByName.increment(name);
}
}
@ -73,9 +64,9 @@ public class TempbanManager implements SettingsDependent, HasCleanup {
*/
public void resetCount(String address, String name) {
if (isEnabled) {
Map<String, TimedCounter> map = ipLoginFailureCounts.get(address);
if (map != null) {
map.remove(name);
TimedCounter<String> counter = ipLoginFailureCounts.get(address);
if (counter != null) {
counter.remove(name);
}
}
}
@ -88,13 +79,9 @@ public class TempbanManager implements SettingsDependent, HasCleanup {
*/
public boolean shouldTempban(String address) {
if (isEnabled) {
Map<String, TimedCounter> countsByName = ipLoginFailureCounts.get(address);
TimedCounter<String> countsByName = ipLoginFailureCounts.get(address);
if (countsByName != null) {
int total = 0;
for (TimedCounter counter : countsByName.values()) {
total += counter.getCount(resetThreshold);
}
return total >= threshold;
return countsByName.total() >= threshold;
}
}
return false;
@ -137,56 +124,9 @@ public class TempbanManager implements SettingsDependent, HasCleanup {
@Override
public void performCleanup() {
for (Map<String, TimedCounter> countsByIp : ipLoginFailureCounts.values()) {
Iterator<TimedCounter> it = countsByIp.values().iterator();
while (it.hasNext()) {
TimedCounter counter = it.next();
if (counter.getCount(resetThreshold) == 0) {
it.remove();
}
}
}
}
/**
* Counter with an associated timestamp, keeping track of when the last entry has been added.
*/
@VisibleForTesting
static final class TimedCounter {
private int counter;
private long lastIncrementTimestamp = System.currentTimeMillis();
/**
* Constructor.
*
* @param start the initial value to set the counter to
*/
TimedCounter(int start) {
this.counter = start;
}
/**
* Returns the count, taking into account the last entry timestamp.
*
* @param threshold the threshold in milliseconds until when to consider a counter
* @return the counter's value, or {@code 0} if it was last incremented longer ago than the threshold
*/
int getCount(long threshold) {
if (System.currentTimeMillis() - lastIncrementTimestamp > threshold) {
return 0;
}
return counter;
}
/**
* Increments the counter, taking into account the last entry timestamp.
*
* @param threshold in milliseconds, the time span until which to consider the existing number
*/
void increment(long threshold) {
counter = getCount(threshold) + 1;
lastIncrementTimestamp = System.currentTimeMillis();
}
for (TimedCounter<String> countsByIp : ipLoginFailureCounts.values()) {
countsByIp.removeExpiredEntries();
}
ipLoginFailureCounts.entrySet().removeIf(e -> e.getValue().isEmpty());
}
}

View File

@ -1,214 +0,0 @@
package fr.xephi.authme.data.backup;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* Class used to store player's data (OP, flying, speed, position) to disk.
*/
public class LimboPlayerStorage {
private final Gson gson;
private final File cacheDir;
private PermissionsManager permissionsManager;
private SpawnLoader spawnLoader;
private BukkitService bukkitService;
@Inject
LimboPlayerStorage(@DataFolder File dataFolder, PermissionsManager permsMan,
SpawnLoader spawnLoader, BukkitService bukkitService) {
this.permissionsManager = permsMan;
this.spawnLoader = spawnLoader;
this.bukkitService = bukkitService;
cacheDir = new File(dataFolder, "playerdata");
if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) {
ConsoleLogger.warning("Failed to create userdata directory.");
}
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer())
.setPrettyPrinting()
.create();
}
/**
* Read and construct new PlayerData from existing player data.
*
* @param player player to read
*
* @return PlayerData object if the data is exist, null otherwise.
*/
public LimboPlayer readData(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id + File.separator + "data.json");
if (!file.exists()) {
return null;
}
try {
String str = Files.toString(file, StandardCharsets.UTF_8);
return gson.fromJson(str, LimboPlayer.class);
} catch (IOException e) {
ConsoleLogger.logException("Could not read player data on disk for '" + player.getName() + "'", e);
return null;
}
}
/**
* Save player data (OP, flying, location, etc) to disk.
*
* @param player player to save
*/
public void saveData(Player player) {
String id = PlayerUtils.getUUIDorName(player);
Location location = spawnLoader.getPlayerLocationOrSpawn(player);
String group = "";
if (permissionsManager.hasGroupSupport()) {
group = permissionsManager.getPrimaryGroup(player);
}
boolean operator = player.isOp();
boolean canFly = player.getAllowFlight();
float walkSpeed = player.getWalkSpeed();
float flySpeed = player.getFlySpeed();
LimboPlayer limboPlayer = new LimboPlayer(location, operator, group, canFly, walkSpeed, flySpeed);
try {
File file = new File(cacheDir, id + File.separator + "data.json");
Files.createParentDirs(file);
Files.touch(file);
Files.write(gson.toJson(limboPlayer), file, StandardCharsets.UTF_8);
} catch (IOException e) {
ConsoleLogger.logException("Failed to write " + player.getName() + " data.", e);
}
}
/**
* Remove player data, this will delete
* "playerdata/&lt;uuid or name&gt;/" folder from disk.
*
* @param player player to remove
*/
public void removeData(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id);
if (file.exists()) {
FileUtils.purgeDirectory(file);
if (!file.delete()) {
ConsoleLogger.warning("Failed to remove " + player.getName() + " cache.");
}
}
}
/**
* Use to check is player data is exist.
*
* @param player player to check
*
* @return true if data exist, false otherwise.
*/
public boolean hasData(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id + File.separator + "data.json");
return file.exists();
}
private class LimboPlayerDeserializer implements JsonDeserializer<LimboPlayer> {
@Override
public LimboPlayer deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext context) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject == null) {
return null;
}
Location loc = null;
String group = "";
boolean operator = false;
boolean canFly = false;
float walkSpeed = 0.2f;
float flySpeed = 0.2f;
JsonElement e;
if ((e = jsonObject.getAsJsonObject("location")) != null) {
JsonObject obj = e.getAsJsonObject();
World world = bukkitService.getWorld(obj.get("world").getAsString());
if (world != null) {
double x = obj.get("x").getAsDouble();
double y = obj.get("y").getAsDouble();
double z = obj.get("z").getAsDouble();
float yaw = obj.get("yaw").getAsFloat();
float pitch = obj.get("pitch").getAsFloat();
loc = new Location(world, x, y, z, yaw, pitch);
}
}
if ((e = jsonObject.get("group")) != null) {
group = e.getAsString();
}
if ((e = jsonObject.get("operator")) != null) {
operator = e.getAsBoolean();
}
if ((e = jsonObject.get("can-fly")) != null) {
canFly = e.getAsBoolean();
}
if ((e = jsonObject.get("walk-speed")) != null) {
walkSpeed = e.getAsFloat();
}
if ((e = jsonObject.get("fly-speed")) != null) {
flySpeed = e.getAsFloat();
}
return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed);
}
}
private class LimboPlayerSerializer implements JsonSerializer<LimboPlayer> {
@Override
public JsonElement serialize(LimboPlayer limboPlayer, Type type,
JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("group", limboPlayer.getGroup());
Location loc = limboPlayer.getLocation();
JsonObject obj2 = new JsonObject();
obj2.addProperty("world", loc.getWorld().getName());
obj2.addProperty("x", loc.getX());
obj2.addProperty("y", loc.getY());
obj2.addProperty("z", loc.getZ());
obj2.addProperty("yaw", loc.getYaw());
obj2.addProperty("pitch", loc.getPitch());
obj.add("location", obj2);
obj.addProperty("operator", limboPlayer.isOperator());
obj.addProperty("can-fly", limboPlayer.isCanFly());
obj.addProperty("walk-speed", limboPlayer.getWalkSpeed());
obj.addProperty("fly-speed", limboPlayer.getFlySpeed());
return obj;
}
}
}

View File

@ -0,0 +1,43 @@
package fr.xephi.authme.data.limbo;
import org.bukkit.entity.Player;
import java.util.function.Function;
/**
* Possible types to restore the "allow flight" property
* from LimboPlayer to Bukkit Player.
*/
public enum AllowFlightRestoreType {
/** Set value from LimboPlayer to Player. */
RESTORE(LimboPlayer::isCanFly),
/** Always set flight enabled to true. */
ENABLE(l -> true),
/** Always set flight enabled to false. */
DISABLE(l -> false);
private final Function<LimboPlayer, Boolean> valueGetter;
/**
* Constructor.
*
* @param valueGetter function with which the value to set on the player can be retrieved
*/
AllowFlightRestoreType(Function<LimboPlayer, Boolean> valueGetter) {
this.valueGetter = valueGetter;
}
/**
* Restores the "allow flight" property from the LimboPlayer to the Player.
* This method behaves differently for each restoration type.
*
* @param player the player to modify
* @param limbo the limbo player to read from
*/
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
player.setAllowFlight(valueGetter.apply(limbo));
}
}

View File

@ -1,164 +0,0 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.data.backup.LimboPlayerStorage;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Manages all {@link LimboPlayer} instances.
*/
public class LimboCache {
private final Map<String, LimboPlayer> cache = new ConcurrentHashMap<>();
private LimboPlayerStorage limboPlayerStorage;
private Settings settings;
private PermissionsManager permissionsManager;
private SpawnLoader spawnLoader;
@Inject
LimboCache(Settings settings, PermissionsManager permissionsManager,
SpawnLoader spawnLoader, LimboPlayerStorage limboPlayerStorage) {
this.settings = settings;
this.permissionsManager = permissionsManager;
this.spawnLoader = spawnLoader;
this.limboPlayerStorage = limboPlayerStorage;
}
/**
* Load player data if exist, otherwise current player's data will be stored.
*
* @param player Player instance to add.
*/
public void addPlayerData(Player player) {
String name = player.getName().toLowerCase();
Location location = spawnLoader.getPlayerLocationOrSpawn(player);
boolean operator = player.isOp();
boolean flyEnabled = player.getAllowFlight();
float walkSpeed = player.getWalkSpeed();
float flySpeed = player.getFlySpeed();
String playerGroup = "";
if (permissionsManager.hasGroupSupport()) {
playerGroup = permissionsManager.getPrimaryGroup(player);
}
if (limboPlayerStorage.hasData(player)) {
LimboPlayer cache = limboPlayerStorage.readData(player);
if (cache != null) {
location = cache.getLocation();
playerGroup = cache.getGroup();
operator = cache.isOperator();
flyEnabled = cache.isCanFly();
walkSpeed = cache.getWalkSpeed();
flySpeed = cache.getFlySpeed();
}
} else {
limboPlayerStorage.saveData(player);
}
cache.put(name, new LimboPlayer(location, operator, playerGroup, flyEnabled, walkSpeed, flySpeed));
}
/**
* Restore player's data to player if exist.
*
* @param player Player instance to restore
*/
public void restoreData(Player player) {
String lowerName = player.getName().toLowerCase();
if (cache.containsKey(lowerName)) {
LimboPlayer data = cache.get(lowerName);
player.setOp(data.isOperator());
player.setAllowFlight(data.isCanFly());
float walkSpeed = data.getWalkSpeed();
float flySpeed = data.getFlySpeed();
// Reset the speed value if it was 0
if(walkSpeed < 0.01f) {
walkSpeed = 0.2f;
}
if(flySpeed < 0.01f) {
flySpeed = 0.2f;
}
player.setWalkSpeed(walkSpeed);
player.setFlySpeed(flySpeed);
restoreGroup(player, data.getGroup());
data.clearTasks();
}
}
/**
* Remove PlayerData from cache and disk.
*
* @param player Player player to remove.
*/
public void deletePlayerData(Player player) {
removeFromCache(player);
limboPlayerStorage.removeData(player);
}
/**
* Remove PlayerData from cache.
*
* @param player player to remove.
*/
public void removeFromCache(Player player) {
String name = player.getName().toLowerCase();
LimboPlayer cachedPlayer = cache.remove(name);
if (cachedPlayer != null) {
cachedPlayer.clearTasks();
}
}
/**
* Method getPlayerData.
*
* @param name String
*
* @return PlayerData
*/
public LimboPlayer getPlayerData(String name) {
checkNotNull(name);
return cache.get(name.toLowerCase());
}
/**
* Method hasPlayerData.
*
* @param name String
*
* @return boolean
*/
public boolean hasPlayerData(String name) {
checkNotNull(name);
return cache.containsKey(name.toLowerCase());
}
/**
* Method updatePlayerData.
*
* @param player Player
*/
public void updatePlayerData(Player player) {
checkNotNull(player);
removeFromCache(player);
addPlayerData(player);
}
private void restoreGroup(Player player, String group) {
if (!StringUtils.isEmpty(group) && permissionsManager.hasGroupSupport()
&& settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)) {
permissionsManager.setGroup(player, group);
}
}
}

View File

@ -10,6 +10,9 @@ import org.bukkit.scheduler.BukkitTask;
*/
public class LimboPlayer {
public static final float DEFAULT_WALK_SPEED = 0.2f;
public static final float DEFAULT_FLY_SPEED = 0.1f;
private final boolean canFly;
private final boolean operator;
private final String group;
@ -115,13 +118,7 @@ public class LimboPlayer {
* Clears all tasks associated to the player.
*/
public void clearTasks() {
if (messageTask != null) {
messageTask.cancel();
}
messageTask = null;
if (timeoutTask != null) {
timeoutTask.cancel();
}
timeoutTask = null;
setMessageTask(null);
setTimeoutTask(null);
}
}

View File

@ -0,0 +1,97 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.task.MessageTask;
import fr.xephi.authme.task.TimeoutTask;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import javax.inject.Inject;
import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND;
/**
* Registers tasks associated with a LimboPlayer.
*/
class LimboPlayerTaskManager {
@Inject
private Messages messages;
@Inject
private Settings settings;
@Inject
private BukkitService bukkitService;
@Inject
private PlayerCache playerCache;
LimboPlayerTaskManager() {
}
/**
* Registers a {@link MessageTask} for the given player name.
*
* @param player the player
* @param limbo the associated limbo player of the player
* @param isRegistered whether the player is registered or not
* (false shows "please register", true shows "please log in")
*/
void registerMessageTask(Player player, LimboPlayer limbo, boolean isRegistered) {
int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL);
MessageKey key = getMessageKey(isRegistered);
if (interval > 0) {
MessageTask messageTask = new MessageTask(player, messages.retrieve(key));
bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, interval * TICKS_PER_SECOND);
limbo.setMessageTask(messageTask);
}
}
/**
* Registers a {@link TimeoutTask} for the given player according to the configuration.
*
* @param player the player to register a timeout task for
* @param limbo the associated limbo player
*/
void registerTimeoutTask(Player player, LimboPlayer limbo) {
final int timeout = settings.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND;
if (timeout > 0) {
String message = messages.retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR);
BukkitTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout);
limbo.setTimeoutTask(task);
}
}
/**
* Null-safe method to set the muted flag on a message task.
*
* @param task the task to modify (or null)
* @param isMuted the value to set if task is not null
*/
static void setMuted(MessageTask task, boolean isMuted) {
if (task != null) {
task.setMuted(isMuted);
}
}
/**
* Returns the appropriate message key according to the registration status and settings.
*
* @param isRegistered whether or not the username is registered
* @return the message key to display to the user
*/
private static MessageKey getMessageKey(boolean isRegistered) {
if (isRegistered) {
return MessageKey.LOGIN_MESSAGE;
} else {
return MessageKey.REGISTER_MESSAGE;
}
}
}

View File

@ -0,0 +1,170 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.settings.Settings;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_ALLOW_FLIGHT;
import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_FLY_SPEED;
import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_WALK_SPEED;
/**
* Service for managing players that are in "limbo," a temporary state players are
* put in which have joined but not yet logged in yet.
*/
public class LimboService {
private final Map<String, LimboPlayer> entries = new ConcurrentHashMap<>();
@Inject
private Settings settings;
@Inject
private LimboPlayerTaskManager taskManager;
@Inject
private LimboServiceHelper helper;
@Inject
private LimboPersistence persistence;
LimboService() {
}
/**
* Creates a LimboPlayer for the given player and revokes all "limbo data" from the player.
*
* @param player the player to process
* @param isRegistered whether or not the player is registered
*/
public void createLimboPlayer(Player player, boolean isRegistered) {
final String name = player.getName().toLowerCase();
LimboPlayer limboFromDisk = persistence.getLimboPlayer(player);
if (limboFromDisk != null) {
ConsoleLogger.debug("LimboPlayer for `{0}` already exists on disk", name);
}
LimboPlayer existingLimbo = entries.remove(name);
if (existingLimbo != null) {
existingLimbo.clearTasks();
ConsoleLogger.debug("LimboPlayer for `{0}` already present in memory", name);
}
LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered), limboPlayer);
taskManager.registerMessageTask(player, limboPlayer, isRegistered);
taskManager.registerTimeoutTask(player, limboPlayer);
helper.revokeLimboStates(player);
entries.put(name, limboPlayer);
persistence.saveLimboPlayer(player, limboPlayer);
}
/**
* Returns the limbo player for the given name, or null otherwise.
*
* @param name the name to retrieve the data for
* @return the associated limbo player, or null if none available
*/
public LimboPlayer getLimboPlayer(String name) {
return entries.get(name.toLowerCase());
}
/**
* Returns whether there is a limbo player for the given name.
*
* @param name the name to check
* @return true if present, false otherwise
*/
public boolean hasLimboPlayer(String name) {
return entries.containsKey(name.toLowerCase());
}
/**
* Restores the limbo data and subsequently deletes the entry.
* <p>
* Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and
* changing the permission group is handled by {@link fr.xephi.authme.permission.AuthGroupHandler}.
*
* @param player the player whose data should be restored
*/
public void restoreData(Player player) {
String lowerName = player.getName().toLowerCase();
LimboPlayer limbo = entries.remove(lowerName);
if (limbo == null) {
ConsoleLogger.debug("No LimboPlayer found for `{0}` - cannot restore", lowerName);
} else {
player.setOp(limbo.isOperator());
settings.getProperty(RESTORE_ALLOW_FLIGHT).restoreAllowFlight(player, limbo);
settings.getProperty(RESTORE_FLY_SPEED).restoreFlySpeed(player, limbo);
settings.getProperty(RESTORE_WALK_SPEED).restoreWalkSpeed(player, limbo);
limbo.clearTasks();
ConsoleLogger.debug("Restored LimboPlayer stats for `{0}`", lowerName);
persistence.removeLimboPlayer(player);
}
}
/**
* Creates new tasks for the given player and cancels the old ones for a newly registered player.
* This resets his time to log in (TimeoutTask) and updates the message he is shown (MessageTask).
*
* @param player the player to reset the tasks for
*/
public void replaceTasksAfterRegistration(Player player) {
getLimboOrLogError(player, "reset tasks")
.ifPresent(limbo -> {
taskManager.registerTimeoutTask(player, limbo);
taskManager.registerMessageTask(player, limbo, true);
});
}
/**
* Resets the message task associated with the player's LimboPlayer.
*
* @param player the player to set a new message task for
* @param isRegistered whether or not the player is registered
*/
public void resetMessageTask(Player player, boolean isRegistered) {
getLimboOrLogError(player, "reset message task")
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, isRegistered));
}
/**
* @param player the player whose message task should be muted
*/
public void muteMessageTask(Player player) {
getLimboOrLogError(player, "mute message task")
.ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), true));
}
/**
* @param player the player whose message task should be unmuted
*/
public void unmuteMessageTask(Player player) {
getLimboOrLogError(player, "unmute message task")
.ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), false));
}
/**
* Returns the limbo player for the given player or logs an error.
*
* @param player the player to retrieve the limbo player for
* @param context the action for which the limbo player is being retrieved (for logging)
* @return Optional with the limbo player
*/
private Optional<LimboPlayer> getLimboOrLogError(Player player, String context) {
LimboPlayer limbo = entries.get(player.getName().toLowerCase());
if (limbo == null) {
ConsoleLogger.debug("No LimboPlayer found for `{0}`. Action: {1}", player.getName(), context);
}
return Optional.ofNullable(limbo);
}
}

View File

@ -0,0 +1,106 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Helper class for the LimboService.
*/
class LimboServiceHelper {
@Inject
private SpawnLoader spawnLoader;
@Inject
private PermissionsManager permissionsManager;
@Inject
private Settings settings;
/**
* Creates a LimboPlayer with the given player's details.
*
* @param player the player to process
* @param isRegistered whether the player is registered
* @return limbo player with the player's data
*/
LimboPlayer createLimboPlayer(Player player, boolean isRegistered) {
Location location = spawnLoader.getPlayerLocationOrSpawn(player);
// For safety reasons an unregistered player should not have OP status after registration
boolean isOperator = isRegistered && player.isOp();
boolean flyEnabled = player.getAllowFlight();
float walkSpeed = player.getWalkSpeed();
float flySpeed = player.getFlySpeed();
String playerGroup = permissionsManager.hasGroupSupport()
? permissionsManager.getPrimaryGroup(player) : "";
ConsoleLogger.debug("Player `{0}` has primary group `{1}`", player.getName(), playerGroup);
return new LimboPlayer(location, isOperator, playerGroup, flyEnabled, walkSpeed, flySpeed);
}
/**
* Removes the data that is saved in a LimboPlayer from the player.
* <p>
* Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and
* changing the permission group is handled by {@link fr.xephi.authme.permission.AuthGroupHandler}.
*
* @param player the player to set defaults to
*/
void revokeLimboStates(Player player) {
player.setOp(false);
player.setAllowFlight(false);
if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)) {
player.setFlySpeed(0.0f);
player.setWalkSpeed(0.0f);
}
}
/**
* Merges two existing LimboPlayer instances of a player. Merging is done the following way:
* <ul>
* <li><code>isOperator, allowFlight</code>: true if either limbo has true</li>
* <li><code>flySpeed, walkSpeed</code>: maximum value of either limbo player</li>
* <li><code>group, location</code>: from old limbo if not empty/null, otherwise from new limbo</li>
* </ul>
*
* @param newLimbo the new limbo player
* @param oldLimbo the old limbo player
* @return merged limbo player if both arguments are not null, otherwise the first non-null argument
*/
LimboPlayer merge(LimboPlayer newLimbo, LimboPlayer oldLimbo) {
if (newLimbo == null) {
return oldLimbo;
} else if (oldLimbo == null) {
return newLimbo;
}
boolean isOperator = newLimbo.isOperator() || oldLimbo.isOperator();
boolean canFly = newLimbo.isCanFly() || oldLimbo.isCanFly();
float flySpeed = Math.max(newLimbo.getFlySpeed(), oldLimbo.getFlySpeed());
float walkSpeed = Math.max(newLimbo.getWalkSpeed(), oldLimbo.getWalkSpeed());
String group = firstNotEmpty(newLimbo.getGroup(), oldLimbo.getGroup());
Location location = firstNotNull(oldLimbo.getLocation(), newLimbo.getLocation());
return new LimboPlayer(location, isOperator, group, canFly, walkSpeed, flySpeed);
}
private static String firstNotEmpty(String newGroup, String oldGroup) {
ConsoleLogger.debug("Limbo merge: new and old perm groups are `{0}` and `{1}`", newGroup, oldGroup);
if ("".equals(oldGroup)) {
return newGroup;
}
return oldGroup;
}
private static Location firstNotNull(Location first, Location second) {
return first == null ? second : first;
}
}

View File

@ -0,0 +1,81 @@
package fr.xephi.authme.data.limbo;
import org.bukkit.entity.Player;
/**
* Possible types to restore the walk and fly speed from LimboPlayer
* back to Bukkit Player.
*/
public enum WalkFlySpeedRestoreType {
/** Restores from LimboPlayer to Player. */
RESTORE {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
player.setFlySpeed(limbo.getFlySpeed());
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
player.setWalkSpeed(limbo.getWalkSpeed());
}
},
/** Restores from LimboPlayer, using the default speed if the speed on LimboPlayer is 0. */
RESTORE_NO_ZERO {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
float limboFlySpeed = limbo.getFlySpeed();
player.setFlySpeed(limboFlySpeed > 0.01f ? limboFlySpeed : LimboPlayer.DEFAULT_FLY_SPEED);
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
float limboWalkSpeed = limbo.getWalkSpeed();
player.setWalkSpeed(limboWalkSpeed > 0.01f ? limboWalkSpeed : LimboPlayer.DEFAULT_WALK_SPEED);
}
},
/** Uses the max speed of Player (current speed) and the LimboPlayer. */
MAX_RESTORE {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
player.setFlySpeed(Math.max(player.getFlySpeed(), limbo.getFlySpeed()));
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
player.setWalkSpeed(Math.max(player.getWalkSpeed(), limbo.getWalkSpeed()));
}
},
/** Always sets the default speed to the player. */
DEFAULT {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
}
};
/**
* Restores the fly speed from Limbo to Player according to the restoration type.
*
* @param player the player to modify
* @param limbo the limbo player to read from
*/
public abstract void restoreFlySpeed(Player player, LimboPlayer limbo);
/**
* Restores the walk speed from Limbo to Player according to the restoration type.
*
* @param player the player to modify
* @param limbo the limbo player to read from
*/
public abstract void restoreWalkSpeed(Player player, LimboPlayer limbo);
}

View File

@ -0,0 +1,79 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.initialization.factory.Factory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Handles the persistence of LimboPlayers.
*/
public class LimboPersistence implements SettingsDependent {
private final Factory<LimboPersistenceHandler> handlerFactory;
private LimboPersistenceHandler handler;
@Inject
LimboPersistence(Settings settings, Factory<LimboPersistenceHandler> handlerFactory) {
this.handlerFactory = handlerFactory;
reload(settings);
}
/**
* Retrieves the LimboPlayer for the given player if available.
*
* @param player the player to retrieve the LimboPlayer for
* @return the player's limbo player, or null if not available
*/
public LimboPlayer getLimboPlayer(Player player) {
try {
return handler.getLimboPlayer(player);
} catch (Exception e) {
ConsoleLogger.logException("Could not get LimboPlayer for '" + player.getName() + "'", e);
}
return null;
}
/**
* Saves the given LimboPlayer for the provided player.
*
* @param player the player to save the LimboPlayer for
* @param limbo the limbo player to save
*/
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
try {
handler.saveLimboPlayer(player, limbo);
} catch (Exception e) {
ConsoleLogger.logException("Could not save LimboPlayer for '" + player.getName() + "'", e);
}
}
/**
* Removes the LimboPlayer for the given player.
*
* @param player the player whose LimboPlayer should be removed
*/
public void removeLimboPlayer(Player player) {
try {
handler.removeLimboPlayer(player);
} catch (Exception e) {
ConsoleLogger.logException("Could not remove LimboPlayer for '" + player.getName() + "'", e);
}
}
@Override
public void reload(Settings settings) {
LimboPersistenceType persistenceType = settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE);
// If we're changing from an existing handler, output a quick hint that nothing is converted.
if (handler != null && handler.getType() != persistenceType) {
ConsoleLogger.info("Limbo persistence type has changed! Note that the data is not converted.");
}
handler = handlerFactory.newInstance(persistenceType.getImplementationClass());
}
}

View File

@ -0,0 +1,39 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Handles I/O for storing LimboPlayer objects.
*/
interface LimboPersistenceHandler {
/**
* Returns the limbo player for the given player if it exists.
*
* @param player the player
* @return the stored limbo player, or null if not available
*/
LimboPlayer getLimboPlayer(Player player);
/**
* Saves the given limbo player for the given player to the disk.
*
* @param player the player to save the limbo player for
* @param limbo the limbo player to save
*/
void saveLimboPlayer(Player player, LimboPlayer limbo);
/**
* Removes the limbo player from the disk.
*
* @param player the player whose limbo player should be removed
*/
void removeLimboPlayer(Player player);
/**
* @return the type of the limbo persistence implementation
*/
LimboPersistenceType getType();
}

View File

@ -0,0 +1,37 @@
package fr.xephi.authme.data.limbo.persistence;
/**
* Types of persistence for LimboPlayer objects.
*/
public enum LimboPersistenceType {
/** Store each LimboPlayer in a separate file. */
INDIVIDUAL_FILES(SeparateFilePersistenceHandler.class),
/** Store all LimboPlayers in the same file. */
SINGLE_FILE(SingleFilePersistenceHandler.class),
/** Distribute LimboPlayers by segments into a set number of files. */
SEGMENT_FILES(SegmentFilesPersistenceHolder.class),
/** No persistence to disk. */
DISABLED(NoOpPersistenceHandler.class);
private final Class<? extends LimboPersistenceHandler> implementationClass;
/**
* Constructor.
*
* @param implementationClass the implementation class
*/
LimboPersistenceType(Class<? extends LimboPersistenceHandler> implementationClass) {
this.implementationClass= implementationClass;
}
/**
* @return class implementing the persistence type
*/
public Class<? extends LimboPersistenceHandler> getImplementationClass() {
return implementationClass;
}
}

View File

@ -0,0 +1,115 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.Location;
import org.bukkit.World;
import java.lang.reflect.Type;
import java.util.function.Function;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.CAN_FLY;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.FLY_SPEED;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.GROUP;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.IS_OP;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOCATION;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_PITCH;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_WORLD;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_X;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Y;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_YAW;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Z;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.WALK_SPEED;
/**
* Converts a JsonElement to a LimboPlayer.
*/
class LimboPlayerDeserializer implements JsonDeserializer<LimboPlayer> {
private BukkitService bukkitService;
LimboPlayerDeserializer(BukkitService bukkitService) {
this.bukkitService = bukkitService;
}
@Override
public LimboPlayer deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject == null) {
return null;
}
Location loc = deserializeLocation(jsonObject);
boolean operator = getBoolean(jsonObject, IS_OP);
String group = getString(jsonObject, GROUP);
boolean canFly = getBoolean(jsonObject, CAN_FLY);
float walkSpeed = getFloat(jsonObject, WALK_SPEED, LimboPlayer.DEFAULT_WALK_SPEED);
float flySpeed = getFloat(jsonObject, FLY_SPEED, LimboPlayer.DEFAULT_FLY_SPEED);
return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed);
}
private Location deserializeLocation(JsonObject jsonObject) {
JsonElement e;
if ((e = jsonObject.getAsJsonObject(LOCATION)) != null) {
JsonObject locationObject = e.getAsJsonObject();
World world = bukkitService.getWorld(getString(locationObject, LOC_WORLD));
if (world != null) {
double x = getDouble(locationObject, LOC_X);
double y = getDouble(locationObject, LOC_Y);
double z = getDouble(locationObject, LOC_Z);
float yaw = getFloat(locationObject, LOC_YAW);
float pitch = getFloat(locationObject, LOC_PITCH);
return new Location(world, x, y, z, yaw, pitch);
}
}
return null;
}
private static String getString(JsonObject jsonObject, String memberName) {
JsonElement element = jsonObject.get(memberName);
return element != null ? element.getAsString() : "";
}
private static boolean getBoolean(JsonObject jsonObject, String memberName) {
JsonElement element = jsonObject.get(memberName);
return element != null && element.getAsBoolean();
}
private static float getFloat(JsonObject jsonObject, String memberName) {
return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, 0.0f);
}
private static float getFloat(JsonObject jsonObject, String memberName, float defaultValue) {
return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, defaultValue);
}
private static double getDouble(JsonObject jsonObject, String memberName) {
return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsDouble, 0.0);
}
/**
* Gets a number from the given JsonElement safely.
*
* @param jsonElement the element to retrieve the number from
* @param numberFunction the function to get the number from the element
* @param defaultValue the value to return if the element is null or the number cannot be retrieved
* @param <N> the number type
* @return the number from the given JSON element, or the default value
*/
private static <N extends Number> N getNumberFromElement(JsonElement jsonElement,
Function<JsonElement, N> numberFunction,
N defaultValue) {
if (jsonElement != null) {
try {
return numberFunction.apply(jsonElement);
} catch (NumberFormatException ignore) {
}
}
return defaultValue;
}
}

View File

@ -0,0 +1,52 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.Location;
import java.lang.reflect.Type;
/**
* Converts a LimboPlayer to a JsonElement.
*/
class LimboPlayerSerializer implements JsonSerializer<LimboPlayer> {
static final String LOCATION = "location";
static final String LOC_WORLD = "world";
static final String LOC_X = "x";
static final String LOC_Y = "y";
static final String LOC_Z = "z";
static final String LOC_YAW = "yaw";
static final String LOC_PITCH = "pitch";
static final String GROUP = "group";
static final String IS_OP = "operator";
static final String CAN_FLY = "can-fly";
static final String WALK_SPEED = "walk-speed";
static final String FLY_SPEED = "fly-speed";
@Override
public JsonElement serialize(LimboPlayer limboPlayer, Type type, JsonSerializationContext context) {
Location loc = limboPlayer.getLocation();
JsonObject locationObject = new JsonObject();
locationObject.addProperty(LOC_WORLD, loc.getWorld().getName());
locationObject.addProperty(LOC_X, loc.getX());
locationObject.addProperty(LOC_Y, loc.getY());
locationObject.addProperty(LOC_Z, loc.getZ());
locationObject.addProperty(LOC_YAW, loc.getYaw());
locationObject.addProperty(LOC_PITCH, loc.getPitch());
JsonObject obj = new JsonObject();
obj.add(LOCATION, locationObject);
obj.addProperty(GROUP, limboPlayer.getGroup());
obj.addProperty(IS_OP, limboPlayer.isOperator());
obj.addProperty(CAN_FLY, limboPlayer.isCanFly());
obj.addProperty(WALK_SPEED, limboPlayer.getWalkSpeed());
obj.addProperty(FLY_SPEED, limboPlayer.getFlySpeed());
return obj;
}
}

View File

@ -0,0 +1,30 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Limbo player persistence implementation that does nothing.
*/
class NoOpPersistenceHandler implements LimboPersistenceHandler {
@Override
public LimboPlayer getLimboPlayer(Player player) {
return null;
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
// noop
}
@Override
public void removeLimboPlayer(Player player) {
// noop
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.DISABLED;
}
}

View File

@ -0,0 +1,94 @@
package fr.xephi.authme.data.limbo.persistence;
/**
* Configuration for the total number of segments to use.
* <p>
* The {@link SegmentFilesPersistenceHolder} reduces the number of files by assigning each UUID
* to a segment. This enum allows to define how many segments the UUIDs should be distributed in.
* <p>
* Segments are defined by a <b>distribution</b> and a <b>length.</b> The distribution defines
* to how many outputs a single hexadecimal characters should be mapped. So e.g. a distribution
* of 3 means that all hexadecimal characters 0-f should be distributed over three different
* outputs evenly. The {@link SegmentNameBuilder} simply uses hexadecimal characters as outputs,
* so e.g. with a distribution of 3 all hex characters 0-f are mapped to 0, 1, or 2.
* <p>
* To ensure an even distribution the segments must be powers of 2. Trivially, to implement a
* distribution of 16, the same character may be returned as was input (since 0-f make up 16
* characters). A distribution of 1, on the other hand, means that the same output is returned
* regardless of the input character.
* <p>
* The <b>length</b> parameter defines how many characters of a player's UUID should be used to
* create the segment ID. In other words, with a distribution of 2 and a length of 3, the first
* three characters of the UUID are taken into consideration, each mapped to one of two possible
* characters. For instance, a UUID starting with "0f5c9321" may yield the segment ID "010."
* Such a segment ID defines in which file the given UUID can be found and stored.
* <p>
* The number of segments such a configuration yields is computed as {@code distribution ^ length},
* since distribution defines how many outputs there are per digit, and length defines the number
* of digits. For instance, a distribution of 2 and a length of 3 will yield segment IDs 000, 001,
* 010, 011, 100, 101, 110 and 111 (i.e. all binary numbers from 0 to 7).
* <p>
* There are multiple possibilities to achieve certain segment totals, e.g. 8 different segments
* may be created by setting distribution to 8 and length to 1, or distr. to 2 and length to 3.
* Where possible, prefer a length of 1 (no string concatenation required) or a distribution of
* 16 (no remapping of the characters required).
*/
public enum SegmentConfiguration {
/** 1. */
ONE(1, 1),
///** 2. */
//TWO(2, 1),
/** 4. */
FOUR(4, 1),
/** 8. */
EIGHT(8, 1),
/** 16. */
SIXTEEN(16, 1),
/** 32. */
THIRTY_TWO(2, 5),
/** 64. */
SIXTY_FOUR(4, 3),
/** 128. */
ONE_TWENTY(2, 7),
/** 256. */
TWO_FIFTY(16, 2);
private final int distribution;
private final int length;
SegmentConfiguration(int distribution, int length) {
this.distribution = distribution;
this.length = length;
}
/**
* @return the distribution size per character, i.e. how many possible outputs there are
* for any hexadecimal character
*/
public int getDistribution() {
return distribution;
}
/**
* @return number of characters from a UUID that should be used to create a segment ID
*/
public int getLength() {
return length;
}
/**
* @return number of segments to which this configuration will distribute UUIDs
*/
public int getTotalSegments() {
return (int) Math.pow(distribution, length);
}
}

View File

@ -0,0 +1,226 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.io.Files;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Persistence handler for LimboPlayer objects by distributing the objects to store
* in various segments (buckets) based on the start of the player's UUID.
*/
class SegmentFilesPersistenceHolder implements LimboPersistenceHandler {
private static final Type LIMBO_MAP_TYPE = new TypeToken<Map<String, LimboPlayer>>(){}.getType();
private final File cacheFolder;
private final Gson gson;
private final SegmentNameBuilder segmentNameBuilder;
@Inject
SegmentFilesPersistenceHolder(@DataFolder File dataFolder, BukkitService bukkitService, Settings settings) {
cacheFolder = new File(dataFolder, "playerdata");
if (!cacheFolder.exists()) {
// TODO ljacqu 20170313: Create FileUtils#mkdirs
cacheFolder.mkdirs();
}
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService))
.setPrettyPrinting()
.create();
segmentNameBuilder = new SegmentNameBuilder(settings.getProperty(LimboSettings.SEGMENT_DISTRIBUTION));
convertOldDataToCurrentSegmentScheme();
deleteEmptyFiles();
}
@Override
public LimboPlayer getLimboPlayer(Player player) {
String uuid = PlayerUtils.getUUIDorName(player);
File file = getPlayerSegmentFile(uuid);
Map<String, LimboPlayer> entries = readLimboPlayers(file);
return entries == null ? null : entries.get(uuid);
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
String uuid = PlayerUtils.getUUIDorName(player);
File file = getPlayerSegmentFile(uuid);
Map<String, LimboPlayer> entries = null;
if (file.exists()) {
entries = readLimboPlayers(file);
} else {
FileUtils.create(file);
}
/* intentionally separate if */
if (entries == null) {
entries = new HashMap<>();
}
entries.put(PlayerUtils.getUUIDorName(player), limbo);
saveEntries(entries, file);
}
@Override
public void removeLimboPlayer(Player player) {
String uuid = PlayerUtils.getUUIDorName(player);
File file = getPlayerSegmentFile(uuid);
if (file.exists()) {
Map<String, LimboPlayer> entries = readLimboPlayers(file);
if (entries != null && entries.remove(PlayerUtils.getUUIDorName(player)) != null) {
saveEntries(entries, file);
}
}
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.SEGMENT_FILES;
}
private void saveEntries(Map<String, LimboPlayer> entries, File file) {
try (FileWriter fw = new FileWriter(file)) {
gson.toJson(entries, fw);
} catch (Exception e) {
ConsoleLogger.logException("Could not write to '" + file + "':", e);
}
}
private Map<String, LimboPlayer> readLimboPlayers(File file) {
if (!file.exists()) {
return null;
}
try {
return gson.fromJson(Files.toString(file, StandardCharsets.UTF_8), LIMBO_MAP_TYPE);
} catch (Exception e) {
ConsoleLogger.logException("Failed reading '" + file + "':", e);
}
return null;
}
private File getPlayerSegmentFile(String uuid) {
String segment = segmentNameBuilder.createSegmentName(uuid);
return new File(cacheFolder, segment + "-limbo.json");
}
/**
* Loads segment files in the cache folder that don't correspond to the current segmenting scheme
* and migrates the data into files of the current segments. This allows a player to change the
* segment size without any loss of data.
*/
private void convertOldDataToCurrentSegmentScheme() {
String currentPrefix = segmentNameBuilder.getPrefix();
File[] files = listFiles(cacheFolder);
Map<String, LimboPlayer> allLimboPlayers = new HashMap<>();
List<File> migratedFiles = new ArrayList<>();
for (File file : files) {
if (isLimboJsonFile(file) && !file.getName().startsWith(currentPrefix)) {
Map<String, LimboPlayer> data = readLimboPlayers(file);
if (data != null) {
allLimboPlayers.putAll(data);
migratedFiles.add(file);
}
}
}
if (!allLimboPlayers.isEmpty()) {
saveToNewSegments(allLimboPlayers);
migratedFiles.forEach(FileUtils::delete);
}
}
/**
* Saves the LimboPlayer data read from old segmenting schemes into the current segmenting scheme.
*
* @param limbosFromOldSegments the limbo players to store into the current segment files
*/
private void saveToNewSegments(Map<String, LimboPlayer> limbosFromOldSegments) {
Map<String, Map<String, LimboPlayer>> limboBySegment = groupBySegment(limbosFromOldSegments);
ConsoleLogger.info("Saving " + limbosFromOldSegments.size() + " LimboPlayers from old segments into "
+ limboBySegment.size() + " current segments");
for (Map.Entry<String, Map<String, LimboPlayer>> entry : limboBySegment.entrySet()) {
File file = new File(cacheFolder, entry.getKey() + "-limbo.json");
Map<String, LimboPlayer> limbosToSave = Optional.ofNullable(readLimboPlayers(file))
.orElseGet(HashMap::new);
limbosToSave.putAll(entry.getValue());
saveEntries(limbosToSave, file);
}
}
/**
* Converts a Map of UUID to LimboPlayers to a 2-dimensional Map of LimboPlayers by segment ID and UUID.
* {@code Map(uuid -> LimboPlayer) to Map(segment -> Map(uuid -> LimboPlayer))}
*
* @param readLimboPlayers the limbo players to order by segment
* @return limbo players ordered by segment ID and associated player UUID
*/
private Map<String, Map<String, LimboPlayer>> groupBySegment(Map<String, LimboPlayer> readLimboPlayers) {
Map<String, Map<String, LimboPlayer>> limboBySegment = new HashMap<>();
for (Map.Entry<String, LimboPlayer> entry : readLimboPlayers.entrySet()) {
String segmentId = segmentNameBuilder.createSegmentName(entry.getKey());
limboBySegment.computeIfAbsent(segmentId, s -> new HashMap<>())
.put(entry.getKey(), entry.getValue());
}
return limboBySegment;
}
/**
* Deletes files from the current segmenting scheme that are empty.
*/
private void deleteEmptyFiles() {
File[] files = listFiles(cacheFolder);
long deletedFiles = Arrays.stream(files)
// typically the size is 2 because there's an empty JSON map: {}
.filter(f -> isLimboJsonFile(f) && f.length() < 3)
.peek(FileUtils::delete)
.count();
ConsoleLogger.debug("Limbo: Deleted {0} empty segment files", deletedFiles);
}
/**
* @param file the file to check
* @return true if it is a segment file storing Limbo data, false otherwise
*/
private static boolean isLimboJsonFile(File file) {
String name = file.getName();
return name.startsWith("seg") && name.endsWith("-limbo.json");
}
private static File[] listFiles(File folder) {
File[] files = folder.listFiles();
if (files == null) {
ConsoleLogger.warning("Could not get files of '" + folder + "'");
return new File[0];
}
return files;
}
}

View File

@ -0,0 +1,73 @@
package fr.xephi.authme.data.limbo.persistence;
import java.util.HashMap;
import java.util.Map;
/**
* Creates segment names for {@link SegmentFilesPersistenceHolder}.
*/
class SegmentNameBuilder {
private final int length;
private final int distribution;
private final String prefix;
private final Map<Character, Character> charToSegmentChar;
/**
* Constructor.
*
* @param partition the segment configuration
*/
SegmentNameBuilder(SegmentConfiguration partition) {
this.length = partition.getLength();
this.distribution = partition.getDistribution();
this.prefix = "seg" + partition.getTotalSegments() + "-";
this.charToSegmentChar = buildCharMap(distribution);
}
/**
* Returns the segment ID for the given UUID.
*
* @param uuid the player's uuid to get the segment for
* @return id the uuid belongs to
*/
String createSegmentName(String uuid) {
if (distribution == 16) {
return prefix + uuid.substring(0, length);
} else {
return prefix + buildSegmentName(uuid.substring(0, length).toCharArray());
}
}
/**
* @return the prefix used for the current segment configuration
*/
String getPrefix() {
return prefix;
}
private String buildSegmentName(char[] chars) {
if (chars.length == 1) {
return String.valueOf(charToSegmentChar.get(chars[0]));
}
StringBuilder sb = new StringBuilder(chars.length);
for (char chr : chars) {
sb.append(charToSegmentChar.get(chr));
}
return sb.toString();
}
private static Map<Character, Character> buildCharMap(int distribution) {
final char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
final int divisor = 16 / distribution;
Map<Character, Character> charToSegmentChar = new HashMap<>();
for (int i = 0; i < hexChars.length; ++i) {
int mappedChar = i / divisor;
charToSegmentChar.put(hexChars[i], hexChars[mappedChar]);
}
return charToSegmentChar;
}
}

View File

@ -0,0 +1,90 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Saves LimboPlayer objects as JSON into individual files.
*/
class SeparateFilePersistenceHandler implements LimboPersistenceHandler {
private final Gson gson;
private final File cacheDir;
@Inject
SeparateFilePersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) {
cacheDir = new File(dataFolder, "playerdata");
if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) {
ConsoleLogger.warning("Failed to create playerdata directory '" + cacheDir + "'");
}
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService))
.setPrettyPrinting()
.create();
}
@Override
public LimboPlayer getLimboPlayer(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id + File.separator + "data.json");
if (!file.exists()) {
return null;
}
try {
String str = Files.toString(file, StandardCharsets.UTF_8);
return gson.fromJson(str, LimboPlayer.class);
} catch (IOException e) {
ConsoleLogger.logException("Could not read player data on disk for '" + player.getName() + "'", e);
return null;
}
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limboPlayer) {
String id = PlayerUtils.getUUIDorName(player);
try {
File file = new File(cacheDir, id + File.separator + "data.json");
Files.createParentDirs(file);
Files.touch(file);
Files.write(gson.toJson(limboPlayer), file, StandardCharsets.UTF_8);
} catch (IOException e) {
ConsoleLogger.logException("Failed to write " + player.getName() + " data:", e);
}
}
/**
* Removes the LimboPlayer. This will delete the
* "playerdata/&lt;uuid or name&gt;/" folder from disk.
*
* @param player player to remove
*/
@Override
public void removeLimboPlayer(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id);
if (file.exists()) {
FileUtils.purgeDirectory(file);
FileUtils.delete(file);
}
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.INDIVIDUAL_FILES;
}
}

View File

@ -0,0 +1,94 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Saves all LimboPlayers in one JSON file and keeps the entries in memory.
*/
class SingleFilePersistenceHandler implements LimboPersistenceHandler {
private final File cacheFile;
private final Gson gson;
private Map<String, LimboPlayer> entries;
@Inject
SingleFilePersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) {
cacheFile = new File(dataFolder, "limbo.json");
if (!cacheFile.exists()) {
FileUtils.create(cacheFile);
}
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService))
.setPrettyPrinting()
.create();
Type type = new TypeToken<ConcurrentMap<String, LimboPlayer>>(){}.getType();
try (FileReader fr = new FileReader(cacheFile)) {
entries = gson.fromJson(fr, type);
} catch (IOException e) {
ConsoleLogger.logException("Failed to read from '" + cacheFile + "':", e);
}
if (entries == null) {
entries = new ConcurrentHashMap<>();
}
}
@Override
public LimboPlayer getLimboPlayer(Player player) {
return entries.get(PlayerUtils.getUUIDorName(player));
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
entries.put(PlayerUtils.getUUIDorName(player), limbo);
saveEntries("adding '" + player.getName() + "'");
}
@Override
public void removeLimboPlayer(Player player) {
LimboPlayer entry = entries.remove(PlayerUtils.getUUIDorName(player));
if (entry != null) {
saveEntries("removing '" + player.getName() + "'");
}
}
/**
* Saves the entries to the disk.
*
* @param action the reason for saving (for logging purposes)
*/
private void saveEntries(String action) {
try (FileWriter fw = new FileWriter(cacheFile)) {
gson.toJson(entries, fw);
} catch (IOException e) {
ConsoleLogger.logException("Failed saving JSON limbo cache after " + action, e);
}
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.SINGLE_FILE;
}
}

View File

@ -6,6 +6,8 @@ import fr.xephi.authme.settings.properties.DatabaseSettings;
/**
* Database column names.
*/
// Justification: String is immutable and this class is used to easily access the configurable column names
@SuppressWarnings({"checkstyle:VisibilityModifier", "checkstyle:MemberName", "checkstyle:AbbreviationAsWordInName"})
public final class Columns {
public final String NAME;

View File

@ -7,7 +7,7 @@ import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.security.crypts.XFBCRYPT;
import fr.xephi.authme.security.crypts.XfBCrypt;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
@ -31,7 +31,7 @@ import java.util.Set;
public class MySQL implements DataSource {
private boolean useSSL;
private boolean useSsl;
private String host;
private String port;
private String username;
@ -45,7 +45,10 @@ public class MySQL implements DataSource {
private HikariDataSource ds;
private String phpBbPrefix;
private String ipbPrefix;
private int phpBbGroup;
private int ipbGroup;
private int xfGroup;
private String wordpressPrefix;
public MySQL(Settings settings) throws ClassNotFoundException, SQLException {
@ -96,12 +99,15 @@ public class MySQL implements DataSource {
this.hashAlgorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH);
this.phpBbPrefix = settings.getProperty(HooksSettings.PHPBB_TABLE_PREFIX);
this.phpBbGroup = settings.getProperty(HooksSettings.PHPBB_ACTIVATED_GROUP_ID);
this.ipbPrefix = settings.getProperty(HooksSettings.IPB_TABLE_PREFIX);
this.ipbGroup = settings.getProperty(HooksSettings.IPB_ACTIVATED_GROUP_ID);
this.xfGroup = settings.getProperty(HooksSettings.XF_ACTIVATED_GROUP_ID);
this.wordpressPrefix = settings.getProperty(HooksSettings.WORDPRESS_TABLE_PREFIX);
this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE);
if (poolSize == -1) {
poolSize = Utils.getCoreCount();
poolSize = Utils.getCoreCount()*3;
}
this.useSSL = settings.getProperty(DatabaseSettings.MYSQL_USE_SSL);
this.useSsl = settings.getProperty(DatabaseSettings.MYSQL_USE_SSL);
}
private void setConnectionArguments() {
@ -119,7 +125,7 @@ public class MySQL implements DataSource {
ds.setPassword(this.password);
// Request mysql over SSL
ds.addDataSourceProperty("useSSL", useSSL);
ds.addDataSourceProperty("useSSL", useSsl);
// Encoding
ds.addDataSourceProperty("characterEncoding", "utf8");
@ -286,7 +292,7 @@ public class MySQL implements DataSource {
if (rs.next()) {
Blob blob = rs.getBlob("data");
byte[] bytes = blob.getBytes(1, (int) blob.length());
auth.setPassword(new HashedPassword(XFBCRYPT.getHashFromBlob(bytes)));
auth.setPassword(new HashedPassword(XfBCrypt.getHashFromBlob(bytes)));
}
}
}
@ -334,8 +340,39 @@ public class MySQL implements DataSource {
pst.close();
}
}
if (hashAlgorithm == HashAlgorithm.PHPBB) {
if (hashAlgorithm == HashAlgorithm.IPB4){
sql = "SELECT " + col.ID + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
pst = con.prepareStatement(sql);
pst.setString(1, auth.getNickname());
rs = pst.executeQuery();
if (rs.next()){
// Update player group in core_members
sql = "UPDATE " + ipbPrefix + tableName + " SET "+ tableName + ".member_group_id=? WHERE " + col.NAME + "=?;";
pst2 = con.prepareStatement(sql);
pst2.setInt(1, ipbGroup);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
pst2.close();
// Get current time without ms
long time = System.currentTimeMillis() / 1000;
// update joined date
sql = "UPDATE " + ipbPrefix + tableName + " SET "+ tableName + ".joined=? WHERE " + col.NAME + "=?;";
pst2 = con.prepareStatement(sql);
pst2.setLong(1, time);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
pst2.close();
// Update last_visit
sql = "UPDATE " + ipbPrefix + tableName + " SET " + tableName + ".last_visit=? WHERE " + col.NAME + "=?;";
pst2 = con.prepareStatement(sql);
pst2.setLong(1, time);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
pst2.close();
}
rs.close();
pst.close();
} else if (hashAlgorithm == HashAlgorithm.PHPBB) {
sql = "SELECT " + col.ID + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
pst = con.prepareStatement(sql);
pst.setString(1, auth.getNickname());
@ -479,17 +516,51 @@ public class MySQL implements DataSource {
rs = pst.executeQuery();
if (rs.next()) {
int id = rs.getInt(col.ID);
// Insert player password, salt in xf_user_authenticate
sql = "INSERT INTO xf_user_authenticate (user_id, scheme_class, data) VALUES (?,?,?)";
pst2 = con.prepareStatement(sql);
pst2.setInt(1, id);
pst2.setString(2, XFBCRYPT.SCHEME_CLASS);
String serializedHash = XFBCRYPT.serializeHash(auth.getPassword().getHash());
pst2.setString(2, XfBCrypt.SCHEME_CLASS);
String serializedHash = XfBCrypt.serializeHash(auth.getPassword().getHash());
byte[] bytes = serializedHash.getBytes();
Blob blob = con.createBlob();
blob.setBytes(1, bytes);
pst2.setBlob(3, blob);
pst2.executeUpdate();
pst2.close();
// Update player group in xf_users
sql = "UPDATE " + tableName + " SET "+ tableName + ".user_group_id=? WHERE " + col.NAME + "=?;";
pst2 = con.prepareStatement(sql);
pst2.setInt(1, xfGroup);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
pst2.close();
// Update player permission combination in xf_users
sql = "UPDATE " + tableName + " SET "+ tableName + ".permission_combination_id=? WHERE " + col.NAME + "=?;";
pst2 = con.prepareStatement(sql);
pst2.setInt(1, xfGroup);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
pst2.close();
// Insert player privacy combination in xf_user_privacy
sql = "INSERT INTO xf_user_privacy (user_id, allow_view_profile, allow_post_profile, allow_send_personal_conversation, allow_view_identities, allow_receive_news_feed) VALUES (?,?,?,?,?,?)";
pst2 = con.prepareStatement(sql);
pst2.setInt(1, id);
pst2.setString(2, "everyone");
pst2.setString(3, "members");
pst2.setString(4, "members");
pst2.setString(5, "everyone");
pst2.setString(6, "everyone");
pst2.executeUpdate();
pst2.close();
// Insert player group relation in xf_user_group_relation
sql = "INSERT INTO xf_user_group_relation (user_id, user_group_id, is_primary) VALUES (?,?,?)";
pst2 = con.prepareStatement(sql);
pst2.setInt(1, id);
pst2.setInt(2, xfGroup);
pst2.setString(3, "1");
pst2.executeUpdate();
pst2.close();
}
rs.close();
pst.close();
@ -538,7 +609,7 @@ public class MySQL implements DataSource {
// Insert password in the correct table
sql = "UPDATE xf_user_authenticate SET data=? WHERE " + col.ID + "=?;";
PreparedStatement pst2 = con.prepareStatement(sql);
String serializedHash = XFBCRYPT.serializeHash(password.getHash());
String serializedHash = XfBCrypt.serializeHash(password.getHash());
byte[] bytes = serializedHash.getBytes();
Blob blob = con.createBlob();
blob.setBytes(1, bytes);
@ -549,7 +620,7 @@ public class MySQL implements DataSource {
// ...
sql = "UPDATE xf_user_authenticate SET scheme_class=? WHERE " + col.ID + "=?;";
pst2 = con.prepareStatement(sql);
pst2.setString(1, XFBCRYPT.SCHEME_CLASS);
pst2.setString(1, XfBCrypt.SCHEME_CLASS);
pst2.setInt(2, id);
pst2.executeUpdate();
pst2.close();
@ -824,7 +895,7 @@ public class MySQL implements DataSource {
if (rs2.next()) {
Blob blob = rs2.getBlob("data");
byte[] bytes = blob.getBytes(1, (int) blob.length());
pAuth.setPassword(new HashedPassword(XFBCRYPT.getHashFromBlob(bytes)));
pAuth.setPassword(new HashedPassword(XfBCrypt.getHashFromBlob(bytes)));
}
rs2.close();
}
@ -856,7 +927,7 @@ public class MySQL implements DataSource {
if (rs2.next()) {
Blob blob = rs2.getBlob("data");
byte[] bytes = blob.getBytes(1, (int) blob.length());
pAuth.setPassword(new HashedPassword(XFBCRYPT.getHashFromBlob(bytes)));
pAuth.setPassword(new HashedPassword(XfBCrypt.getHashFromBlob(bytes)));
}
rs2.close();
}

View File

@ -37,7 +37,7 @@ public abstract class AbstractDataSourceConverter<S extends DataSource> implemen
// which is never the case when a converter is launched from the /authme converter command.
@Override
public void execute(CommandSender sender) {
if (!destinationType.equals(destination.getType())) {
if (destinationType != destination.getType()) {
if (sender != null) {
sender.sendMessage("Please configure your connection to "
+ destinationType + " and re-run this command");
@ -59,6 +59,7 @@ public abstract class AbstractDataSourceConverter<S extends DataSource> implemen
if (destination.isAuthAvailable(auth.getNickname())) {
skippedPlayers.add(auth.getNickname());
} else {
adaptPlayerAuth(auth);
destination.saveAuth(auth);
destination.updateQuitLoc(auth);
}
@ -72,6 +73,15 @@ public abstract class AbstractDataSourceConverter<S extends DataSource> implemen
+ " to " + destinationType);
}
/**
* Adapts the PlayerAuth from the source before it is saved in the destination.
*
* @param auth the auth from the source
*/
protected void adaptPlayerAuth(PlayerAuth auth) {
// noop
}
/**
* @return the data source to convert from
* @throws Exception during initialization of source

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.FlatFile;
@ -25,4 +26,11 @@ public class ForceFlatToSqlite extends AbstractDataSourceConverter<FlatFile> {
public FlatFile getSource() {
return source;
}
@Override
protected void adaptPlayerAuth(PlayerAuth auth) {
// Issue #1120: FlatFile returns PlayerAuth objects with realname = lower-case name all the time.
// We don't want to take this over into the new data source.
auth.setRealName("Player");
}
}

View File

@ -16,6 +16,7 @@ import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
@ -40,43 +41,41 @@ public class RakamakConverter implements Converter {
@Override
// TODO ljacqu 20151229: Restructure this into smaller portions
public void execute(CommandSender sender) {
boolean useIP = settings.getProperty(ConverterSettings.RAKAMAK_USE_IP);
boolean useIp = settings.getProperty(ConverterSettings.RAKAMAK_USE_IP);
String fileName = settings.getProperty(ConverterSettings.RAKAMAK_FILE_NAME);
String ipFileName = settings.getProperty(ConverterSettings.RAKAMAK_IP_FILE_NAME);
File source = new File(pluginFolder, fileName);
File ipfiles = new File(pluginFolder, ipFileName);
HashMap<String, String> playerIP = new HashMap<>();
HashMap<String, HashedPassword> playerPSW = new HashMap<>();
File ipFiles = new File(pluginFolder, ipFileName);
Map<String, String> playerIp = new HashMap<>();
Map<String, HashedPassword> playerPassword = new HashMap<>();
try {
BufferedReader users;
BufferedReader ipFile;
ipFile = new BufferedReader(new FileReader(ipfiles));
BufferedReader ipFile = new BufferedReader(new FileReader(ipFiles));
String line;
if (useIP) {
if (useIp) {
String tempLine;
while ((tempLine = ipFile.readLine()) != null) {
if (tempLine.contains("=")) {
String[] args = tempLine.split("=");
playerIP.put(args[0], args[1]);
playerIp.put(args[0], args[1]);
}
}
}
ipFile.close();
users = new BufferedReader(new FileReader(source));
BufferedReader users = new BufferedReader(new FileReader(source));
while ((line = users.readLine()) != null) {
if (line.contains("=")) {
String[] arguments = line.split("=");
HashedPassword hashedPassword = passwordSecurity.computeHash(arguments[1], arguments[0]);
playerPSW.put(arguments[0], hashedPassword);
playerPassword.put(arguments[0], hashedPassword);
}
}
users.close();
for (Entry<String, HashedPassword> m : playerPSW.entrySet()) {
for (Entry<String, HashedPassword> m : playerPassword.entrySet()) {
String playerName = m.getKey();
HashedPassword psw = playerPSW.get(playerName);
String ip = useIP ? playerIP.get(playerName) : "127.0.0.1";
HashedPassword psw = playerPassword.get(playerName);
String ip = useIp ? playerIp.get(playerName) : "127.0.0.1";
PlayerAuth auth = PlayerAuth.builder()
.name(playerName)
.realName(playerName)

View File

@ -16,13 +16,13 @@ import java.util.UUID;
import static fr.xephi.authme.util.FileUtils.makePath;
public class vAuthConverter implements Converter {
public class VAuthConverter implements Converter {
private final DataSource dataSource;
private final File vAuthPasswordsFile;
@Inject
vAuthConverter(@DataFolder File dataFolder, DataSource dataSource) {
VAuthConverter(@DataFolder File dataFolder, DataSource dataSource) {
vAuthPasswordsFile = new File(dataFolder.getParent(), makePath("vAuth", "passwords.yml"));
this.dataSource = dataSource;
}

View File

@ -6,7 +6,7 @@ import de.luricos.bukkit.xAuth.xAuth;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.util.CollectionUtils;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.PluginManager;
@ -21,7 +21,7 @@ import java.util.List;
import static fr.xephi.authme.util.FileUtils.makePath;
public class xAuthConverter implements Converter {
public class XAuthConverter implements Converter {
@Inject
@DataFolder
@ -31,7 +31,7 @@ public class xAuthConverter implements Converter {
@Inject
private PluginManager pluginManager;
xAuthConverter() {
XAuthConverter() {
}
@Override
@ -55,7 +55,7 @@ public class xAuthConverter implements Converter {
sender.sendMessage("[AuthMe] xAuth H2 database not found, checking for MySQL or SQLite data...");
}
List<Integer> players = getXAuthPlayers();
if (CollectionUtils.isEmpty(players)) {
if (Utils.isCollectionEmpty(players)) {
sender.sendMessage("[AuthMe] Error while importing xAuthPlayers: did not find any players");
return;
}

View File

@ -2,15 +2,14 @@ package fr.xephi.authme.initialization;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.backup.LimboPlayerStorage;
import fr.xephi.authme.data.limbo.LimboCache;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.PluginHookService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.ValidationService;
import org.bukkit.Location;
import org.bukkit.entity.Player;
@ -28,17 +27,15 @@ public class OnShutdownPlayerSaver {
@Inject
private ValidationService validationService;
@Inject
private LimboCache limboCache;
@Inject
private DataSource dataSource;
@Inject
private LimboPlayerStorage limboPlayerStorage;
@Inject
private SpawnLoader spawnLoader;
@Inject
private PluginHookService pluginHookService;
@Inject
private PlayerCache playerCache;
@Inject
private LimboService limboService;
OnShutdownPlayerSaver() {
}
@ -57,9 +54,8 @@ public class OnShutdownPlayerSaver {
if (pluginHookService.isNpc(player) || validationService.isUnrestricted(name)) {
return;
}
if (limboCache.hasPlayerData(name)) {
limboCache.restoreData(player);
limboCache.removeFromCache(player);
if (limboService.hasLimboPlayer(name)) {
limboService.restoreData(player);
} else {
saveLoggedinPlayer(player);
}
@ -75,9 +71,5 @@ public class OnShutdownPlayerSaver {
.location(loc).build();
dataSource.updateQuitLoc(auth);
}
if (settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)
&& !settings.getProperty(RestrictionSettings.NO_TELEPORT) && !limboPlayerStorage.hasData(player)) {
limboPlayerStorage.saveData(player);
}
}
}

View File

@ -1,11 +1,16 @@
package fr.xephi.authme.initialization;
import ch.jalu.injector.exceptions.InjectorReflectionException;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import org.bstats.Metrics;
import fr.xephi.authme.output.ConsoleFilter;
import fr.xephi.authme.output.Log4JFilter;
import fr.xephi.authme.service.BukkitService;
@ -18,10 +23,9 @@ import fr.xephi.authme.util.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.mcstats.Metrics;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Optional;
import java.util.logging.Logger;
import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE;
@ -44,34 +48,28 @@ public class OnStartupTasks {
OnStartupTasks() {
}
/**
* Sends bstats metrics.
*
* @param plugin the plugin instance
* @param settings the settings
*/
public static void sendMetrics(AuthMe plugin, Settings settings) {
try {
final Metrics metrics = new Metrics(plugin);
final Metrics.Graph languageGraph = metrics.createGraph("Messages Language");
final String messagesLanguage = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
languageGraph.addPlotter(new Metrics.Plotter(messagesLanguage) {
metrics.addCustomChart(new Metrics.SimplePie("messages_language") {
@Override
public int getValue() {
return 1;
public String getValue() {
return settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
}
});
final Metrics.Graph databaseBackend = metrics.createGraph("Database Backend");
final String dataSource = settings.getProperty(DatabaseSettings.BACKEND).toString();
databaseBackend.addPlotter(new Metrics.Plotter(dataSource) {
metrics.addCustomChart(new Metrics.SimplePie("database_backend") {
@Override
public int getValue() {
return 1;
public String getValue() {
return settings.getProperty(DatabaseSettings.BACKEND).toString();
}
});
// Submit metrics
metrics.start();
} catch (IOException e) {
// Failed to submit the metrics data
ConsoleLogger.logException("Can't send Metrics data! The plugin will work anyway...", e);
}
}
/**
@ -124,4 +122,42 @@ public class OnStartupTasks {
}
}, 1, TICKS_PER_MINUTE * settings.getProperty(EmailSettings.DELAY_RECALL));
}
/**
* Displays a hint to use the legacy AuthMe JAR if AuthMe could not be started
* because Gson was not found.
*
* @param e the exception to process
*/
public static void displayLegacyJarHint(Exception e) {
if (e instanceof InjectorReflectionException) {
Throwable causeOfCause = Optional.of(e)
.map(Throwable::getCause)
.map(Throwable::getCause).orElse(null);
if (causeOfCause instanceof NoClassDefFoundError
&& "Lcom/google/gson/Gson;".equals(causeOfCause.getMessage())) {
ConsoleLogger.warning("YOU MUST DOWNLOAD THE LEGACY JAR TO USE AUTHME ON YOUR SERVER");
ConsoleLogger.warning("Get authme-legacy.jar from http://ci.xephi.fr/job/AuthMeReloaded/");
}
}
}
/**
* Returns whether the hash algorithm is deprecated and won't be able
* to be actively used anymore in 5.4.
*
* @param hash the hash algorithm to check
* @return true if the hash will be deprecated, false otherwise
* @see <a href="https://github.com/Xephi/AuthMeReloaded/issues/1016">#1016</a>
*/
public static boolean isHashDeprecatedIn54(HashAlgorithm hash) {
if (hash.getClazz() == null || hash == HashAlgorithm.PLAINTEXT) {
// Exclude PLAINTEXT from this check because it already has a mandatory migration, which takes care of
// sending all the necessary messages and warnings.
return false;
}
Recommendation recommendation = hash.getClazz().getAnnotation(Recommendation.class);
return recommendation != null && recommendation.value() == Usage.DEPRECATED;
}
}

View File

@ -0,0 +1,19 @@
package fr.xephi.authme.initialization.factory;
/**
* Injectable factory that creates new instances of a certain type.
*
* @param <P> the parent type to which the factory is limited to
*/
public interface Factory<P> {
/**
* Creates an instance of the given class.
*
* @param clazz the class to instantiate
* @param <C> the class type
* @return new instance of the class
*/
<C extends P> C newInstance(Class<C> clazz);
}

View File

@ -0,0 +1,46 @@
package fr.xephi.authme.initialization.factory;
import ch.jalu.injector.Injector;
import ch.jalu.injector.context.ResolvedInstantiationContext;
import ch.jalu.injector.handlers.dependency.DependencyHandler;
import ch.jalu.injector.handlers.instantiation.DependencyDescription;
import ch.jalu.injector.utils.ReflectionUtils;
/**
* Dependency handler that builds {@link Factory} objects.
*/
public class FactoryDependencyHandler implements DependencyHandler {
@Override
public Object resolveValue(ResolvedInstantiationContext<?> context, DependencyDescription dependencyDescription) {
if (dependencyDescription.getType() == Factory.class) {
Class<?> genericType = ReflectionUtils.getGenericType(dependencyDescription.getGenericType());
if (genericType == null) {
throw new IllegalStateException("Factory fields must have concrete generic type. "
+ "Cannot get generic type for field in '" + context.getMappedClass() + "'");
}
return new FactoryImpl<>(genericType, context.getInjector());
}
return null;
}
private static final class FactoryImpl<P> implements Factory<P> {
private final Injector injector;
private final Class<P> parentClass;
FactoryImpl(Class<P> parentClass, Injector injector) {
this.parentClass = parentClass;
this.injector = injector;
}
@Override
public <C extends P> C newInstance(Class<C> clazz) {
if (parentClass.isAssignableFrom(clazz)) {
return injector.newInstance(clazz);
}
throw new IllegalArgumentException(clazz + " not child of " + parentClass);
}
}
}

View File

@ -0,0 +1,37 @@
package fr.xephi.authme.initialization.factory;
import java.util.Collection;
/**
* Injectable object to retrieve and create singletons of a common parent.
*
* @param <P> the parent class to which this store is limited to
*/
public interface SingletonStore<P> {
/**
* Returns the singleton of the given type, creating it if it hasn't been yet created.
*
* @param clazz the class to get the singleton for
* @param <C> type of the singleton
* @return the singleton of type {@code C}
*/
<C extends P> C getSingleton(Class<C> clazz);
/**
* Returns all existing singletons of this store's type.
*
* @return all registered singletons of type {@code P}
*/
Collection<P> retrieveAllOfType();
/**
* Returns all existing singletons of the given type.
*
* @param clazz the type to get singletons for
* @param <C> class type
* @return all registered singletons of type {@code C}
*/
<C extends P> Collection<C> retrieveAllOfType(Class<C> clazz);
}

View File

@ -0,0 +1,61 @@
package fr.xephi.authme.initialization.factory;
import ch.jalu.injector.Injector;
import ch.jalu.injector.context.ResolvedInstantiationContext;
import ch.jalu.injector.handlers.dependency.DependencyHandler;
import ch.jalu.injector.handlers.instantiation.DependencyDescription;
import ch.jalu.injector.utils.ReflectionUtils;
import java.util.Collection;
/**
* Dependency handler that builds {@link SingletonStore} objects.
*/
public class SingletonStoreDependencyHandler implements DependencyHandler {
@Override
public Object resolveValue(ResolvedInstantiationContext<?> context, DependencyDescription dependencyDescription) {
if (dependencyDescription.getType() == SingletonStore.class) {
Class<?> genericType = ReflectionUtils.getGenericType(dependencyDescription.getGenericType());
if (genericType == null) {
throw new IllegalStateException("Singleton store fields must have concrete generic type. "
+ "Cannot get generic type for field in '" + context.getMappedClass() + "'");
}
return new SingletonStoreImpl<>(genericType, context.getInjector());
}
return null;
}
private static final class SingletonStoreImpl<P> implements SingletonStore<P> {
private final Injector injector;
private final Class<P> parentClass;
SingletonStoreImpl(Class<P> parentClass, Injector injector) {
this.parentClass = parentClass;
this.injector = injector;
}
@Override
public <C extends P> C getSingleton(Class<C> clazz) {
if (parentClass.isAssignableFrom(clazz)) {
return injector.getSingleton(clazz);
}
throw new IllegalArgumentException(clazz + " not child of " + parentClass);
}
@Override
public Collection<P> retrieveAllOfType() {
return retrieveAllOfType(parentClass);
}
@Override
public <C extends P> Collection<C> retrieveAllOfType(Class<C> clazz) {
if (parentClass.isAssignableFrom(clazz)) {
return injector.retrieveAllOfType(clazz);
}
throw new IllegalArgumentException(clazz + " not child of " + parentClass);
}
}
}

View File

@ -29,7 +29,7 @@ import java.util.regex.Pattern;
/**
* Service for performing various verifications when a player joins.
*/
class OnJoinVerifier implements Reloadable {
public class OnJoinVerifier implements Reloadable {
@Inject
private Settings settings;

View File

@ -7,6 +7,7 @@ import fr.xephi.authme.message.Messages;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.AntiBotService;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.JoinMessageService;
import fr.xephi.authme.service.TeleportationService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.settings.Settings;
@ -54,8 +55,6 @@ import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAU
*/
public class PlayerListener implements Listener {
public static final Map<String, String> joinMessage = new ConcurrentHashMap<>();
@Inject
private Settings settings;
@Inject
@ -78,6 +77,8 @@ public class PlayerListener implements Listener {
private TeleportationService teleportationService;
@Inject
private ValidationService validationService;
@Inject
private JoinMessageService joinMessageService;
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
@ -175,7 +176,7 @@ public class PlayerListener implements Listener {
String customJoinMessage = settings.getProperty(RegistrationSettings.CUSTOM_JOIN_MESSAGE);
if (!customJoinMessage.isEmpty()) {
event.setJoinMessage(customJoinMessage.replace("{PLAYERNAME}", player.getName())
.replace("{DISPLAYNAME]", player.getDisplayName()));
.replace("{DISPLAYNAME}", player.getDisplayName()));
}
if (!settings.getProperty(RegistrationSettings.DELAY_JOIN_MESSAGE)) {
@ -188,7 +189,7 @@ public class PlayerListener implements Listener {
// Remove the join message while the player isn't logging in
if (joinMsg != null) {
event.setJoinMessage(null);
joinMessage.put(name, joinMsg);
joinMessageService.putMessage(name, joinMsg);
}
}

View File

@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.xephi.authme.listener.protocollib;
import com.comphenix.protocol.PacketType;
@ -46,7 +47,7 @@ class InventoryPacketAdapter extends PacketAdapter {
private static final int MAIN_SIZE = 27;
private static final int HOTBAR_SIZE = 9;
public InventoryPacketAdapter(AuthMe plugin) {
InventoryPacketAdapter(AuthMe plugin) {
super(plugin, PacketType.Play.Server.SET_SLOT, PacketType.Play.Server.WINDOW_ITEMS);
}

View File

@ -12,7 +12,7 @@ import fr.xephi.authme.data.auth.PlayerCache;
class TabCompletePacketAdapter extends PacketAdapter {
public TabCompletePacketAdapter(AuthMe plugin) {
TabCompletePacketAdapter(AuthMe plugin) {
super(plugin, ListenerPriority.NORMAL, PacketType.Play.Client.TAB_COMPLETE);
}

View File

@ -0,0 +1,125 @@
package fr.xephi.authme.mail;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.FileUtils;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.bukkit.Server;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.imageio.ImageIO;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
/**
* Creates emails and sends them.
*/
public class EmailService {
private final File dataFolder;
private final String serverName;
private final Settings settings;
private final SendMailSsl sendMailSsl;
@Inject
EmailService(@DataFolder File dataFolder, Server server, Settings settings, SendMailSsl sendMailSsl) {
this.dataFolder = dataFolder;
this.serverName = server.getServerName();
this.settings = settings;
this.sendMailSsl = sendMailSsl;
}
public boolean hasAllInformation() {
return sendMailSsl.hasAllInformation();
}
/**
* Sends an email to the user with his new password.
*
* @param name the name of the player
* @param mailAddress the player's email
* @param newPass the new password
* @return true if email could be sent, false otherwise
*/
public boolean sendPasswordMail(String name, String mailAddress, String newPass) {
if (!hasAllInformation()) {
ConsoleLogger.warning("Cannot perform email registration: not all email settings are complete");
return false;
}
HtmlEmail email;
try {
email = sendMailSsl.initializeMail(mailAddress);
} catch (EmailException e) {
ConsoleLogger.logException("Failed to create email with the given settings:", e);
return false;
}
String mailText = replaceTagsForPasswordMail(settings.getPasswordEmailMessage(), name, newPass);
// Generate an image?
File file = null;
if (settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)) {
try {
file = generateImage(name, newPass);
mailText = embedImageIntoEmailContent(file, email, mailText);
} catch (IOException | EmailException e) {
ConsoleLogger.logException(
"Unable to send new password as image for email " + mailAddress + ":", e);
}
}
boolean couldSendEmail = sendMailSsl.sendEmail(mailText, email);
FileUtils.delete(file);
return couldSendEmail;
}
public boolean sendRecoveryCode(String name, String email, String code) {
HtmlEmail htmlEmail;
try {
htmlEmail = sendMailSsl.initializeMail(email);
} catch (EmailException e) {
ConsoleLogger.logException("Failed to create email for recovery code:", e);
return false;
}
String message = replaceTagsForRecoveryCodeMail(settings.getRecoveryCodeEmailMessage(),
name, code, settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID));
return sendMailSsl.sendEmail(message, htmlEmail);
}
private File generateImage(String name, String newPass) throws IOException {
ImageGenerator gen = new ImageGenerator(newPass);
File file = new File(dataFolder, name + "_new_pass.jpg");
ImageIO.write(gen.generateImage(), "jpg", file);
return file;
}
private static String embedImageIntoEmailContent(File image, HtmlEmail email, String content)
throws EmailException {
DataSource source = new FileDataSource(image);
String tag = email.embed(source, image.getName());
return content.replace("<image />", "<img src=\"cid:" + tag + "\">");
}
private String replaceTagsForPasswordMail(String mailText, String name, String newPass) {
return mailText
.replace("<playername />", name)
.replace("<servername />", serverName)
.replace("<generatedpass />", newPass);
}
private String replaceTagsForRecoveryCodeMail(String mailText, String name, String code, int hoursValid) {
return mailText
.replace("<playername />", name)
.replace("<servername />", serverName)
.replace("<recoverycode />", code)
.replace("<hoursvalid />", String.valueOf(hoursValid));
}
}

View File

@ -1,27 +1,19 @@
package fr.xephi.authme.mail;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.LogLevel;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.util.StringUtils;
import org.apache.commons.mail.EmailConstants;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.bukkit.Server;
import javax.activation.CommandMap;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.activation.MailcapCommandMap;
import javax.imageio.ImageIO;
import javax.inject.Inject;
import javax.mail.Session;
import java.io.File;
import java.io.IOException;
import java.security.Security;
import java.util.Properties;
@ -32,18 +24,10 @@ import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_PASSWORD;
/**
* Sends emails to players on behalf of the server.
*/
public class SendMailSSL {
private final File dataFolder;
private final String serverName;
private final Settings settings;
public class SendMailSsl {
@Inject
SendMailSSL(@DataFolder File dataFolder, Server server, Settings settings) {
this.dataFolder = dataFolder;
this.serverName = server.getServerName();
this.settings = settings;
}
private Settings settings;
/**
* Returns whether all necessary settings are set for sending mails.
@ -55,76 +39,7 @@ public class SendMailSSL {
&& !settings.getProperty(MAIL_PASSWORD).isEmpty();
}
/**
* Sends an email to the user with his new password.
*
* @param name the name of the player
* @param mailAddress the player's email
* @param newPass the new password
* @return true if email could be sent, false otherwise
*/
public boolean sendPasswordMail(String name, String mailAddress, String newPass) {
if (!hasAllInformation()) {
ConsoleLogger.warning("Cannot perform email registration: not all email settings are complete");
return false;
}
HtmlEmail email;
try {
email = initializeMail(mailAddress);
} catch (EmailException e) {
ConsoleLogger.logException("Failed to create email with the given settings:", e);
return false;
}
String mailText = replaceTagsForPasswordMail(settings.getPasswordEmailMessage(), name, newPass);
// Generate an image?
File file = null;
if (settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)) {
try {
file = generateImage(name, newPass);
mailText = embedImageIntoEmailContent(file, email, mailText);
} catch (IOException | EmailException e) {
ConsoleLogger.logException(
"Unable to send new password as image for email " + mailAddress + ":", e);
}
}
boolean couldSendEmail = sendEmail(mailText, email);
FileUtils.delete(file);
return couldSendEmail;
}
public boolean sendRecoveryCode(String name, String email, String code) {
HtmlEmail htmlEmail;
try {
htmlEmail = initializeMail(email);
} catch (EmailException e) {
ConsoleLogger.logException("Failed to create email for recovery code:", e);
return false;
}
String message = replaceTagsForRecoveryCodeMail(settings.getRecoveryCodeEmailMessage(),
name, code, settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID));
return sendEmail(message, htmlEmail);
}
private File generateImage(String name, String newPass) throws IOException {
ImageGenerator gen = new ImageGenerator(newPass);
File file = new File(dataFolder, name + "_new_pass.jpg");
ImageIO.write(gen.generateImage(), "jpg", file);
return file;
}
private static String embedImageIntoEmailContent(File image, HtmlEmail email, String content)
throws EmailException {
DataSource source = new FileDataSource(image);
String tag = email.embed(source, image.getName());
return content.replace("<image />", "<img src=\"cid:" + tag + "\">");
}
@VisibleForTesting
HtmlEmail initializeMail(String emailAddress) throws EmailException {
public HtmlEmail initializeMail(String emailAddress) throws EmailException {
String senderMail = StringUtils.isEmpty(settings.getProperty(EmailSettings.MAIL_ADDRESS))
? settings.getProperty(EmailSettings.MAIL_ACCOUNT)
: settings.getProperty(EmailSettings.MAIL_ADDRESS);
@ -143,14 +58,16 @@ public class SendMailSSL {
email.setFrom(senderMail, senderName);
email.setSubject(settings.getProperty(EmailSettings.RECOVERY_MAIL_SUBJECT));
email.setAuthentication(settings.getProperty(EmailSettings.MAIL_ACCOUNT), mailPassword);
if (settings.getProperty(PluginSettings.LOG_LEVEL).includes(LogLevel.DEBUG)) {
email.setDebug(true);
}
setPropertiesForPort(email, port);
return email;
}
@VisibleForTesting
boolean sendEmail(String content, HtmlEmail email) {
Thread.currentThread().setContextClassLoader(SendMailSSL.class.getClassLoader());
public boolean sendEmail(String content, HtmlEmail email) {
Thread.currentThread().setContextClassLoader(SendMailSsl.class.getClassLoader());
// Issue #999: Prevent UnsupportedDataTypeException: no object DCH for MIME type multipart/alternative
// cf. http://stackoverflow.com/questions/21856211/unsupporteddatatypeexception-no-object-dch-for-mime-type
MailcapCommandMap mc = (MailcapCommandMap) CommandMap.getDefaultCommandMap();
@ -176,21 +93,6 @@ public class SendMailSSL {
}
}
private String replaceTagsForPasswordMail(String mailText, String name, String newPass) {
return mailText
.replace("<playername />", name)
.replace("<servername />", serverName)
.replace("<generatedpass />", newPass);
}
private String replaceTagsForRecoveryCodeMail(String mailText, String name, String code, int hoursValid) {
return mailText
.replace("<playername />", name)
.replace("<servername />", serverName)
.replace("<recoverycode />", code)
.replace("<hoursvalid />", String.valueOf(hoursValid));
}
private void setPropertiesForPort(HtmlEmail email, int port) throws EmailException {
switch (port) {
case 587:
@ -214,8 +116,10 @@ public class SendMailSSL {
}
break;
case 25:
if (settings.getProperty(EmailSettings.PORT25_USE_TLS)) {
email.setStartTLSEnabled(true);
email.setSSLCheckServerIdentity(true);
}
break;
case 465:
email.setSslSmtpPort(Integer.toString(port));

View File

@ -1,6 +1,7 @@
package fr.xephi.authme.message;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.util.FileUtils;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
@ -16,6 +17,7 @@ public class MessageFileHandler {
// regular file
private final String filename;
private final FileConfiguration configuration;
private final String updateAddition;
// default file
private final String defaultFile;
private FileConfiguration defaultConfiguration;
@ -25,11 +27,15 @@ public class MessageFileHandler {
*
* @param file the file to use for messages
* @param defaultFile the default file from the JAR to use if no message is found
* @param updateCommand command to update the messages file (nullable) to show in error messages
*/
public MessageFileHandler(File file, String defaultFile) {
public MessageFileHandler(File file, String defaultFile, String updateCommand) {
this.filename = file.getName();
this.configuration = YamlConfiguration.loadConfiguration(file);
this.defaultFile = defaultFile;
this.updateAddition = updateCommand == null
? ""
: " (or run " + updateCommand + ")";
}
/**
@ -53,7 +59,7 @@ public class MessageFileHandler {
if (message == null) {
ConsoleLogger.warning("Error getting message with key '" + key + "'. "
+ "Please update your config file '" + filename + "' (or run /authme messages)");
+ "Please update your config file '" + filename + "'" + updateAddition);
return getDefault(key);
}
return message;
@ -78,7 +84,7 @@ public class MessageFileHandler {
*/
private String getDefault(String key) {
if (defaultConfiguration == null) {
InputStream stream = MessageFileHandler.class.getClassLoader().getResourceAsStream(defaultFile);
InputStream stream = FileUtils.getResourceFromJar(defaultFile);
defaultConfiguration = YamlConfiguration.loadConfiguration(new InputStreamReader(stream));
}
String message = defaultConfiguration.getString(key);

View File

@ -36,10 +36,23 @@ public class MessageFileHandlerProvider {
* @return the message file handler
*/
public MessageFileHandler initializeHandler(Function<String, String> pathBuilder) {
return initializeHandler(pathBuilder, null);
}
/**
* Initializes a message file handler with the messages file of the configured language.
* Ensures beforehand that the messages file exists or creates it otherwise.
*
* @param pathBuilder function taking the configured language code as argument and returning the messages file
* @param updateCommand command to run to update the languages file (nullable)
* @return the message file handler
*/
public MessageFileHandler initializeHandler(Function<String, String> pathBuilder, String updateCommand) {
String language = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
return new MessageFileHandler(
initializeFile(language, pathBuilder),
pathBuilder.apply(DEFAULT_LANGUAGE));
pathBuilder.apply(DEFAULT_LANGUAGE),
updateCommand);
}
/**
@ -53,7 +66,8 @@ public class MessageFileHandlerProvider {
File initializeFile(String language, Function<String, String> pathBuilder) {
String filePath = pathBuilder.apply(language);
File file = new File(dataFolder, filePath);
if (FileUtils.copyFileFromResource(file, filePath)) {
// Check that JAR file exists to avoid logging an error
if (FileUtils.getResourceFromJar(filePath) != null && FileUtils.copyFileFromResource(file, filePath)) {
return file;
}

View File

@ -17,16 +17,13 @@ public enum MessageKey {
/** AntiBot protection mode is enabled! You have to wait some minutes before joining the server. */
KICK_ANTIBOT("kick_antibot"),
/** Can't find the requested user in the database! */
/** This user isn't registered! */
UNKNOWN_USER("unknown_user"),
/** Your quit location was unsafe, you have been teleported to the world's spawnpoint. */
UNSAFE_QUIT_LOCATION("unsafe_spawn"),
/** You're not logged in! */
NOT_LOGGED_IN("not_logged_in"),
/** Usage: /login &lt;password> */
/** Usage: /login &lt;password&gt; */
USAGE_LOGIN("usage_log"),
/** Wrong password! */
@ -56,19 +53,19 @@ public enum MessageKey {
/** An unexpected error occurred, please contact an administrator! */
ERROR("error"),
/** Please, login with the command "/login &lt;password>" */
/** Please, login with the command: /login &lt;password&gt; */
LOGIN_MESSAGE("login_msg"),
/** Please, register to the server with the command "/register &lt;password> &lt;ConfirmPassword>" */
/** Please, register to the server with the command: /register &lt;password&gt; &lt;ConfirmPassword&gt; */
REGISTER_MESSAGE("reg_msg"),
/** You have exceeded the maximum number of registrations (%reg_count/%max_acc %reg_names) for your connection! */
MAX_REGISTER_EXCEEDED("max_reg", "%max_acc", "%reg_count", "%reg_names"),
/** Usage: /register &lt;password> &lt;ConfirmPassword> */
/** Usage: /register &lt;password&gt; &lt;ConfirmPassword&gt; */
USAGE_REGISTER("usage_reg"),
/** Usage: /unregister &lt;password> */
/** Usage: /unregister &lt;password&gt; */
USAGE_UNREGISTER("usage_unreg"),
/** Password changed successfully! */
@ -95,7 +92,7 @@ public enum MessageKey {
/** You're already logged in! */
ALREADY_LOGGED_IN_ERROR("logged_in"),
/** Logged-out successfully! */
/** Logged out successfully! */
LOGOUT_SUCCESS("logout"),
/** The same username is already playing on the server! */
@ -113,7 +110,7 @@ public enum MessageKey {
/** Login timeout exceeded, you have been kicked from the server, please try again! */
LOGIN_TIMEOUT_ERROR("timeout"),
/** Usage: /changepassword &lt;oldPassword> &lt;newPassword> */
/** Usage: /changepassword &lt;oldPassword&gt; &lt;newPassword&gt; */
USAGE_CHANGE_PASSWORD("usage_changepassword"),
/** Your username is either too short or too long! */
@ -122,13 +119,13 @@ public enum MessageKey {
/** Your username contains illegal characters. Allowed chars: REG_EX */
INVALID_NAME_CHARACTERS("regex", "REG_EX"),
/** Please add your email to your account with the command "/email add &lt;yourEmail> &lt;confirmEmail>" */
/** Please add your email to your account with the command: /email add &lt;yourEmail&gt; &lt;confirmEmail&gt; */
ADD_EMAIL_MESSAGE("add_email"),
/** Forgot your password? Please use the command "/email recovery &lt;yourEmail>" */
/** Forgot your password? Please use the command: /email recovery &lt;yourEmail&gt; */
FORGOT_PASSWORD_MESSAGE("recovery_email"),
/** To login you have to solve a captcha code, please use the command "/captcha &lt;theCaptcha>" */
/** To login you have to solve a captcha code, please use the command: /captcha &lt;theCaptcha&gt; */
USAGE_CAPTCHA("usage_captcha", "<theCaptcha>"),
/** Wrong captcha, please type "/captcha THE_CAPTCHA" into the chat! */
@ -143,13 +140,13 @@ public enum MessageKey {
/** The server is full, try again later! */
KICK_FULL_SERVER("kick_fullserver"),
/** Usage: /email add &lt;email> &lt;confirmEmail> */
/** Usage: /email add &lt;email&gt; &lt;confirmEmail&gt; */
USAGE_ADD_EMAIL("usage_email_add"),
/** Usage: /email change &lt;oldEmail> &lt;newEmail> */
/** Usage: /email change &lt;oldEmail&gt; &lt;newEmail&gt; */
USAGE_CHANGE_EMAIL("usage_email_change"),
/** Usage: /email recovery &lt;Email> */
/** Usage: /email recovery &lt;Email&gt; */
USAGE_RECOVER_EMAIL("usage_email_recovery"),
/** Invalid new email, try again! */
@ -179,9 +176,6 @@ public enum MessageKey {
/** Recovery email sent successfully! Please check your email inbox! */
RECOVERY_EMAIL_SENT_MESSAGE("email_send"),
/** A recovery email was already sent! You can discard it and send a new one using the command below: */
RECOVERY_EMAIL_ALREADY_SENT_MESSAGE("email_exists"),
/** Your country is banned from this server! */
COUNTRY_BANNED_ERROR("country_banned"),
@ -224,8 +218,51 @@ public enum MessageKey {
/** A recovery code to reset your password has been sent to your email. */
RECOVERY_CODE_SENT("recovery_code_sent"),
/** The recovery code is not correct! Use /email recovery [email] to generate a new one */
INCORRECT_RECOVERY_CODE("recovery_code_incorrect");
/** The recovery code is not correct! You have %count tries remaining. */
INCORRECT_RECOVERY_CODE("recovery_code_incorrect", "%count"),
/**
* You have exceeded the maximum number of attempts to enter the recovery code.
* Use "/email recovery [email]" to generate a new one.
*/
RECOVERY_TRIES_EXCEEDED("recovery_tries_exceeded"),
/** Recovery code entered correctly! */
RECOVERY_CODE_CORRECT("recovery_code_correct"),
/** Please use the command /email setpassword to change your password immediately. */
RECOVERY_CHANGE_PASSWORD("recovery_change_password"),
/** You cannot change your password using this command anymore. */
CHANGE_PASSWORD_EXPIRED("change_password_expired"),
/** An email was already sent recently. You must wait %time before you can send a new one. */
EMAIL_COOLDOWN_ERROR("email_cooldown_error", "%time"),
/** second */
SECOND("second"),
/** seconds */
SECONDS("seconds"),
/** minute */
MINUTE("minute"),
/** minutes */
MINUTES("minutes"),
/** hour */
HOUR("hour"),
/** hours */
HOURS("hours"),
/** day */
DAY("day"),
/** days */
DAYS("days");
private String key;
private String[] tags;

View File

@ -1,11 +1,15 @@
package fr.xephi.authme.message;
import com.google.common.collect.ImmutableMap;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.util.expiring.Duration;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Class for retrieving and sending translatable messages to players.
@ -15,6 +19,20 @@ public class Messages implements Reloadable {
// Custom Authme tag replaced to new line
private static final String NEWLINE_TAG = "%nl%";
/** Contains the keys of the singular messages for time units. */
private static final Map<TimeUnit, MessageKey> TIME_UNIT_SINGULARS = ImmutableMap.<TimeUnit, MessageKey>builder()
.put(TimeUnit.SECONDS, MessageKey.SECOND)
.put(TimeUnit.MINUTES, MessageKey.MINUTE)
.put(TimeUnit.HOURS, MessageKey.HOUR)
.put(TimeUnit.DAYS, MessageKey.DAY).build();
/** Contains the keys of the plural messages for time units. */
private static final Map<TimeUnit, MessageKey> TIME_UNIT_PLURALS = ImmutableMap.<TimeUnit, MessageKey>builder()
.put(TimeUnit.SECONDS, MessageKey.SECONDS)
.put(TimeUnit.MINUTES, MessageKey.MINUTES)
.put(TimeUnit.HOURS, MessageKey.HOURS)
.put(TimeUnit.DAYS, MessageKey.DAYS).build();
private final MessageFileHandlerProvider messageFileHandlerProvider;
private MessageFileHandler messageFileHandler;
@ -71,6 +89,22 @@ public class Messages implements Reloadable {
return message.split("\n");
}
/**
* Returns the textual representation for the given duration.
* Note that this class only supports the time units days, hour, minutes and seconds.
*
* @param duration the duration to build a text of
* @return text of the duration
*/
public String formatDuration(Duration duration) {
long value = duration.getDuration();
MessageKey timeUnitKey = value == 1
? TIME_UNIT_SINGULARS.get(duration.getTimeUnit())
: TIME_UNIT_PLURALS.get(duration.getTimeUnit());
return value + " " + retrieveMessage(timeUnitKey);
}
/**
* Retrieve the message from the text file.
*
@ -107,7 +141,7 @@ public class Messages implements Reloadable {
@Override
public void reload() {
this.messageFileHandler = messageFileHandlerProvider
.initializeHandler(lang -> "messages/messages_" + lang + ".yml");
.initializeHandler(lang -> "messages/messages_" + lang + ".yml", "/authme messages");
}
private static String formatMessage(String message) {

View File

@ -1,17 +1,24 @@
package fr.xephi.authme.output;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.util.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Service class for the log filters.
*/
public final class LogFilterHelper {
final class LogFilterHelper {
private static final String ISSUED_COMMAND_TEXT = "issued server command:";
private static final String[] COMMANDS_TO_SKIP = {"/login ", "/l ", "/reg ", "/changepassword ",
"/unregister ", "/authme register ", "/authme changepassword ", "/authme reg ", "/authme cp ",
"/register "};
@VisibleForTesting
static final List<String> COMMANDS_TO_SKIP = withAndWithoutAuthMePrefix(
"/login ", "/l ", "/log ", "/register ", "/reg ", "/unregister ", "/unreg ",
"/changepassword ", "/cp ", "/changepass ", "/authme register ", "/authme reg ", "/authme r ",
"/authme changepassword ", "/authme password ", "/authme changepass ", "/authme cp ");
private LogFilterHelper() {
// Util class
@ -24,11 +31,20 @@ public final class LogFilterHelper {
*
* @return True if it is a sensitive AuthMe command, false otherwise
*/
public static boolean isSensitiveAuthMeCommand(String message) {
static boolean isSensitiveAuthMeCommand(String message) {
if (message == null) {
return false;
}
String lowerMessage = message.toLowerCase();
return lowerMessage.contains(ISSUED_COMMAND_TEXT) && StringUtils.containsAny(lowerMessage, COMMANDS_TO_SKIP);
}
private static List<String> withAndWithoutAuthMePrefix(String... commands) {
List<String> commandList = new ArrayList<>(commands.length * 2);
for (String command : commands) {
commandList.add(command);
commandList.add(command.substring(0, 1) + "authme:" + command.substring(1));
}
return Collections.unmodifiableList(commandList);
}
}

View File

@ -1,21 +1,28 @@
package fr.xephi.authme.permission;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboCache;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import org.bukkit.entity.Player;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.Optional;
/**
* Changes the permission group according to the auth status of the player and the configuration.
* <p>
* If this feature is enabled, the <i>primary permissions group</i> of a player is replaced until he has
* logged in. Some permission plugins have a notion of a primary group; for other permission plugins the
* first group is simply taken.
* <p>
* The groups that are used as replacement until the player logs in is configurable and depends on if
* the player is registered or not. Note that some (all?) permission systems require the group to actually
* exist for the replacement to take place. Furthermore, since some permission groups require that players
* be in at least one group, this will mean that the player is not removed from his primary group.
*/
public class AuthGroupHandler implements Reloadable {
@ -26,9 +33,8 @@ public class AuthGroupHandler implements Reloadable {
private Settings settings;
@Inject
private LimboCache limboCache;
private LimboService limboService;
private String unloggedInGroup;
private String unregisteredGroup;
private String registeredGroup;
@ -36,15 +42,53 @@ public class AuthGroupHandler implements Reloadable {
}
/**
* Set the group of a player, by its AuthMe group type.
* Sets the group of a player by its authentication status.
*
* @param player The player.
* @param group The group type.
*
* @return True if succeeded, false otherwise. False is also returned if groups aren't supported
* with the current permissions system.
* @param player the player
* @param groupType the group type
*/
public boolean setGroup(Player player, AuthGroupType group) {
public void setGroup(Player player, AuthGroupType groupType) {
if (!useAuthGroups()) {
return;
}
String primaryGroup = Optional
.ofNullable(limboService.getLimboPlayer(player.getName()))
.map(LimboPlayer::getGroup)
.orElse("");
switch (groupType) {
// Implementation note: some permission systems don't support players not being in any group,
// so add the new group before removing the old ones
case UNREGISTERED:
permissionsManager.addGroup(player, unregisteredGroup);
permissionsManager.removeGroups(player, registeredGroup, primaryGroup);
break;
case REGISTERED_UNAUTHENTICATED:
permissionsManager.addGroup(player, registeredGroup);
permissionsManager.removeGroups(player, unregisteredGroup, primaryGroup);
break;
case LOGGED_IN:
permissionsManager.addGroup(player, primaryGroup);
permissionsManager.removeGroups(player, unregisteredGroup, registeredGroup);
break;
default:
throw new IllegalStateException("Encountered unhandled auth group type '" + groupType + "'");
}
ConsoleLogger.debug(() -> player.getName() + " changed to "
+ groupType + ": has groups " + permissionsManager.getGroups(player));
}
/**
* Returns whether the auth permissions group function should be used.
*
* @return true if should be used, false otherwise
*/
private boolean useAuthGroups() {
// Check whether the permissions check is enabled
if (!settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)) {
return false;
@ -55,72 +99,14 @@ public class AuthGroupHandler implements Reloadable {
ConsoleLogger.warning("The current permissions system doesn't have group support, unable to set group!");
return false;
}
switch (group) {
case UNREGISTERED:
// Remove the other group type groups, set the current group
permissionsManager.removeGroups(player, Arrays.asList(registeredGroup, unloggedInGroup));
return permissionsManager.addGroup(player, unregisteredGroup);
case REGISTERED:
// Remove the other group type groups, set the current group
permissionsManager.removeGroups(player, Arrays.asList(unregisteredGroup, unloggedInGroup));
return permissionsManager.addGroup(player, registeredGroup);
case NOT_LOGGED_IN:
// Remove the other group type groups, set the current group
permissionsManager.removeGroups(player, Arrays.asList(unregisteredGroup, registeredGroup));
return permissionsManager.addGroup(player, unloggedInGroup);
case LOGGED_IN:
// Get the player data
LimboPlayer data = limboCache.getPlayerData(player.getName().toLowerCase());
if (data == null) {
return false;
}
// Get the players group
String realGroup = data.getGroup();
// Remove the other group types groups, set the real group
permissionsManager.removeGroups(player,
Arrays.asList(unregisteredGroup, registeredGroup, unloggedInGroup)
);
return permissionsManager.addGroup(player, realGroup);
default:
return false;
}
}
/**
* TODO: This method requires better explanation.
* <p>
* Set the normal group of a player.
*
* @param player The player.
* @param group The normal group.
*
* @return True on success, false on failure.
*/
public boolean addNormal(Player player, String group) {
// Check whether the permissions check is enabled
if (!settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)) {
return false;
}
// Remove old groups
permissionsManager.removeGroups(player, Arrays.asList(unregisteredGroup, registeredGroup, unloggedInGroup));
// Add the normal group, return the result
return permissionsManager.addGroup(player, group);
return true;
}
@Override
@PostConstruct
public void reload() {
unloggedInGroup = settings.getProperty(SecuritySettings.UNLOGGEDIN_GROUP);
unregisteredGroup = settings.getProperty(HooksSettings.UNREGISTERED_GROUP);
registeredGroup = settings.getProperty(HooksSettings.REGISTERED_GROUP);
unregisteredGroup = settings.getProperty(PluginSettings.UNREGISTERED_GROUP);
registeredGroup = settings.getProperty(PluginSettings.REGISTERED_GROUP);
}
}

View File

@ -8,11 +8,8 @@ public enum AuthGroupType {
/** Player does not have an account. */
UNREGISTERED,
/** Registered? */
REGISTERED, // TODO #761: Remove this or the NOT_LOGGED_IN one
/** Player is registered and not logged in. */
NOT_LOGGED_IN,
/** Player is registered but not logged in. */
REGISTERED_UNAUTHENTICATED,
/** Player is logged in. */
LOGGED_IN

View File

@ -19,8 +19,8 @@ import org.bukkit.plugin.PluginManager;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
import java.util.Collections;
/**
* <p>
@ -74,7 +74,7 @@ public class PermissionsManager implements Reloadable {
// Loop through all the available permissions system types
for (PermissionsSystemType type : PermissionsSystemType.values()) {
try {
PermissionHandler handler = getPermissionHandler(type);
PermissionHandler handler = createPermissionHandler(type);
if (handler != null) {
// Show a success message and return
this.handler = handler;
@ -91,7 +91,14 @@ public class PermissionsManager implements Reloadable {
ConsoleLogger.info("No supported permissions system found! Permissions are disabled!");
}
private PermissionHandler getPermissionHandler(PermissionsSystemType type) throws PermissionHandlerException {
/**
* Creates a permission handler for the provided permission systems if possible.
*
* @param type the permission systems type for which to create a corresponding permission handler
* @return the permission handler, or {@code null} if not possible
* @throws PermissionHandlerException during initialization of the permission handler
*/
private PermissionHandler createPermissionHandler(PermissionsSystemType type) throws PermissionHandlerException {
// Try to find the plugin for the current permissions system
Plugin plugin = pluginManager.getPlugin(type.getPluginName());
@ -255,12 +262,12 @@ public class PermissionsManager implements Reloadable {
*
* @param player The player.
*
* @return Permission groups, or an empty list if this feature is not supported.
* @return Permission groups, or an empty collection if this feature is not supported.
*/
public List<String> getGroups(Player player) {
public Collection<String> getGroups(Player player) {
// If no permissions system is used, return an empty list
if (!isEnabled())
return new ArrayList<>();
return Collections.emptyList();
return handler.getGroups(player);
}
@ -289,7 +296,7 @@ public class PermissionsManager implements Reloadable {
* @return True if the player is in the specified group, false otherwise.
* False is also returned if groups aren't supported by the used permissions system.
*/
public boolean inGroup(Player player, String groupName) {
public boolean isInGroup(Player player, String groupName) {
// If no permissions system is used, return false
if (!isEnabled())
return false;
@ -307,42 +314,12 @@ public class PermissionsManager implements Reloadable {
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean addGroup(Player player, String groupName) {
if (StringUtils.isEmpty(groupName)) {
if (!isEnabled() || StringUtils.isEmpty(groupName)) {
return false;
}
// If no permissions system is used, return false
if (!isEnabled()) {
return false;
}
return handler.addToGroup(player, groupName);
}
/**
* Add the permission groups of a player, if supported.
*
* @param player The player
* @param groupNames The name of the groups to add.
*
* @return True if succeed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean addGroups(Player player, List<String> groupNames) {
// If no permissions system is used, return false
if (!isEnabled())
return false;
// Add each group to the user
boolean result = true;
for (String groupName : groupNames)
if (!addGroup(player, groupName))
result = false;
// Return the result
return result;
}
/**
* Remove the permission group of a player, if supported.
*
@ -352,8 +329,7 @@ public class PermissionsManager implements Reloadable {
* @return True if succeed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean removeGroup(Player player, String groupName) {
// If no permissions system is used, return false
public boolean removeGroups(Player player, String groupName) {
if (!isEnabled())
return false;
@ -369,16 +345,18 @@ public class PermissionsManager implements Reloadable {
* @return True if succeed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean removeGroups(Player player, List<String> groupNames) {
public boolean removeGroups(Player player, String... groupNames) {
// If no permissions system is used, return false
if (!isEnabled())
return false;
// Add each group to the user
boolean result = true;
for (String groupName : groupNames)
if (!removeGroup(player, groupName))
for (String groupName : groupNames) {
if (!handler.removeFromGroup(player, groupName)) {
result = false;
}
}
// Return the result
return result;
@ -402,41 +380,6 @@ public class PermissionsManager implements Reloadable {
return handler.setGroup(player, groupName);
}
/**
* Set the permission groups of a player, if supported.
* This clears the current groups of the player.
*
* @param player The player
* @param groupNames The name of the groups to set.
*
* @return True if succeed, false otherwise.
* False is also returned if this feature isn't supported for the current permissions system.
*/
public boolean setGroups(Player player, List<String> groupNames) {
// If no permissions system is used or if there's no group supplied, return false
if (!isEnabled() || groupNames.isEmpty())
return false;
// Set the main group
if (!setGroup(player, groupNames.get(0)))
return false;
// Add the rest of the groups
boolean result = true;
for (int i = 1; i < groupNames.size(); i++) {
// Get the group name
String groupName = groupNames.get(i);
// Add this group
if (!addGroup(player, groupName)) {
result = false;
}
}
// Return the result
return result;
}
/**
* Remove all groups of the specified player, if supported.
* Systems like Essentials GroupManager don't allow all groups to be removed from a player, thus the user will stay
@ -453,9 +396,9 @@ public class PermissionsManager implements Reloadable {
return false;
// Get a list of current groups
List<String> groupNames = getGroups(player);
Collection<String> groupNames = getGroups(player);
// Remove each group
return removeGroups(player, groupNames);
return removeGroups(player, groupNames.toArray(new String[groupNames.size()]));
}
}

View File

@ -29,7 +29,12 @@ public enum PlayerStatePermission implements PermissionNode {
/**
* Permission to bypass the purging process.
*/
BYPASS_PURGE("authme.bypasspurge", DefaultPermission.NOT_ALLOWED);
BYPASS_PURGE("authme.bypasspurge", DefaultPermission.NOT_ALLOWED),
/**
* Permission to use the /authme debug command.
*/
DEBUG_COMMAND("authme.debug", DefaultPermission.OP_ONLY);
/**
* The permission node.

View File

@ -9,6 +9,12 @@ import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.List;
/**
* Handler for bPermissions.
*
* @see <a href="https://dev.bukkit.org/projects/bpermissions">bPermissions Bukkit page</a>
* @see <a href="https://github.com/rymate1234/bPermissions/">bPermissions on Github</a>
*/
public class BPermissionsHandler implements PermissionHandler {
@Override
@ -49,19 +55,6 @@ public class BPermissionsHandler implements PermissionHandler {
return Arrays.asList(ApiLayer.getGroups(player.getWorld().getName(), CalculableType.USER, player.getName()));
}
@Override
public String getPrimaryGroup(Player player) {
// Get the groups of the player
List<String> groups = getGroups(player);
// Make sure there is any group available, or return null
if (groups.isEmpty())
return null;
// Return the first group
return groups.get(0);
}
@Override
public PermissionsSystemType getPermissionSystem() {
return PermissionsSystemType.B_PERMISSIONS;

View File

@ -2,9 +2,10 @@ package fr.xephi.authme.permission.handlers;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PermissionsSystemType;
import fr.xephi.authme.util.Utils;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Collection;
public interface PermissionHandler {
@ -48,7 +49,9 @@ public interface PermissionHandler {
* @return True if the player is in the specified group, false otherwise.
* False is also returned if groups aren't supported by the used permissions system.
*/
boolean isInGroup(Player player, String group);
default boolean isInGroup(Player player, String group) {
return getGroups(player).contains(group);
}
/**
* Remove the permission group of a player, if supported.
@ -80,7 +83,7 @@ public interface PermissionHandler {
*
* @return Permission groups, or an empty list if this feature is not supported.
*/
List<String> getGroups(Player player);
Collection<String> getGroups(Player player);
/**
* Get the primary group of a player, if available.
@ -89,7 +92,13 @@ public interface PermissionHandler {
*
* @return The name of the primary permission group. Or null.
*/
String getPrimaryGroup(Player player);
default String getPrimaryGroup(Player player) {
Collection<String> groups = getGroups(player);
if (Utils.isCollectionEmpty(groups)) {
return null;
}
return groups.iterator().next();
}
/**
* Get the permission system that is being used.

View File

@ -12,6 +12,11 @@ import org.bukkit.plugin.PluginManager;
import java.util.ArrayList;
import java.util.List;
/**
* Handler for PermissionsBukkit.
*
* @see <a href="https://dev.bukkit.org/projects/permbukkit">PermissionsBukkit Bukkit page</a>
*/
public class PermissionsBukkitHandler implements PermissionHandler {
private PermissionsPlugin permissionsBukkitInstance;
@ -26,7 +31,8 @@ public class PermissionsBukkitHandler implements PermissionHandler {
@Override
public boolean addToGroup(Player player, String group) {
return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player addgroup " + player.getName() + " " + group);
return Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
"permissions player addgroup " + player.getName() + " " + group);
}
@Override
@ -39,46 +45,27 @@ public class PermissionsBukkitHandler implements PermissionHandler {
return false;
}
@Override
public boolean isInGroup(Player player, String group) {
List<String> groupNames = getGroups(player);
return groupNames.contains(group);
}
@Override
public boolean removeFromGroup(Player player, String group) {
return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player removegroup " + player.getName() + " " + group);
return Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
"permissions player removegroup " + player.getName() + " " + group);
}
@Override
public boolean setGroup(Player player, String group) {
return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player setgroup " + player.getName() + " " + group);
return Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
"permissions player setgroup " + player.getName() + " " + group);
}
@Override
public List<String> getGroups(Player player) {
List<String> groups = new ArrayList<String>();
List<String> groups = new ArrayList<>();
for (Group group : permissionsBukkitInstance.getGroups(player.getUniqueId())) {
groups.add(group.getName());
}
return groups;
}
@Override
public String getPrimaryGroup(Player player) {
// Get the groups of the player
List<String> groups = getGroups(player);
// Make sure there is any group available, or return null
if (groups.isEmpty()) {
return null;
}
// Return the first group
return groups.get(0);
}
@Override
public PermissionsSystemType getPermissionSystem() {
return PermissionsSystemType.PERMISSIONS_BUKKIT;

View File

@ -10,6 +10,12 @@ import ru.tehkode.permissions.bukkit.PermissionsEx;
import java.util.ArrayList;
import java.util.List;
/**
* Handler for PermissionsEx.
*
* @see <a href="https://dev.bukkit.org/projects/permissionsex">PermissionsEx Bukkit page</a>
* @see <a href="https://github.com/PEXPlugins/PermissionsEx">PermissionsEx on Github</a>
*/
public class PermissionsExHandler implements PermissionHandler {
private PermissionManager permissionManager;
@ -72,17 +78,6 @@ public class PermissionsExHandler implements PermissionHandler {
return user.getParentIdentifiers(null);
}
@Override
public String getPrimaryGroup(Player player) {
PermissionUser user = permissionManager.getUser(player);
List<String> groups = user.getParentIdentifiers(null);
if (groups.isEmpty())
return null;
return groups.get(0);
}
@Override
public PermissionsSystemType getPermissionSystem() {
return PermissionsSystemType.PERMISSIONS_EX;

Some files were not shown because too many files have changed in this diff Show More