diff --git a/.checkstyle.xml b/.checkstyle.xml new file mode 100644 index 000000000..9a91018c8 --- /dev/null +++ b/.checkstyle.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 000000000..d62f1250c --- /dev/null +++ b/.codeclimate.yml @@ -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' diff --git a/.gitignore b/.gitignore index 573eb0612..da292ac52 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml index bca4843fd..ba8dd103d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,8 @@ sudo: false +addons: + apt: + packages: + - oracle-java8-installer language: java jdk: oraclejdk8 diff --git a/README.md b/README.md index c9c717c70..0bd06437f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

The most used authentication plugin for Spigot and CraftBukkit!


-#####Links and Contacts: +##### Links and Contacts: - GitHub pages: - [Main](https://github.com/Xephi/AuthMeReloaded) (**release sources, issue tracker!**) @@ -19,41 +19,35 @@ - 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: - JavaDocs - Maven Repository -#####Statistics: - -McStats: http://mcstats.org/plugin/AuthMe - - - - - - +- Statistics: + - bStats: [AuthMe on bstats.org](https://bstats.org/plugin/bukkit/AuthMe)
-#####Compiling requirements: +##### Compiling requirements: >- JDK 1.8 >- Maven >- Git/Github (Optional) -#####How to compile the project: +##### How to compile the project: >- Clone the project with Git/Github >- Execute command "mvn clean package" -#####Running requirements: +##### Running requirements: >- Java 1.8 >- TacoSpigot, PaperSpigot, Spigot or CraftBukkit (1.7.10, 1.8.X, 1.9.X, 1.10.X, 1.11.X) >- ProtocolLib (optional, required by some features)
-###Plugin Description: +### Plugin Description: -#####"The best authentication plugin for the Bukkit/Spigot API!" +##### "The best authentication plugin for the Bukkit/Spigot API!" Prevent username stealing on your server!
Use it to secure your Offline mode server or to increase your Online mode server's protection! @@ -66,7 +60,7 @@ Each command and every feature can be enabled or disabled from our well structur You can also create your own translation file and, if you want, you can share it with us! :) -####Features: +#### Features:
  • E-Mail Recovery System !!!
  • Username spoofing protection.
  • @@ -92,7 +86,7 @@ You can also create your own translation file and, if you want, you can share it
  • DoubleSaltedMD5: SALTED2MD5
  • WordPress: WORDPRESS
-
  • Custom MySQL tables/columns names (useful with forums databases)
  • +
  • Custom MySQL tables/columns names (useful with forum databases)
  • Cached database queries!
  • Fully compatible with Citizens2, CombatTag, CombatTagPlus!
  • Compatible with Minecraft mods like BuildCraft or RedstoneCraft
  • @@ -105,18 +99,18 @@ You can also create your own translation file and, if you want, you can share it
  • Import your old database from other plugins like Rakamak, xAuth, CrazyLogin, RoyalAuth and vAuth!
  • -####Configuration +#### Configuration How to configure Authme -####Email Recovery Dependency +#### Email Recovery Dependency How to configure email recovery system? -####Commands +#### Commands [Command list and usage](https://github.com/AuthMe/AuthMeReloaded/blob/master/docs/commands.md) -####Permissions +#### Permissions - authme.player.* - for all user commands - authme.admin.* - for all admin commands - [List of all permission nodes](http://github.com/AuthMe-Team/AuthMeReloaded/blob/master/docs/permission_nodes.md) -####How To +#### How To - [How to import database from xAuth](http://dev.bukkit.org/server-mods/authme-reloaded/pages/how-to-import-database-from-xauth/) - [Website integration](http://dev.bukkit.org/server-mods/authme-reloaded/pages/web-site-integration/) - [How to convert from Rakamak](http://dev.bukkit.org/server-mods/authme-reloaded/pages/how-to-import-database-from-rakamak/) @@ -124,14 +118,14 @@ You can also create your own translation file and, if you want, you can share it
    -#####Sponsor +##### Sponsor GameHosting.it is leader in Italy as Game Server Provider. With its own DataCenter offers Anti-DDoS solutions at affordable prices. Game Server of Minecraft based on Multicraft are equipped with the latest technology in hardware. [![GameHosting](http://www.gamehosting.it/images/bn3.png)](http://www.gamehosting.it) -#####Credits +##### Credits

    Team members: look at the member list

    Credit for old version of the plugin to: d4rkwarriors, fabe1337, Whoami2 and pomo4ka

    Thanks also to: AS1LV3RN1NJA, Hoeze and eprimex

    -#####GeoIP License +##### GeoIP License This product uses data from the GeoLite API created by MaxMind, available at http://www.maxmind.com diff --git a/docs/commands.md b/docs/commands.md index b9f68054e..344b643de 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,5 +1,5 @@ - + ## 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 (`[ ]`).
    Requires `authme.admin.converter` - **/authme messages**: Adds missing messages to the current messages file.
    Requires `authme.admin.updatemessages` +- **/authme debug** [child] [params]: Allows various operations for debugging. +
    Requires `authme.debug` - **/authme help** [query]: View detailed help for /authme commands. - **/login** <password>: Command to log in using AuthMeReloaded.
    Requires `authme.player.login` @@ -69,7 +71,11 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
    Requires `authme.player.email.add` - **/email change** <oldEmail> <newEmail>: Change an email address of your account.
    Requires `authme.player.email.change` -- **/email recover** <email> [code]: Recover your account using an Email address by sending a mail containing a new password. +- **/email recover** <email>: Recover your account using an Email address by sending a mail containing a new password. +
    Requires `authme.player.email.recover` +- **/email code** <code>: Recover your account by submitting a code delivered to your email. +
    Requires `authme.player.email.recover` +- **/email setpassword** <password>: Set a new password after successfully recovering your account.
    Requires `authme.player.email.recover` - **/email help** [query]: View detailed help for /email commands. - **/captcha** <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 diff --git a/docs/config.md b/docs/config.md index 73c5eed61..13e704e7a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## 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 diff --git a/docs/hash_algorithms.md b/docs/hash_algorithms.md index 02bcafe6f..8e066dc7f 100644 --- a/docs/hash_algorithms.md +++ b/docs/hash_algorithms.md @@ -1,5 +1,5 @@ - + ## 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 diff --git a/docs/translations.md b/docs/translations.md index dee35ed79..66493be46 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -1,5 +1,5 @@ - + # 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 |   ---- | -------- | ---------: | ------ [en](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_en.yml) | English | 100% | bar -[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 69% | bar -[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 100% | bar -[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 100% | bar -[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 100% | bar -[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 100% | bar -[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 62% | bar -[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 66% | bar -[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 100% | bar -[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 70% | bar -[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 99% | bar -[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 70% | bar -[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 100% | bar -[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 72% | bar -[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 53% | bar -[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 77% | bar -[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 100% | bar -[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 86% | bar -[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 99% | bar -[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 99% | bar -[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 46% | bar -[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 81% | bar -[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 93% | bar -[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 100% | bar -[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 81% | bar -[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 81% | bar -[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 96% | bar -[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 81% | bar - +[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 95% | bar +[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 85% | bar +[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 85% | bar +[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 85% | bar +[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 95% | bar +[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 53% | bar +[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 56% | bar +[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 95% | bar +[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 60% | bar +[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 84% | bar +[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 60% | bar +[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 95% | bar +[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 61% | bar +[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 45% | bar +[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 85% | bar +[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 95% | bar +[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 95% | bar +[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 84% | bar +[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 95% | bar +[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 39% | bar +[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 95% | bar +[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 79% | bar +[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 85% | bar +[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 95% | bar +[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 69% | bar +[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 82% | bar +[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 69% | 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 diff --git a/pom.xml b/pom.xml index a79f3a9d9..8048970b3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ fr.xephi authme - 5.2 + 5.3-SNAPSHOT AuthMeReloaded The first authentication plugin for the Bukkit API! @@ -108,10 +108,6 @@ junit junit - - json-simple - com.googlecode.json-simple - persistence-api javax.persistence @@ -146,10 +142,6 @@ junit junit - - json-simple - com.googlecode.json-simple - persistence-api javax.persistence @@ -249,8 +241,8 @@ fr.xephi.authme.libs.jalu.injector - com.github.authme.configme - fr.xephi.authme.libs.authme.configme + ch.jalu.configme + fr.xephi.authme.libs.jalu.configme com.zaxxer.hikari @@ -276,10 +268,10 @@ javax.inject fr.xephi.authme.libs.javax.inject - + - org.mcstats - fr.xephi.authme + org.bstats + fr.xephi.authme.libs.org.bstats target/${project.finalName}-spigot.jar @@ -331,10 +323,10 @@ javax.inject fr.xephi.authme.libs.javax.inject - + - org.mcstats - fr.xephi.authme + org.bstats + fr.xephi.authme.libs.org.bstats target/${project.finalName}-legacy.jar @@ -442,6 +434,12 @@ xephi-repo http://ci.xephi.fr/plugin/repository/everything/ + + + + bstats-repo + http://repo.bstats.org/content/groups/public + @@ -451,7 +449,7 @@ ch.jalu injector - 0.3 + 0.4 compile true @@ -513,7 +511,7 @@ com.zaxxer HikariCP - 2.5.1 + 2.6.0 compile @@ -527,7 +525,7 @@ org.slf4j slf4j-simple - 1.7.21 + 1.7.22 compile true @@ -544,19 +542,11 @@ - + - org.mcstats.bukkit - metrics - R8-SNAPSHOT - compile - - - org.bukkit - bukkit - - - true + org.bstats + bstats-bukkit + 1.0 @@ -902,7 +892,7 @@ ch.jalu configme - 0.3 + 0.4 compile true @@ -933,7 +923,7 @@ org.mockito mockito-core test - 2.4.1 + 2.7.9 hamcrest-core diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 74a986543..f74d56067 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -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. * diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index b01674591..9d18820cd 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -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. *

    @@ -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 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)); + } } diff --git a/src/main/java/fr/xephi/authme/api/API.java b/src/main/java/fr/xephi/authme/api/API.java index d05fbe6b9..75e746e6d 100644 --- a/src/main/java/fr/xephi/authme/api/API.java +++ b/src/main/java/fr/xephi/authme/api/API.java @@ -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; diff --git a/src/main/java/fr/xephi/authme/api/NewAPI.java b/src/main/java/fr/xephi/authme/api/NewAPI.java index d4f34b2c6..62f1fefc9 100644 --- a/src/main/java/fr/xephi/authme/api/NewAPI.java +++ b/src/main/java/fr/xephi/authme/api/NewAPI.java @@ -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(); * */ +@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)); } /** diff --git a/src/main/java/fr/xephi/authme/command/CommandDescription.java b/src/main/java/fr/xephi/authme/command/CommandDescription.java index b6c006f9b..e195c4752 100644 --- a/src/main/java/fr/xephi/authme/command/CommandDescription.java +++ b/src/main/java/fr/xephi/authme/command/CommandDescription.java @@ -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"); diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index 394b6f144..083d8a531 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -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, ExecutableCommand> commands = new HashMap<>(); @Inject - CommandHandler(Injector injector, CommandMapper commandMapper, PermissionsManager permissionsManager, - Messages messages, HelpProvider helpProvider) { + CommandHandler(Factory 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 commandFactory, Set> commandClasses) { for (Class clazz : commandClasses) { - commands.put(clazz, injector.newInstance(clazz)); + commands.put(clazz, commandFactory.newInstance(clazz)); } } diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 7e01e0e30..dd27c2794 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -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) diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index 8e2b1bdc1..33003c3ae 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -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 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 parts) { - if (CollectionUtils.isEmpty(parts)) { + if (Utils.isCollectionEmpty(parts)) { return null; } diff --git a/src/main/java/fr/xephi/authme/command/ExecutableCommand.java b/src/main/java/fr/xephi/authme/command/ExecutableCommand.java index 6dd81ca80..54ee9e846 100644 --- a/src/main/java/fr/xephi/authme/command/ExecutableCommand.java +++ b/src/main/java/fr/xephi/authme/command/ExecutableCommand.java @@ -19,7 +19,7 @@ public interface ExecutableCommand { void executeCommand(CommandSender sender, List 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 (/command help) output is shown. * * @return the message explaining the command's usage, or {@code null} for default behavior diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index efa27ff09..c59c4cb94 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -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 converterFactory; @Override public void executeCommand(final CommandSender sender, List 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> getConverters() { return ImmutableMap.>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(); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java index cba5edf0d..2f1341b92 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java @@ -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 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)); } }); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java index 037846aaf..af0aaaca2 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java @@ -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 reloadableStore; + + @Inject + private SingletonStore settingsDependentStore; + @Override public void executeCommand(CommandSender sender, List 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 reloadables = injector.retrieveAllOfType(Reloadable.class); - for (Reloadable reloadable : reloadables) { - reloadable.reload(); - } + reloadableStore.retrieveAllOfType() + .forEach(r -> r.reload()); - Collection settingsDependents = injector.retrieveAllOfType(SettingsDependent.class); - for (SettingsDependent dependent : settingsDependents) { - dependent.reload(settings); - } + settingsDependentStore.retrieveAllOfType() + .forEach(s -> s.reload(settings)); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java index 791cefe0e..d042c9a65 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java @@ -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."); } /** diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java new file mode 100644 index 000000000..05de8ab13 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java @@ -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 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()); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java new file mode 100644 index 000000000..3bf28e057 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java @@ -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 singletonStore; + + @Override + public String getName() { + return "stats"; + } + + @Override + public String getDescription() { + return "Outputs general data statistics"; + } + + @Override + public void execute(CommandSender sender, List 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())); + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java new file mode 100644 index 000000000..fc7cd4291 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java @@ -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> 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 debugSectionFactory; + + private Map sections; + + @Override + public void executeCommand(CommandSender sender, List 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 arguments) { + if (arguments.isEmpty()) { + return null; + } + return getSections().get(arguments.get(0).toLowerCase()); + } + + // Lazy getter + private Map getSections() { + if (sections == null) { + Map sections = new TreeMap<>(); + for (Class sectionClass : SECTION_CLASSES) { + DebugSection section = debugSectionFactory.newInstance(sectionClass); + sections.put(section.getName(), section); + } + this.sections = sections; + } + return sections; + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSection.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSection.java new file mode 100644 index 000000000..1f0389833 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSection.java @@ -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 arguments); + +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java new file mode 100644 index 000000000..d98ce746c --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java @@ -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 the result type of the function + * + * @return player names for which there is a LimboPlayer (or error message upon failure) + */ + static U applyToLimboPlayersMap(LimboService limboService, Function 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; + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java new file mode 100644 index 000000000..436ef180c --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java @@ -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> 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 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

    the player type + */ + private static

    void performPermissionCheck( + P player, String node, BiFunction 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 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; + } + }; + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/InputValidator.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/InputValidator.java new file mode 100644 index 000000000..472025060 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/InputValidator.java @@ -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 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); + } + } + +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java new file mode 100644 index 000000000..a8c62e683 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java @@ -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 arguments) { + if (arguments.isEmpty()) { + sender.sendMessage("/authme debug limbo : 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 diskLimbo; + private final Optional memoryLimbo; + private final Optional 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 the data type + * @return this instance (for chaining) + */ + InfoDisplayer sendEntry(String title, + Function limboGetter, + Function playerGetter) { + sender.sendMessage( + title + ": " + + getData(diskLimbo, limboGetter) + + " / " + + getData(memoryLimbo, limboGetter) + + " / " + + getData(player, playerGetter)); + return this; + } + + static String getData(Optional entity, Function getter) { + return entity.map(getter).map(String::valueOf).orElse(" -- "); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PermissionGroups.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PermissionGroups.java new file mode 100644 index 000000000..a7bf19eb1 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PermissionGroups.java @@ -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 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)); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java new file mode 100644 index 000000000..a985c8271 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java @@ -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 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 length 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); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/SpawnLocationViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/SpawnLocationViewer.java new file mode 100644 index 000000000..0262055e3 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/SpawnLocationViewer.java @@ -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 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 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."); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java new file mode 100644 index 000000000..04ba276a6 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java @@ -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 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 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 "); + 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!
    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); + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java index c5756cda9..259e20f96 100644 --- a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java @@ -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 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); diff --git a/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java b/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java index 7c5e7d9dc..7c4b53c35 100644 --- a/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java @@ -54,4 +54,9 @@ public class ChangePasswordCommand extends PlayerCommand { protected String getAlternativeCommand() { return "/authme password "; } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_CHANGE_PASSWORD; + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java index eae64fdb0..87e381047 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java @@ -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; + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java index 1f9051e38..aabe99bc3 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java @@ -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; + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java new file mode 100644 index 000000000..0883c18f9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java @@ -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 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); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index 5dc16ac60..bd50cc743 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -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 arguments) { + protected void runCommand(Player player, List 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); - } else { - // Process /email recovery addr@example.com 12394 - processRecoveryCode(player, arguments.get(1), email); - } + // Recovery code is needed; generate and send one + recoveryService.createAndSendRecoveryCode(player, 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; } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java new file mode 100644 index 000000000..d5d084aa6 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java @@ -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 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()); + } + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java index 151236e1a..24a300ef8 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java @@ -24,7 +24,7 @@ public class ShowEmailCommand extends PlayerCommand { @Override public void runCommand(Player player, List 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); diff --git a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java index ac4bb4bd2..5b9d75ab1 100644 --- a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java @@ -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 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 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)); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java index 09b996a27..dd747e0e4 100644 --- a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java @@ -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 "; + } } diff --git a/src/main/java/fr/xephi/authme/command/help/HelpMessagesService.java b/src/main/java/fr/xephi/authme/command/help/HelpMessagesService.java index c2cbaff99..7058c0cb8 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpMessagesService.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpMessagesService.java @@ -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); } diff --git a/src/main/java/fr/xephi/authme/data/CaptchaManager.java b/src/main/java/fr/xephi/authme/data/CaptchaManager.java index 36f33a3cb..b328d5450 100644 --- a/src/main/java/fr/xephi/authme/data/CaptchaManager.java +++ b/src/main/java/fr/xephi/authme/data/CaptchaManager.java @@ -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 playerCounts; + private final TimedCounter playerCounts; private final ConcurrentHashMap 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(); } } diff --git a/src/main/java/fr/xephi/authme/data/SessionManager.java b/src/main/java/fr/xephi/authme/data/SessionManager.java index f86f54214..512941db6 100644 --- a/src/main/java/fr/xephi/authme/data/SessionManager.java +++ b/src/main/java/fr/xephi/authme/data/SessionManager.java @@ -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 sessions = new ConcurrentHashMap<>(); - + private final ExpiringSet 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> iterator = sessions.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - if (entry.getValue() < currentTime) { - iterator.remove(); - } + if (enabled) { + sessions.removeExpiredEntries(); } } } diff --git a/src/main/java/fr/xephi/authme/data/TempbanManager.java b/src/main/java/fr/xephi/authme/data/TempbanManager.java index e5d31ed1a..1d55fa8f5 100644 --- a/src/main/java/fr/xephi/authme/data/TempbanManager.java +++ b/src/main/java/fr/xephi/authme/data/TempbanManager.java @@ -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> ipLoginFailureCounts; + private final Map> 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 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 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 map = ipLoginFailureCounts.get(address); - if (map != null) { - map.remove(name); + TimedCounter 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 countsByName = ipLoginFailureCounts.get(address); + TimedCounter 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 countsByIp : ipLoginFailureCounts.values()) { - Iterator 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 countsByIp : ipLoginFailureCounts.values()) { + countsByIp.removeExpiredEntries(); } + ipLoginFailureCounts.entrySet().removeIf(e -> e.getValue().isEmpty()); } } diff --git a/src/main/java/fr/xephi/authme/data/backup/LimboPlayerStorage.java b/src/main/java/fr/xephi/authme/data/backup/LimboPlayerStorage.java deleted file mode 100644 index 1b2270285..000000000 --- a/src/main/java/fr/xephi/authme/data/backup/LimboPlayerStorage.java +++ /dev/null @@ -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/<uuid or name>/" 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 { - @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 { - @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; - } - } - - -} diff --git a/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java new file mode 100644 index 000000000..f085afbb4 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java @@ -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 valueGetter; + + /** + * Constructor. + * + * @param valueGetter function with which the value to set on the player can be retrieved + */ + AllowFlightRestoreType(Function 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)); + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboCache.java b/src/main/java/fr/xephi/authme/data/limbo/LimboCache.java deleted file mode 100644 index 893aba229..000000000 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboCache.java +++ /dev/null @@ -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 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); - } - } -} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java index b551deeda..6ba4ae2c5 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java @@ -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); } } diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java new file mode 100644 index 000000000..4e0c0a336 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java @@ -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; + } + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboService.java b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java new file mode 100644 index 000000000..e78ca3139 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java @@ -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 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. + *

    + * 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 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); + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java new file mode 100644 index 000000000..6b3acfcb5 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java @@ -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. + *

    + * 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: + *

      + *
    • isOperator, allowFlight: true if either limbo has true
    • + *
    • flySpeed, walkSpeed: maximum value of either limbo player
    • + *
    • group, location: from old limbo if not empty/null, otherwise from new limbo
    • + *
    + * + * @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; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java b/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java new file mode 100644 index 000000000..960cdd435 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java @@ -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); + +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java new file mode 100644 index 000000000..f07e82cd3 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java @@ -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 handlerFactory; + + private LimboPersistenceHandler handler; + + @Inject + LimboPersistence(Settings settings, Factory 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()); + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java new file mode 100644 index 000000000..95e88aada --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java @@ -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(); + +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java new file mode 100644 index 000000000..68b4611bc --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java @@ -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 implementationClass; + + /** + * Constructor. + * + * @param implementationClass the implementation class + */ + LimboPersistenceType(Class implementationClass) { + this.implementationClass= implementationClass; + } + + /** + * @return class implementing the persistence type + */ + public Class getImplementationClass() { + return implementationClass; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java new file mode 100644 index 000000000..94e1950b1 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java @@ -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 { + + 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 the number type + * @return the number from the given JSON element, or the default value + */ + private static N getNumberFromElement(JsonElement jsonElement, + Function numberFunction, + N defaultValue) { + if (jsonElement != null) { + try { + return numberFunction.apply(jsonElement); + } catch (NumberFormatException ignore) { + } + } + return defaultValue; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java new file mode 100644 index 000000000..aeae3b65b --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java @@ -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 { + + 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; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java new file mode 100644 index 000000000..ac6ff9b36 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java @@ -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; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentConfiguration.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentConfiguration.java new file mode 100644 index 000000000..5053ba521 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentConfiguration.java @@ -0,0 +1,94 @@ +package fr.xephi.authme.data.limbo.persistence; + +/** + * Configuration for the total number of segments to use. + *

    + * 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. + *

    + * Segments are defined by a distribution and a length. 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. + *

    + * 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. + *

    + * The length 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. + *

    + * 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). + *

    + * 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); + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolder.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolder.java new file mode 100644 index 000000000..e786ca482 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolder.java @@ -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>(){}.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 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 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 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 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 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 allLimboPlayers = new HashMap<>(); + List migratedFiles = new ArrayList<>(); + + for (File file : files) { + if (isLimboJsonFile(file) && !file.getName().startsWith(currentPrefix)) { + Map 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 limbosFromOldSegments) { + Map> limboBySegment = groupBySegment(limbosFromOldSegments); + + ConsoleLogger.info("Saving " + limbosFromOldSegments.size() + " LimboPlayers from old segments into " + + limboBySegment.size() + " current segments"); + for (Map.Entry> entry : limboBySegment.entrySet()) { + File file = new File(cacheFolder, entry.getKey() + "-limbo.json"); + Map 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> groupBySegment(Map readLimboPlayers) { + Map> limboBySegment = new HashMap<>(); + for (Map.Entry 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; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilder.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilder.java new file mode 100644 index 000000000..52e1141bf --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilder.java @@ -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 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 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 charToSegmentChar = new HashMap<>(); + for (int i = 0; i < hexChars.length; ++i) { + int mappedChar = i / divisor; + charToSegmentChar.put(hexChars[i], hexChars[mappedChar]); + } + return charToSegmentChar; + } + +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandler.java new file mode 100644 index 000000000..438bce69d --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandler.java @@ -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/<uuid or name>/" 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; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SingleFilePersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SingleFilePersistenceHandler.java new file mode 100644 index 000000000..57aa2dcff --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SingleFilePersistenceHandler.java @@ -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 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>(){}.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; + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/Columns.java b/src/main/java/fr/xephi/authme/datasource/Columns.java index b6d732cdb..f984007e3 100644 --- a/src/main/java/fr/xephi/authme/datasource/Columns.java +++ b/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -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; diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index f500ac072..6e09b5853 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -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()); @@ -477,19 +514,53 @@ public class MySQL implements DataSource { pst = con.prepareStatement("SELECT " + col.ID + " FROM " + tableName + " WHERE " + col.NAME + "=?;"); pst.setString(1, auth.getNickname()); rs = pst.executeQuery(); - if (rs.next()) { + 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(); } diff --git a/src/main/java/fr/xephi/authme/datasource/converter/AbstractDataSourceConverter.java b/src/main/java/fr/xephi/authme/datasource/converter/AbstractDataSourceConverter.java index d2be78047..8ca506250 100644 --- a/src/main/java/fr/xephi/authme/datasource/converter/AbstractDataSourceConverter.java +++ b/src/main/java/fr/xephi/authme/datasource/converter/AbstractDataSourceConverter.java @@ -37,7 +37,7 @@ public abstract class AbstractDataSourceConverter 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 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 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 diff --git a/src/main/java/fr/xephi/authme/datasource/converter/ForceFlatToSqlite.java b/src/main/java/fr/xephi/authme/datasource/converter/ForceFlatToSqlite.java index a52b216b6..1c67061f3 100644 --- a/src/main/java/fr/xephi/authme/datasource/converter/ForceFlatToSqlite.java +++ b/src/main/java/fr/xephi/authme/datasource/converter/ForceFlatToSqlite.java @@ -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 { 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"); + } } diff --git a/src/main/java/fr/xephi/authme/datasource/converter/RakamakConverter.java b/src/main/java/fr/xephi/authme/datasource/converter/RakamakConverter.java index eb9d904cd..4257f0aa7 100644 --- a/src/main/java/fr/xephi/authme/datasource/converter/RakamakConverter.java +++ b/src/main/java/fr/xephi/authme/datasource/converter/RakamakConverter.java @@ -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 playerIP = new HashMap<>(); - HashMap playerPSW = new HashMap<>(); + File ipFiles = new File(pluginFolder, ipFileName); + Map playerIp = new HashMap<>(); + Map 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 m : playerPSW.entrySet()) { + for (Entry 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) diff --git a/src/main/java/fr/xephi/authme/datasource/converter/vAuthConverter.java b/src/main/java/fr/xephi/authme/datasource/converter/VAuthConverter.java similarity index 95% rename from src/main/java/fr/xephi/authme/datasource/converter/vAuthConverter.java rename to src/main/java/fr/xephi/authme/datasource/converter/VAuthConverter.java index 6d3de8b7d..ff88db2d7 100644 --- a/src/main/java/fr/xephi/authme/datasource/converter/vAuthConverter.java +++ b/src/main/java/fr/xephi/authme/datasource/converter/VAuthConverter.java @@ -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; } diff --git a/src/main/java/fr/xephi/authme/datasource/converter/xAuthConverter.java b/src/main/java/fr/xephi/authme/datasource/converter/XAuthConverter.java similarity index 96% rename from src/main/java/fr/xephi/authme/datasource/converter/xAuthConverter.java rename to src/main/java/fr/xephi/authme/datasource/converter/XAuthConverter.java index 27fa6c2e9..97979c6c9 100644 --- a/src/main/java/fr/xephi/authme/datasource/converter/xAuthConverter.java +++ b/src/main/java/fr/xephi/authme/datasource/converter/XAuthConverter.java @@ -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 players = getXAuthPlayers(); - if (CollectionUtils.isEmpty(players)) { + if (Utils.isCollectionEmpty(players)) { sender.sendMessage("[AuthMe] Error while importing xAuthPlayers: did not find any players"); return; } diff --git a/src/main/java/fr/xephi/authme/initialization/OnShutdownPlayerSaver.java b/src/main/java/fr/xephi/authme/initialization/OnShutdownPlayerSaver.java index 1e7862443..e22eb9c5f 100644 --- a/src/main/java/fr/xephi/authme/initialization/OnShutdownPlayerSaver.java +++ b/src/main/java/fr/xephi/authme/initialization/OnShutdownPlayerSaver.java @@ -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); - } } } diff --git a/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java b/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java index 0dd421817..bce15af47 100644 --- a/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java +++ b/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java @@ -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,41 +48,35 @@ 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 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) { - @Override - public int getValue() { - return 1; - } - }); + metrics.addCustomChart(new Metrics.SimplePie("messages_language") { + @Override + 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) { - @Override - public int getValue() { - return 1; - } - }); - - // 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); - } + metrics.addCustomChart(new Metrics.SimplePie("database_backend") { + @Override + public String getValue() { + return settings.getProperty(DatabaseSettings.BACKEND).toString(); + } + }); } /** * Sets up the console filter if enabled. * * @param settings the settings - * @param logger the plugin logger + * @param logger the plugin logger */ public static void setupConsoleFilter(Settings settings, Logger logger) { if (!settings.getProperty(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE)) { @@ -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 #1016 + */ + 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; + } } diff --git a/src/main/java/fr/xephi/authme/initialization/factory/Factory.java b/src/main/java/fr/xephi/authme/initialization/factory/Factory.java new file mode 100644 index 000000000..0f4ae62ad --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/factory/Factory.java @@ -0,0 +1,19 @@ +package fr.xephi.authme.initialization.factory; + +/** + * Injectable factory that creates new instances of a certain type. + * + * @param

    the parent type to which the factory is limited to + */ +public interface Factory

    { + + /** + * Creates an instance of the given class. + * + * @param clazz the class to instantiate + * @param the class type + * @return new instance of the class + */ + C newInstance(Class clazz); + +} diff --git a/src/main/java/fr/xephi/authme/initialization/factory/FactoryDependencyHandler.java b/src/main/java/fr/xephi/authme/initialization/factory/FactoryDependencyHandler.java new file mode 100644 index 000000000..e063d1497 --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/factory/FactoryDependencyHandler.java @@ -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

    implements Factory

    { + + private final Injector injector; + private final Class

    parentClass; + + FactoryImpl(Class

    parentClass, Injector injector) { + this.parentClass = parentClass; + this.injector = injector; + } + + @Override + public C newInstance(Class clazz) { + if (parentClass.isAssignableFrom(clazz)) { + return injector.newInstance(clazz); + } + throw new IllegalArgumentException(clazz + " not child of " + parentClass); + } + } +} diff --git a/src/main/java/fr/xephi/authme/initialization/factory/SingletonStore.java b/src/main/java/fr/xephi/authme/initialization/factory/SingletonStore.java new file mode 100644 index 000000000..13a5cbb50 --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/factory/SingletonStore.java @@ -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

    the parent class to which this store is limited to + */ +public interface SingletonStore

    { + + /** + * 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 type of the singleton + * @return the singleton of type {@code C} + */ + C getSingleton(Class clazz); + + /** + * Returns all existing singletons of this store's type. + * + * @return all registered singletons of type {@code P} + */ + Collection

    retrieveAllOfType(); + + /** + * Returns all existing singletons of the given type. + * + * @param clazz the type to get singletons for + * @param class type + * @return all registered singletons of type {@code C} + */ + Collection retrieveAllOfType(Class clazz); + +} diff --git a/src/main/java/fr/xephi/authme/initialization/factory/SingletonStoreDependencyHandler.java b/src/main/java/fr/xephi/authme/initialization/factory/SingletonStoreDependencyHandler.java new file mode 100644 index 000000000..09a198c72 --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/factory/SingletonStoreDependencyHandler.java @@ -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

    implements SingletonStore

    { + + private final Injector injector; + private final Class

    parentClass; + + SingletonStoreImpl(Class

    parentClass, Injector injector) { + this.parentClass = parentClass; + this.injector = injector; + } + + @Override + public C getSingleton(Class clazz) { + if (parentClass.isAssignableFrom(clazz)) { + return injector.getSingleton(clazz); + } + throw new IllegalArgumentException(clazz + " not child of " + parentClass); + } + + @Override + public Collection

    retrieveAllOfType() { + return retrieveAllOfType(parentClass); + } + + @Override + public Collection retrieveAllOfType(Class clazz) { + if (parentClass.isAssignableFrom(clazz)) { + return injector.retrieveAllOfType(clazz); + } + throw new IllegalArgumentException(clazz + " not child of " + parentClass); + } + } +} diff --git a/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java index e3ff4ec5b..c7644e083 100644 --- a/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java +++ b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java @@ -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; @@ -111,7 +111,7 @@ class OnJoinVerifier implements Reloadable { * @param event the login event to verify * * @return true if the player's connection should be refused (i.e. the event does not need to be processed - * further), false if the player is not refused + * further), false if the player is not refused */ public boolean refusePlayerForFullServer(PlayerLoginEvent event) { final Player player = event.getPlayer(); diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index ddd3e430d..71fa2fc1f 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -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 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); } } diff --git a/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java b/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java index 48cf1868e..051507651 100644 --- a/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java +++ b/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + 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); } diff --git a/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java b/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java index 4476f80ad..daf686389 100644 --- a/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java +++ b/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java @@ -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); } diff --git a/src/main/java/fr/xephi/authme/mail/EmailService.java b/src/main/java/fr/xephi/authme/mail/EmailService.java new file mode 100644 index 000000000..5eb588d18 --- /dev/null +++ b/src/main/java/fr/xephi/authme/mail/EmailService.java @@ -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("", ""); + } + + private String replaceTagsForPasswordMail(String mailText, String name, String newPass) { + return mailText + .replace("", name) + .replace("", serverName) + .replace("", newPass); + } + + private String replaceTagsForRecoveryCodeMail(String mailText, String name, String code, int hoursValid) { + return mailText + .replace("", name) + .replace("", serverName) + .replace("", code) + .replace("", String.valueOf(hoursValid)); + } +} diff --git a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java b/src/main/java/fr/xephi/authme/mail/SendMailSsl.java similarity index 54% rename from src/main/java/fr/xephi/authme/mail/SendMailSSL.java rename to src/main/java/fr/xephi/authme/mail/SendMailSsl.java index a782d3a13..34011595f 100644 --- a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java +++ b/src/main/java/fr/xephi/authme/mail/SendMailSsl.java @@ -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("", ""); - } - - @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("", name) - .replace("", serverName) - .replace("", newPass); - } - - private String replaceTagsForRecoveryCodeMail(String mailText, String name, String code, int hoursValid) { - return mailText - .replace("", name) - .replace("", serverName) - .replace("", code) - .replace("", 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: - email.setStartTLSEnabled(true); - email.setSSLCheckServerIdentity(true); + if (settings.getProperty(EmailSettings.PORT25_USE_TLS)) { + email.setStartTLSEnabled(true); + email.setSSLCheckServerIdentity(true); + } break; case 465: email.setSslSmtpPort(Integer.toString(port)); diff --git a/src/main/java/fr/xephi/authme/message/MessageFileHandler.java b/src/main/java/fr/xephi/authme/message/MessageFileHandler.java index a0d4e947c..5d19e8f40 100644 --- a/src/main/java/fr/xephi/authme/message/MessageFileHandler.java +++ b/src/main/java/fr/xephi/authme/message/MessageFileHandler.java @@ -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); diff --git a/src/main/java/fr/xephi/authme/message/MessageFileHandlerProvider.java b/src/main/java/fr/xephi/authme/message/MessageFileHandlerProvider.java index 987caeccc..5b809b2c9 100644 --- a/src/main/java/fr/xephi/authme/message/MessageFileHandlerProvider.java +++ b/src/main/java/fr/xephi/authme/message/MessageFileHandlerProvider.java @@ -36,10 +36,23 @@ public class MessageFileHandlerProvider { * @return the message file handler */ public MessageFileHandler initializeHandler(Function 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 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 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; } diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index 6cc57480f..f962ba4e1 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -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 <password> */ + /** Usage: /login <password> */ 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 <password>" */ + /** Please, login with the command: /login <password> */ LOGIN_MESSAGE("login_msg"), - /** Please, register to the server with the command "/register <password> <ConfirmPassword>" */ + /** Please, register to the server with the command: /register <password> <ConfirmPassword> */ 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 <password> <ConfirmPassword> */ + /** Usage: /register <password> <ConfirmPassword> */ USAGE_REGISTER("usage_reg"), - /** Usage: /unregister <password> */ + /** Usage: /unregister <password> */ 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 <oldPassword> <newPassword> */ + /** Usage: /changepassword <oldPassword> <newPassword> */ 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 <yourEmail> <confirmEmail>" */ + /** Please add your email to your account with the command: /email add <yourEmail> <confirmEmail> */ ADD_EMAIL_MESSAGE("add_email"), - /** Forgot your password? Please use the command "/email recovery <yourEmail>" */ + /** Forgot your password? Please use the command: /email recovery <yourEmail> */ FORGOT_PASSWORD_MESSAGE("recovery_email"), - /** To login you have to solve a captcha code, please use the command "/captcha <theCaptcha>" */ + /** To login you have to solve a captcha code, please use the command: /captcha <theCaptcha> */ USAGE_CAPTCHA("usage_captcha", ""), /** 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 <email> <confirmEmail> */ + /** Usage: /email add <email> <confirmEmail> */ USAGE_ADD_EMAIL("usage_email_add"), - /** Usage: /email change <oldEmail> <newEmail> */ + /** Usage: /email change <oldEmail> <newEmail> */ USAGE_CHANGE_EMAIL("usage_email_change"), - /** Usage: /email recovery <Email> */ + /** Usage: /email recovery <Email> */ 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; diff --git a/src/main/java/fr/xephi/authme/message/Messages.java b/src/main/java/fr/xephi/authme/message/Messages.java index 86fa2db6f..d7fdcec89 100644 --- a/src/main/java/fr/xephi/authme/message/Messages.java +++ b/src/main/java/fr/xephi/authme/message/Messages.java @@ -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 TIME_UNIT_SINGULARS = ImmutableMap.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 TIME_UNIT_PLURALS = ImmutableMap.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) { diff --git a/src/main/java/fr/xephi/authme/output/LogFilterHelper.java b/src/main/java/fr/xephi/authme/output/LogFilterHelper.java index 605283ac2..7d46daf84 100644 --- a/src/main/java/fr/xephi/authme/output/LogFilterHelper.java +++ b/src/main/java/fr/xephi/authme/output/LogFilterHelper.java @@ -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 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 withAndWithoutAuthMePrefix(String... commands) { + List 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); + } } diff --git a/src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java b/src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java index 537c3ebc5..2759b636a 100644 --- a/src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java +++ b/src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java @@ -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. + *

    + * If this feature is enabled, the primary permissions group 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. + *

    + * 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. - *

    - * 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); } } diff --git a/src/main/java/fr/xephi/authme/permission/AuthGroupType.java b/src/main/java/fr/xephi/authme/permission/AuthGroupType.java index 9ab2a370c..dfedf8ee0 100644 --- a/src/main/java/fr/xephi/authme/permission/AuthGroupType.java +++ b/src/main/java/fr/xephi/authme/permission/AuthGroupType.java @@ -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 diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 7ce0b3e5a..aac2f4bba 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -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; /** *

    @@ -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 getGroups(Player player) { + public Collection 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 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 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 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 groupNames = getGroups(player); + Collection groupNames = getGroups(player); // Remove each group - return removeGroups(player, groupNames); + return removeGroups(player, groupNames.toArray(new String[groupNames.size()])); } } diff --git a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java index aaeb0eea3..2160eea56 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java @@ -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. diff --git a/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java index 9f52214b9..849ecd657 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java @@ -9,6 +9,12 @@ import org.bukkit.entity.Player; import java.util.Arrays; import java.util.List; +/** + * Handler for bPermissions. + * + * @see bPermissions Bukkit page + * @see bPermissions on Github + */ 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 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; diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java index 71c7a2879..50816ce01 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java @@ -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 { @@ -16,7 +17,7 @@ public interface PermissionHandler { * @param group The name of the group. * * @return True if succeed, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ boolean addToGroup(Player player, String group); @@ -46,9 +47,11 @@ public interface PermissionHandler { * @param group The group name. * * @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. + * 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. @@ -57,7 +60,7 @@ public interface PermissionHandler { * @param group The name of the group. * * @return True if succeed, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ boolean removeFromGroup(Player player, String group); @@ -69,7 +72,7 @@ public interface PermissionHandler { * @param group The name of the group. * * @return True if succeed, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ boolean setGroup(Player player, String group); @@ -80,7 +83,7 @@ public interface PermissionHandler { * * @return Permission groups, or an empty list if this feature is not supported. */ - List getGroups(Player player); + Collection 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 groups = getGroups(player); + if (Utils.isCollectionEmpty(groups)) { + return null; + } + return groups.iterator().next(); + } /** * Get the permission system that is being used. diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java index c215b8d17..acae466c1 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java @@ -12,6 +12,11 @@ import org.bukkit.plugin.PluginManager; import java.util.ArrayList; import java.util.List; +/** + * Handler for PermissionsBukkit. + * + * @see PermissionsBukkit Bukkit page + */ 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 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 getGroups(Player player) { - List groups = new ArrayList(); + List 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 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; diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java index b11d00003..9b4550d84 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java @@ -10,6 +10,12 @@ import ru.tehkode.permissions.bukkit.PermissionsEx; import java.util.ArrayList; import java.util.List; +/** + * Handler for PermissionsEx. + * + * @see PermissionsEx Bukkit page + * @see PermissionsEx on Github + */ 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 groups = user.getParentIdentifiers(null); - if (groups.isEmpty()) - return null; - - return groups.get(0); - } - @Override public PermissionsSystemType getPermissionSystem() { return PermissionsSystemType.PERMISSIONS_EX; diff --git a/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java index 8955569ec..79badc53e 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java @@ -10,6 +10,12 @@ import org.bukkit.plugin.RegisteredServiceProvider; import java.util.Arrays; import java.util.List; +/** + * Handler for permissions via Vault. + * + * @see Vault Bukkit page + * @see Vault on Github + */ public class VaultHandler implements PermissionHandler { private Permission vaultProvider; diff --git a/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java index 498ba4a91..c86863f78 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java @@ -6,10 +6,15 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.tyrannyofheaven.bukkit.zPermissions.ZPermissionsService; -import java.util.ArrayList; -import java.util.List; +import java.util.Collection; import java.util.Map; +/** + * Handler for zPermissions. + * + * @see zPermissions Bukkit page + * @see zPermissions on Github + */ public class ZPermissionsHandler implements PermissionHandler { private ZPermissionsService zPermissionsService; @@ -25,7 +30,8 @@ public class ZPermissionsHandler implements PermissionHandler { @Override public boolean addToGroup(Player player, String group) { - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " addgroup " + group); + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "permissions player " + player.getName() + " addgroup " + group); } @Override @@ -36,31 +42,29 @@ public class ZPermissionsHandler implements PermissionHandler { @Override public boolean hasPermissionOffline(String name, PermissionNode node) { Map perms = zPermissionsService.getPlayerPermissions(null, null, name); - if (perms.containsKey(node.getNode())) + if (perms.containsKey(node.getNode())) { return perms.get(node.getNode()); - else + } else { return false; - } - - @Override - public boolean isInGroup(Player player, String group) { - return getGroups(player).contains(group); + } } @Override public boolean removeFromGroup(Player player, String group) { - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " removegroup " + group); + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "permissions player " + player.getName() + " removegroup " + group); } @Override public boolean setGroup(Player player, String group) { - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " setgroup " + group); + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "permissions player " + player.getName() + " setgroup " + group); } @Override - public List getGroups(Player player) { + public Collection getGroups(Player player) { // TODO Gnat008 20160631: Use UUID not name? - return new ArrayList(zPermissionsService.getPlayerGroups(player.getName())); + return zPermissionsService.getPlayerGroups(player.getName()); } @Override diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index 8aea8341a..fee949574 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -8,7 +8,8 @@ import fr.xephi.authme.process.login.AsynchronousLogin; import fr.xephi.authme.process.logout.AsynchronousLogout; import fr.xephi.authme.process.quit.AsynchronousQuit; import fr.xephi.authme.process.register.AsyncRegister; -import fr.xephi.authme.process.register.executors.RegistrationExecutor; +import fr.xephi.authme.process.register.executors.RegistrationMethod; +import fr.xephi.authme.process.register.executors.RegistrationParameters; import fr.xephi.authme.process.unregister.AsynchronousUnregister; import fr.xephi.authme.service.BukkitService; import org.bukkit.command.CommandSender; @@ -60,8 +61,8 @@ public class Management { runTask(() -> asynchronousLogout.logout(player)); } - public void performRegister(Player player, RegistrationExecutor registrationExecutor) { - runTask(() -> asyncRegister.register(player, registrationExecutor)); + public

    void performRegister(RegistrationMethod

    variant, P parameters) { + runTask(() -> asyncRegister.register(variant, parameters)); } public void performUnregister(Player player, String password) { diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index dab9cdc2c..53f4d8fd0 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -1,37 +1,37 @@ package fr.xephi.authme.process.join; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.SessionManager; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.data.limbo.LimboCache; +import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.ProtectInventoryEvent; -import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.process.login.AsynchronousLogin; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PluginHookService; +import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; -import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.util.PlayerUtils; import org.bukkit.GameMode; +import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import javax.inject.Inject; -import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; +import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; /** * Asynchronous process for when a player joins. @@ -39,7 +39,7 @@ import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; public class AsynchronousJoin implements AsynchronousProcess { @Inject - private AuthMe plugin; + private Server server; @Inject private DataSource database; @@ -51,7 +51,7 @@ public class AsynchronousJoin implements AsynchronousProcess { private PlayerCache playerCache; @Inject - private LimboCache limboCache; + private LimboService limboService; @Inject private SessionManager sessionManager; @@ -62,15 +62,15 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private BukkitService bukkitService; - @Inject - private LimboPlayerTaskManager limboPlayerTaskManager; - @Inject private AsynchronousLogin asynchronousLogin; @Inject private CommandManager commandManager; + @Inject + private ValidationService validationService; + AsynchronousJoin() { } @@ -91,13 +91,13 @@ public class AsynchronousJoin implements AsynchronousProcess { pluginHookService.setEssentialsSocialSpyStatus(player, false); } - if (isNameRestricted(name, ip, player.getAddress().getHostName())) { + if (!validationService.fulfillsNameRestrictions(player)) { bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(new Runnable() { @Override public void run() { player.kickPlayer(service.retrieveSingleMessage(MessageKey.NOT_OWNER_ERROR)); if (service.getProperty(RestrictionSettings.BAN_UNKNOWN_IP)) { - plugin.getServer().banIP(ip); + server.banIP(ip); } } }); @@ -111,8 +111,7 @@ public class AsynchronousJoin implements AsynchronousProcess { final boolean isAuthAvailable = database.isAuthAvailable(name); if (isAuthAvailable) { - limboCache.addPlayerData(player); - service.setGroup(player, AuthGroupType.NOT_LOGGED_IN); + service.setGroup(player, AuthGroupType.REGISTERED_UNAUTHENTICATED); // Protect inventory if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { @@ -130,19 +129,17 @@ public class AsynchronousJoin implements AsynchronousProcess { PlayerAuth auth = database.getAuth(name); database.setUnlogged(name); playerCache.removePlayer(name); - if (auth != null && auth.getIp().equals(ip)) { - service.send(player, MessageKey.SESSION_RECONNECTION); - bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); - return; - } else if (service.getProperty(PluginSettings.SESSIONS_EXPIRE_ON_IP_CHANGE)) { - service.send(player, MessageKey.SESSION_EXPIRED); + if (auth != null) { + if (auth.getIp().equals(ip)) { + service.send(player, MessageKey.SESSION_RECONNECTION); + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); + return; + } else { + service.send(player, MessageKey.SESSION_EXPIRED); + } } } } else { - // Not Registered. Delete old data, load default one. - limboCache.deletePlayerData(player); - limboCache.addPlayerData(player); - // Groups logic service.setGroup(player, AuthGroupType.UNREGISTERED); @@ -155,12 +152,8 @@ public class AsynchronousJoin implements AsynchronousProcess { final int registrationTimeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { - player.setOp(false); - if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) - && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { - player.setFlySpeed(0.0f); - player.setWalkSpeed(0.0f); - } + limboService.createLimboPlayer(player, isAuthAvailable); + player.setNoDamageTicks(registrationTimeout); if (pluginHookService.isEssentialsAvailable() && service.getProperty(HooksSettings.USE_ESSENTIALS_MOTD)) { player.performCommand("motd"); @@ -172,40 +165,6 @@ public class AsynchronousJoin implements AsynchronousProcess { } commandManager.runCommandsOnJoin(player); }); - - // Timeout and message task - limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, isAuthAvailable); - } - - /** - * Returns whether the name is restricted based on the restriction settings. - * - * @param name The name to check - * @param ip The IP address of the player - * @param domain The hostname of the IP address - * - * @return True if the name is restricted (IP/domain is not allowed for the given name), - * false if the restrictions are met or if the name has no restrictions to it - */ - private boolean isNameRestricted(String name, String ip, String domain) { - if (!service.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS)) { - return false; - } - - boolean nameFound = false; - for (String entry : service.getProperty(RestrictionSettings.ALLOWED_RESTRICTED_USERS)) { - String[] args = entry.split(";"); - String testName = args[0]; - String testIp = args[1]; - if (testName.equalsIgnoreCase(name)) { - nameFound = true; - if ((ip != null && testIp.equals(ip)) || (domain != null && testIp.equalsIgnoreCase(domain))) { - return false; - } - } - } - return nameFound; } /** diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index db85b8581..fa0c76ab1 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -6,26 +6,24 @@ import fr.xephi.authme.data.CaptchaManager; import fr.xephi.authme.data.TempbanManager; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; -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.datasource.DataSource; import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent; +import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.AdminPermission; -import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; -import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.util.PlayerUtils; import fr.xephi.authme.util.StringUtils; import org.bukkit.ChatColor; @@ -46,15 +44,9 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private CommonService service; - @Inject - private PermissionsManager permissionsManager; - @Inject private PlayerCache playerCache; - @Inject - private LimboCache limboCache; - @Inject private SyncProcessManager syncProcessManager; @@ -71,7 +63,10 @@ public class AsynchronousLogin implements AsynchronousProcess { private TempbanManager tempbanManager; @Inject - private LimboPlayerTaskManager limboPlayerTaskManager; + private LimboService limboService; + + @Inject + private EmailService emailService; AsynchronousLogin() { } @@ -105,6 +100,7 @@ public class AsynchronousLogin implements AsynchronousProcess { * Checks the precondition for authentication (like user known) and returns * the player's {@link PlayerAuth} object. * + * @param player the player to check * @return the PlayerAuth object, or {@code null} if the player doesn't exist or may not log in * (e.g. because he is already logged in) */ @@ -119,8 +115,7 @@ public class AsynchronousLogin implements AsynchronousProcess { if (auth == null) { service.send(player, MessageKey.UNKNOWN_USER); // Recreate the message task to immediately send the message again as response - // and to make sure we send the right register message (password vs. email registration) - limboPlayerTaskManager.registerMessageTask(name, false); + limboService.resetMessageTask(player, false); return null; } @@ -195,9 +190,11 @@ public class AsynchronousLogin implements AsynchronousProcess { // If the authentication fails check if Captcha is required and send a message to the player if (captchaManager.isCaptchaRequired(player.getName())) { - limboCache.getPlayerData(player.getName()).getMessageTask().setMuted(true); + limboService.muteMessageTask(player); service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(player.getName())); + } else if (emailService.hasAllInformation()) { + service.send(player, MessageKey.FORGOT_PASSWORD_MESSAGE); } } } @@ -246,10 +243,6 @@ public class AsynchronousLogin implements AsynchronousProcess { // task, we schedule it in the end // so that we can be sure, and have not to care if it might be // processed in other order. - LimboPlayer limboPlayer = limboCache.getPlayerData(name); - if (limboPlayer != null) { - limboPlayer.clearTasks(); - } syncProcessManager.processSyncPlayerLogin(player); } else { ConsoleLogger.warning("Player '" + player.getName() + "' wasn't online during login process, aborted..."); @@ -260,7 +253,7 @@ public class AsynchronousLogin implements AsynchronousProcess { int threshold = service.getProperty(RestrictionSettings.OTHER_ACCOUNTS_CMD_THRESHOLD); String command = service.getProperty(RestrictionSettings.OTHER_ACCOUNTS_CMD); - if(threshold < 2 || command.isEmpty()) { + if (threshold < 2 || command.isEmpty()) { return; } @@ -269,17 +262,13 @@ public class AsynchronousLogin implements AsynchronousProcess { } bukkitService.dispatchConsoleCommand(command - .replaceAll("%playername%", player.getName()) - .replaceAll("%playerip%", ip) + .replace("%playername%", player.getName()) + .replace("%playerip%", ip) ); } private void displayOtherAccounts(List auths, Player player) { - if (!service.getProperty(RestrictionSettings.DISPLAY_OTHER_ACCOUNTS)) { - return; - } - - if (auths.size() <= 1) { + if (!service.getProperty(RestrictionSettings.DISPLAY_OTHER_ACCOUNTS) || auths.size() <= 1) { return; } @@ -300,10 +289,10 @@ public class AsynchronousLogin implements AsynchronousProcess { for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { if (onlinePlayer.getName().equalsIgnoreCase(player.getName()) - && permissionsManager.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS)) { + && service.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS)) { service.send(onlinePlayer, MessageKey.ACCOUNTS_OWNED_SELF, Integer.toString(auths.size())); onlinePlayer.sendMessage(message); - } else if (permissionsManager.hasPermission(onlinePlayer, AdminPermission.SEE_OTHER_ACCOUNTS)) { + } else if (service.hasPermission(onlinePlayer, AdminPermission.SEE_OTHER_ACCOUNTS)) { service.send(onlinePlayer, MessageKey.ACCOUNTS_OWNED_OTHER, player.getName(), Integer.toString(auths.size())); onlinePlayer.sendMessage(message); @@ -323,7 +312,7 @@ public class AsynchronousLogin implements AsynchronousProcess { boolean hasReachedMaxLoggedInPlayersForIp(Player player, String ip) { // Do not perform the check if player has multiple accounts permission or if IP is localhost if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) <= 0 - || permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + || service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) || "127.0.0.1".equalsIgnoreCase(ip) || "localhost".equalsIgnoreCase(ip)) { return false; diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index d64bda776..1e4965d5e 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -1,47 +1,42 @@ package fr.xephi.authme.process.login; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.data.auth.PlayerAuth; -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.datasource.DataSource; import fr.xephi.authme.events.LoginEvent; import fr.xephi.authme.events.RestoreInventoryEvent; import fr.xephi.authme.listener.PlayerListener; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.BungeeService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.JoinMessageService; import fr.xephi.authme.service.TeleportationService; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.WelcomeMessageConfiguration; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.util.StringUtils; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.plugin.PluginManager; import org.bukkit.potion.PotionEffectType; import javax.inject.Inject; +import java.util.List; import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; public class ProcessSyncPlayerLogin implements SynchronousProcess { - @Inject - private AuthMe plugin; - @Inject private BungeeService bungeeService; @Inject - private LimboCache limboCache; + private LimboService limboService; @Inject private BukkitService bukkitService; - @Inject - private PluginManager pluginManager; - @Inject private TeleportationService teleportationService; @@ -52,14 +47,20 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { private CommandManager commandManager; @Inject - private Settings settings; + private CommonService commonService; + + @Inject + private WelcomeMessageConfiguration welcomeMessageConfiguration; + + @Inject + private JoinMessageService joinMessageService; ProcessSyncPlayerLogin() { } private void restoreInventory(Player player) { RestoreInventoryEvent event = new RestoreInventoryEvent(player); - pluginManager.callEvent(event); + bukkitService.callEvent(event); if (!event.isCancelled()) { player.updateInventory(); } @@ -68,16 +69,14 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { public void processPlayerLogin(Player player) { final String name = player.getName().toLowerCase(); - final LimboPlayer limbo = limboCache.getPlayerData(name); + commonService.setGroup(player, AuthGroupType.LOGGED_IN); + final LimboPlayer limbo = limboService.getLimboPlayer(name); // Limbo contains the State of the Player before /login if (limbo != null) { - limboCache.restoreData(player); - limboCache.deletePlayerData(player); - // do we really need to use location from database for now? - // because LimboCache#restoreData teleport player to last location. + limboService.restoreData(player); } - if (settings.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { + if (commonService.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { restoreInventory(player); } @@ -85,16 +84,9 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { teleportationService.teleportOnLogin(player, auth, limbo); // We can now display the join message (if delayed) - String joinMessage = PlayerListener.joinMessage.remove(name); - if (!StringUtils.isEmpty(joinMessage)) { - for (Player p : bukkitService.getOnlinePlayers()) { - if (p.isOnline()) { - p.sendMessage(joinMessage); - } - } - } + joinMessageService.sendMessage(name); - if (settings.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + if (commonService.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { player.removePotionEffect(PotionEffectType.BLINDNESS); } @@ -103,15 +95,12 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { player.saveData(); // Login is done, display welcome message - if (settings.getProperty(RegistrationSettings.USE_WELCOME_MESSAGE)) { - if (settings.getProperty(RegistrationSettings.BROADCAST_WELCOME_MESSAGE)) { - for (String s : settings.getWelcomeMessage()) { - Bukkit.getServer().broadcastMessage(plugin.replaceAllInfo(s, player)); - } + List welcomeMessage = welcomeMessageConfiguration.getWelcomeMessage(player); + if (commonService.getProperty(RegistrationSettings.USE_WELCOME_MESSAGE)) { + if (commonService.getProperty(RegistrationSettings.BROADCAST_WELCOME_MESSAGE)) { + welcomeMessage.forEach(bukkitService::broadcastMessage); } else { - for (String s : settings.getWelcomeMessage()) { - player.sendMessage(plugin.replaceAllInfo(s, player)); - } + welcomeMessage.forEach(player::sendMessage); } } diff --git a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java index efd3066aa..1f59d9657 100644 --- a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -2,12 +2,11 @@ package fr.xephi.authme.process.logout; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.data.limbo.LimboCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.AsynchronousProcess; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.service.CommonService; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.entity.Player; @@ -24,9 +23,6 @@ public class AsynchronousLogout implements AsynchronousProcess { @Inject private PlayerCache playerCache; - @Inject - private LimboCache limboCache; - @Inject private SyncProcessManager syncProcessManager; @@ -47,7 +43,6 @@ public class AsynchronousLogout implements AsynchronousProcess { database.updateQuitLoc(auth); } - limboCache.addPlayerData(player); playerCache.removePlayer(name); database.setUnlogged(name); syncProcessManager.processSyncPlayerLogout(player); diff --git a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java index 17710df45..028781587 100644 --- a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java @@ -2,17 +2,17 @@ package fr.xephi.authme.process.logout; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.SessionManager; +import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.events.LogoutEvent; import fr.xephi.authme.listener.protocollib.ProtocolLibService; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.AuthGroupType; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.TeleportationService; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; -import fr.xephi.authme.service.BukkitService; -import fr.xephi.authme.service.TeleportationService; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; @@ -34,7 +34,7 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { private ProtocolLibService protocolLibService; @Inject - private LimboPlayerTaskManager limboPlayerTaskManager; + private LimboService limboService; @Inject private SessionManager sessionManager; @@ -53,9 +53,6 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { protocolLibService.sendBlankInventoryPacket(player); } - limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, true); - applyLogoutEffect(player); // Player is now logout... Time to fire event ! @@ -71,21 +68,14 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { teleportationService.teleportOnJoin(player); // Apply Blindness effect - final int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeout, 2)); } // Set player's data to unauthenticated - service.setGroup(player, AuthGroupType.NOT_LOGGED_IN); - player.setOp(false); - player.setAllowFlight(false); - // Remove speed - if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) - && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { - player.setFlySpeed(0.0f); - player.setWalkSpeed(0.0f); - } + limboService.createLimboPlayer(player, true); + service.setGroup(player, AuthGroupType.REGISTERED_UNAUTHENTICATED); } } diff --git a/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java b/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java index 73db67f89..c5f633e96 100644 --- a/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java +++ b/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java @@ -1,7 +1,6 @@ package fr.xephi.authme.process.quit; -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.process.SynchronousProcess; import org.bukkit.entity.Player; @@ -11,22 +10,10 @@ import javax.inject.Inject; public class ProcessSyncronousPlayerQuit implements SynchronousProcess { @Inject - private LimboPlayerStorage limboPlayerStorage; - - @Inject - private LimboCache limboCache; + private LimboService limboService; public void processSyncQuit(Player player) { - if (limboCache.hasPlayerData(player.getName())) { // it mean player is not authenticated - limboCache.restoreData(player); - limboCache.removeFromCache(player); - } else { - // Save player's data, so we could retrieve it later on player next join - if (!limboPlayerStorage.hasData(player)) { - limboPlayerStorage.saveData(player); - } - } - + limboService.restoreData(player); player.leaveVehicle(); } } diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index d32f6eadb..83e5a82cb 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -3,10 +3,13 @@ package fr.xephi.authme.process.register; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.factory.SingletonStore; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.register.executors.RegistrationExecutor; +import fr.xephi.authme.process.register.executors.RegistrationParameters; +import fr.xephi.authme.process.register.executors.RegistrationMethod; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -31,6 +34,8 @@ public class AsyncRegister implements AsynchronousProcess { private CommonService service; @Inject private PermissionsManager permissionsManager; + @Inject + private SingletonStore registrationExecutorFactory; AsyncRegister() { } @@ -38,12 +43,16 @@ public class AsyncRegister implements AsynchronousProcess { /** * Performs the registration process for the given player. * - * @param player the player to register - * @param executor the registration executor to perform the registration with + * @param variant the registration method + * @param parameters the parameters + * @param

    parameters type */ - public void register(Player player, RegistrationExecutor executor) { - if (preRegisterCheck(player) && executor.isRegistrationAdmitted()) { - executeRegistration(player, executor); + public

    void register(RegistrationMethod

    variant, P parameters) { + if (preRegisterCheck(parameters.getPlayer())) { + RegistrationExecutor

    executor = registrationExecutorFactory.getSingleton(variant.getExecutorClass()); + if (executor.isRegistrationAdmitted(parameters)) { + executeRegistration(parameters, executor); + } } } @@ -66,15 +75,17 @@ public class AsyncRegister implements AsynchronousProcess { /** * Executes the registration. * - * @param player the player to register + * @param parameters the registration parameters * @param executor the executor to perform the registration process with + * @param

    registration params type */ - private void executeRegistration(Player player, RegistrationExecutor executor) { - PlayerAuth auth = executor.buildPlayerAuth(); + private

    + void executeRegistration(P parameters, RegistrationExecutor

    executor) { + PlayerAuth auth = executor.buildPlayerAuth(parameters); if (database.saveAuth(auth)) { - executor.executePostPersistAction(); + executor.executePostPersistAction(parameters); } else { - service.send(player, MessageKey.ERROR); + service.send(parameters.getPlayer(), MessageKey.ERROR); } } diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index aea1b4be8..4e57b973b 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -1,12 +1,11 @@ package fr.xephi.authme.process.register; import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.AuthGroupType; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.process.SynchronousProcess; -import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; +import fr.xephi.authme.service.CommonService; import fr.xephi.authme.util.PlayerUtils; import org.bukkit.entity.Player; @@ -19,20 +18,16 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { private CommonService service; @Inject - private LimboPlayerTaskManager limboPlayerTaskManager; + private LimboService limboService; ProcessSyncEmailRegister() { } public void processEmailRegister(Player player) { - final String name = player.getName().toLowerCase(); - if (!service.getProperty(HooksSettings.REGISTERED_GROUP).isEmpty()) { - service.setGroup(player, AuthGroupType.REGISTERED); - } + service.setGroup(player, AuthGroupType.REGISTERED_UNAUTHENTICATED); service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); - limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, true); + limboService.replaceTasksAfterRegistration(player); player.saveData(); ConsoleLogger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player)); diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index fae428d00..173cc0d82 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -1,17 +1,15 @@ package fr.xephi.authme.process.register; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.data.limbo.LimboCache; +import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.AuthGroupType; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.service.BungeeService; +import fr.xephi.authme.service.CommonService; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.EmailSettings; -import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.PlayerUtils; import org.bukkit.entity.Player; @@ -28,10 +26,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { private CommonService service; @Inject - private LimboCache limboCache; - - @Inject - private LimboPlayerTaskManager limboPlayerTaskManager; + private LimboService limboService; @Inject private CommandManager commandManager; @@ -45,10 +40,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { * @param player the player */ private void requestLogin(Player player) { - final String name = player.getName().toLowerCase(); - limboCache.updatePlayerData(player); - limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, true); + limboService.replaceTasksAfterRegistration(player); if (player.isInsideVehicle() && player.getVehicle() != null) { player.getVehicle().eject(); @@ -56,10 +48,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { } public void processPasswordRegister(Player player) { - if (!service.getProperty(HooksSettings.REGISTERED_GROUP).isEmpty()) { - service.setGroup(player, AuthGroupType.REGISTERED); - } - + service.setGroup(player, AuthGroupType.REGISTERED_UNAUTHENTICATED); service.send(player, MessageKey.REGISTER_SUCCESS); if (!service.getProperty(EmailSettings.MAIL_ACCOUNT).isEmpty()) { diff --git a/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterExecutor.java b/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterExecutor.java new file mode 100644 index 000000000..179aa59df --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterExecutor.java @@ -0,0 +1,99 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.process.login.AsynchronousLogin; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Registration executor for registration methods where the password + * is supplied by the user. + * + * @param

    the parameters type + */ +abstract class AbstractPasswordRegisterExecutor

    + implements RegistrationExecutor

    { + + /** + * Number of ticks to wait before running the login action when it is run synchronously. + * A small delay is necessary or the database won't return the newly saved PlayerAuth object + * and the login process thinks the user is not registered. + */ + private static final int SYNC_LOGIN_DELAY = 5; + + @Inject + private ValidationService validationService; + + @Inject + private CommonService commonService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private BukkitService bukkitService; + + @Inject + private SyncProcessManager syncProcessManager; + + @Inject + private AsynchronousLogin asynchronousLogin; + + @Override + public boolean isRegistrationAdmitted(P params) { + ValidationService.ValidationResult passwordValidation = validationService.validatePassword( + params.getPassword(), params.getPlayer().getName()); + if (passwordValidation.hasError()) { + commonService.send(params.getPlayer(), passwordValidation.getMessageKey(), passwordValidation.getArgs()); + return false; + } + return true; + } + + @Override + public PlayerAuth buildPlayerAuth(P params) { + HashedPassword hashedPassword = passwordSecurity.computeHash(params.getPassword(), params.getPlayerName()); + params.setHashedPassword(hashedPassword); + return createPlayerAuthObject(params); + } + + /** + * Creates the PlayerAuth object to store into the database, based on the registration parameters. + * + * @param params the parameters + * @return the PlayerAuth representing the new account to register + */ + protected abstract PlayerAuth createPlayerAuthObject(P params); + + /** + * Returns whether the player should be automatically logged in after registration. + * + * @param params the registration parameters + * @return true if the player should be logged in, false otherwise + */ + protected boolean performLoginAfterRegister(P params) { + return !commonService.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER); + } + + @Override + public void executePostPersistAction(P params) { + final Player player = params.getPlayer(); + if (performLoginAfterRegister(params)) { + if (commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)) { + bukkitService.runTaskAsynchronously(() -> asynchronousLogin.forceLogin(player)); + } else { + bukkitService.scheduleSyncDelayedTask(() -> asynchronousLogin.forceLogin(player), SYNC_LOGIN_DELAY); + } + } + syncProcessManager.processSyncPasswordRegister(player); + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterParams.java b/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterParams.java new file mode 100644 index 000000000..0c7a1d515 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterParams.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.security.crypts.HashedPassword; +import org.bukkit.entity.Player; + +/** + * Common params type for implementors of {@link AbstractPasswordRegisterExecutor}. + * Password must be supplied on creation and cannot be changed later on. The {@link HashedPassword} + * is stored on the params object for later use. + */ +public abstract class AbstractPasswordRegisterParams extends RegistrationParameters { + + private final String password; + private HashedPassword hashedPassword; + + /** + * Constructor. + * + * @param player the player to register + * @param password the password to use + */ + public AbstractPasswordRegisterParams(Player player, String password) { + super(player); + this.password = password; + } + + /** + * Constructor with no defined password. Use for registration methods which + * have no implicit password (like two factor authentication). + * + * @param player the player to register + */ + public AbstractPasswordRegisterParams(Player player) { + this(player, null); + } + + public String getPassword() { + return password; + } + + void setHashedPassword(HashedPassword hashedPassword) { + this.hashedPassword = hashedPassword; + } + + HashedPassword getHashedPassword() { + return hashedPassword; + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterExecutor.java b/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterExecutor.java new file mode 100644 index 000000000..6005c8e41 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterExecutor.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; + +/** + * Executor for password registration via API call. + */ +class ApiPasswordRegisterExecutor extends AbstractPasswordRegisterExecutor { + + @Override + protected PlayerAuth createPlayerAuthObject(ApiPasswordRegisterParams params) { + return PlayerAuthBuilderHelper + .createPlayerAuth(params.getPlayer(), params.getHashedPassword(), null); + } + + @Override + protected boolean performLoginAfterRegister(ApiPasswordRegisterParams params) { + return params.getLoginAfterRegister(); + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterParams.java b/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterParams.java new file mode 100644 index 000000000..357d32c22 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterParams.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for {@link ApiPasswordRegisterExecutor}. + */ +public class ApiPasswordRegisterParams extends PasswordRegisterParams { + + private final boolean loginAfterRegister; + + protected ApiPasswordRegisterParams(Player player, String password, boolean loginAfterRegister) { + super(player, password, null); + this.loginAfterRegister = loginAfterRegister; + } + + /** + * Creates a parameters object. + * + * @param player the player to register + * @param password the password to register with + * @param loginAfterRegister whether the player should be logged in after registration + * @return params object with the given data + */ + public static ApiPasswordRegisterParams of(Player player, String password, boolean loginAfterRegister) { + return new ApiPasswordRegisterParams(player, password, loginAfterRegister); + } + + /** + * @return true if the player should be logged in after being registered, false otherwise + */ + public boolean getLoginAfterRegister() { + return loginAfterRegister; + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutor.java b/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutor.java new file mode 100644 index 000000000..0f4153b18 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutor.java @@ -0,0 +1,80 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.util.RandomStringUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +import static fr.xephi.authme.permission.PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS; +import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; +import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; + +/** + * Executor for email registration: the player only provides his email address, + * to which a generated password is sent. + */ +class EmailRegisterExecutor implements RegistrationExecutor { + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Inject + private EmailService emailService; + + @Inject + private SyncProcessManager syncProcessManager; + + @Inject + private PasswordSecurity passwordSecurity; + + @Override + public boolean isRegistrationAdmitted(EmailRegisterParams params) { + final int maxRegPerEmail = commonService.getProperty(EmailSettings.MAX_REG_PER_EMAIL); + if (maxRegPerEmail > 0 && !permissionsManager.hasPermission(params.getPlayer(), ALLOW_MULTIPLE_ACCOUNTS)) { + int otherAccounts = dataSource.countAuthsByEmail(params.getEmail()); + if (otherAccounts >= maxRegPerEmail) { + commonService.send(params.getPlayer(), MessageKey.MAX_REGISTER_EXCEEDED, + Integer.toString(maxRegPerEmail), Integer.toString(otherAccounts), "@"); + return false; + } + } + return true; + } + + @Override + public PlayerAuth buildPlayerAuth(EmailRegisterParams params) { + String password = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH)); + HashedPassword hashedPassword = passwordSecurity.computeHash(password, params.getPlayer().getName()); + params.setPassword(password); + return createPlayerAuth(params.getPlayer(), hashedPassword, params.getEmail()); + } + + @Override + public void executePostPersistAction(EmailRegisterParams params) { + Player player = params.getPlayer(); + boolean couldSendMail = emailService.sendPasswordMail( + player.getName(), params.getEmail(), params.getPassword()); + if (couldSendMail) { + syncProcessManager.processSyncEmailRegister(player); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProvider.java b/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProvider.java deleted file mode 100644 index 88f8cd602..000000000 --- a/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProvider.java +++ /dev/null @@ -1,91 +0,0 @@ -package fr.xephi.authme.process.register.executors; - -import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.mail.SendMailSSL; -import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.permission.PermissionsManager; -import fr.xephi.authme.process.SyncProcessManager; -import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.service.CommonService; -import fr.xephi.authme.settings.properties.EmailSettings; -import fr.xephi.authme.util.RandomStringUtils; -import org.bukkit.entity.Player; - -import javax.inject.Inject; - -import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; -import static fr.xephi.authme.permission.PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS; -import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; - -/** - * Provides a registration executor for email registration. - */ -class EmailRegisterExecutorProvider { - - @Inject - private PermissionsManager permissionsManager; - - @Inject - private DataSource dataSource; - - @Inject - private CommonService commonService; - - @Inject - private SendMailSSL sendMailSsl; - - @Inject - private SyncProcessManager syncProcessManager; - - @Inject - private PasswordSecurity passwordSecurity; - - EmailRegisterExecutorProvider() { - } - - /** Registration executor implementation for email registration. */ - class EmailRegisterExecutor implements RegistrationExecutor { - - private final Player player; - private final String email; - private String password; - - EmailRegisterExecutor(Player player, String email) { - this.player = player; - this.email = email; - } - - @Override - public boolean isRegistrationAdmitted() { - final int maxRegPerEmail = commonService.getProperty(EmailSettings.MAX_REG_PER_EMAIL); - if (maxRegPerEmail > 0 && !permissionsManager.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) { - int otherAccounts = dataSource.countAuthsByEmail(email); - if (otherAccounts >= maxRegPerEmail) { - commonService.send(player, MessageKey.MAX_REGISTER_EXCEEDED, Integer.toString(maxRegPerEmail), - Integer.toString(otherAccounts), "@"); - return false; - } - } - return true; - } - - @Override - public PlayerAuth buildPlayerAuth() { - password = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH)); - HashedPassword hashedPassword = passwordSecurity.computeHash(password, player.getName()); - return createPlayerAuth(player, hashedPassword, email); - } - - @Override - public void executePostPersistAction() { - boolean couldSendMail = sendMailSsl.sendPasswordMail(player.getName(), email, password); - if (couldSendMail) { - syncProcessManager.processSyncEmailRegister(player); - } else { - commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); - } - } - } -} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterParams.java b/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterParams.java new file mode 100644 index 000000000..94d03acc5 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterParams.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for email registration. + */ +public class EmailRegisterParams extends RegistrationParameters { + + private final String email; + private String password; + + protected EmailRegisterParams(Player player, String email) { + super(player); + this.email = email; + } + + /** + * Creates a params object for email registration. + * + * @param player the player to register + * @param email the player's email + * @return params object with the given data + */ + public static EmailRegisterParams of(Player player, String email) { + return new EmailRegisterParams(player, email); + } + + public String getEmail() { + return email; + } + + void setPassword(String password) { + this.password = password; + } + + /** + * @return the password generated for the player + */ + String getPassword() { + return password; + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutor.java b/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutor.java new file mode 100644 index 000000000..167bee292 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutor.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; + +import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; + +/** + * Registration executor for password registration. + */ +class PasswordRegisterExecutor extends AbstractPasswordRegisterExecutor { + + @Override + public PlayerAuth createPlayerAuthObject(PasswordRegisterParams params) { + return createPlayerAuth(params.getPlayer(), params.getHashedPassword(), params.getEmail()); + } + +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorProvider.java b/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorProvider.java deleted file mode 100644 index a79c63c82..000000000 --- a/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorProvider.java +++ /dev/null @@ -1,155 +0,0 @@ -package fr.xephi.authme.process.register.executors; - -import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.process.SyncProcessManager; -import fr.xephi.authme.process.login.AsynchronousLogin; -import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.security.crypts.TwoFactor; -import fr.xephi.authme.service.BukkitService; -import fr.xephi.authme.service.CommonService; -import fr.xephi.authme.service.ValidationService; -import fr.xephi.authme.service.ValidationService.ValidationResult; -import fr.xephi.authme.settings.properties.PluginSettings; -import fr.xephi.authme.settings.properties.RegistrationSettings; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import javax.inject.Inject; - -import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; - -/** - * Provides registration executors for password-based registration variants. - */ -class PasswordRegisterExecutorProvider { - - /** - * Number of ticks to wait before running the login action when it is run synchronously. - * A small delay is necessary or the database won't return the newly saved PlayerAuth object - * and the login process thinks the user is not registered. - */ - private static final int SYNC_LOGIN_DELAY = 5; - - @Inject - private ValidationService validationService; - - @Inject - private CommonService commonService; - - @Inject - private PasswordSecurity passwordSecurity; - - @Inject - private BukkitService bukkitService; - - @Inject - private SyncProcessManager syncProcessManager; - - @Inject - private AsynchronousLogin asynchronousLogin; - - PasswordRegisterExecutorProvider() { - } - - /** Registration executor for password registration. */ - class PasswordRegisterExecutor implements RegistrationExecutor { - - protected final Player player; - private final String password; - private final String email; - protected HashedPassword hashedPassword; - - /** - * Constructor. - * - * @param player the player to register - * @param password the password to register with - * @param email the email of the player (may be null) - */ - PasswordRegisterExecutor(Player player, String password, String email) { - this.player = player; - this.password = password; - this.email = email; - } - - @Override - public boolean isRegistrationAdmitted() { - ValidationResult passwordValidation = validationService.validatePassword(password, player.getName()); - if (passwordValidation.hasError()) { - commonService.send(player, passwordValidation.getMessageKey(), passwordValidation.getArgs()); - return false; - } - return true; - } - - @Override - public PlayerAuth buildPlayerAuth() { - hashedPassword = passwordSecurity.computeHash(password, player.getName().toLowerCase()); - return createPlayerAuth(player, hashedPassword, email); - } - - protected boolean performLoginAfterRegister() { - return !commonService.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER); - } - - @Override - public void executePostPersistAction() { - if (performLoginAfterRegister()) { - if (commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)) { - bukkitService.runTaskAsynchronously(() -> asynchronousLogin.forceLogin(player)); - } else { - bukkitService.scheduleSyncDelayedTask(() -> asynchronousLogin.forceLogin(player), SYNC_LOGIN_DELAY); - } - } - syncProcessManager.processSyncPasswordRegister(player); - } - } - - /** Executor for password registration via API call. */ - class ApiPasswordRegisterExecutor extends PasswordRegisterExecutor { - - private final boolean loginAfterRegister; - - /** - * Constructor. - * - * @param player the player to register - * @param password the password to register with - * @param loginAfterRegister whether the user should be automatically logged in after registration - */ - ApiPasswordRegisterExecutor(Player player, String password, boolean loginAfterRegister) { - super(player, password, null); - this.loginAfterRegister = loginAfterRegister; - } - - @Override - protected boolean performLoginAfterRegister() { - return loginAfterRegister; - } - } - - /** Executor for two factor registration. */ - class TwoFactorRegisterExecutor extends PasswordRegisterExecutor { - - TwoFactorRegisterExecutor(Player player) { - super(player, "", null); - } - - @Override - public boolean isRegistrationAdmitted() { - // nothing to check - return true; - } - - @Override - public void executePostPersistAction() { - super.executePostPersistAction(); - - String qrCodeUrl = TwoFactor.getQRBarcodeURL(player.getName(), Bukkit.getIp(), hashedPassword.getHash()); - commonService.send(player, MessageKey.TWO_FACTOR_CREATE, hashedPassword.getHash(), qrCodeUrl); - } - - } -} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterParams.java b/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterParams.java new file mode 100644 index 000000000..f21861bff --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterParams.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for registration with a given password, and optionally an email address. + */ +public class PasswordRegisterParams extends AbstractPasswordRegisterParams { + + private final String email; + + protected PasswordRegisterParams(Player player, String password, String email) { + super(player, password); + this.email = email; + } + + /** + * Creates a params object. + * + * @param player the player to register + * @param password the password to register with + * @param email the email of the player (may be null) + * @return params object with the given data + */ + public static PasswordRegisterParams of(Player player, String password, String email) { + return new PasswordRegisterParams(player, password, email); + } + + public String getEmail() { + return email; + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelper.java b/src/main/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelper.java index 1943b5d54..5d9ab8657 100644 --- a/src/main/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelper.java +++ b/src/main/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelper.java @@ -13,6 +13,14 @@ final class PlayerAuthBuilderHelper { private PlayerAuthBuilderHelper() { } + /** + * Creates a {@link PlayerAuth} object with the given data. + * + * @param player the player to create a PlayerAuth for + * @param hashedPassword the hashed password + * @param email the email address (nullable) + * @return the generated PlayerAuth object + */ static PlayerAuth createPlayerAuth(Player player, HashedPassword hashedPassword, String email) { return PlayerAuth.builder() .name(player.getName().toLowerCase()) diff --git a/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutor.java b/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutor.java index cceb5c18e..32bfd9517 100644 --- a/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutor.java +++ b/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutor.java @@ -4,8 +4,10 @@ import fr.xephi.authme.data.auth.PlayerAuth; /** * Performs the registration action. + * + * @param

    the registration parameters type */ -public interface RegistrationExecutor { +public interface RegistrationExecutor

    { /** * Returns whether the registration may take place. Use this method to execute @@ -14,20 +16,24 @@ public interface RegistrationExecutor { * If this method returns {@code false}, it is expected that the executor inform * the player about the error within this method call. * + * @param params the parameters for the registration * @return true if registration may be performed, false otherwise */ - boolean isRegistrationAdmitted(); + boolean isRegistrationAdmitted(P params); /** * Constructs the PlayerAuth object to persist into the database. * + * @param params the parameters for the registration * @return the player auth to register in the data source */ - PlayerAuth buildPlayerAuth(); + PlayerAuth buildPlayerAuth(P params); /** * Follow-up method called after the player auth could be added into the database. + * + * @param params the parameters for the registration */ - void executePostPersistAction(); + void executePostPersistAction(P params); } diff --git a/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutorProvider.java b/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutorProvider.java deleted file mode 100644 index 286a51634..000000000 --- a/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutorProvider.java +++ /dev/null @@ -1,36 +0,0 @@ -package fr.xephi.authme.process.register.executors; - -import org.bukkit.entity.Player; - -import javax.inject.Inject; - -/** - * Provides a {@link RegistrationExecutor} for various registration methods. - */ -public class RegistrationExecutorProvider { - - @Inject - private PasswordRegisterExecutorProvider passwordRegisterExecutorProvider; - - @Inject - private EmailRegisterExecutorProvider emailRegisterExecutorProvider; - - RegistrationExecutorProvider() { - } - - public RegistrationExecutor getPasswordRegisterExecutor(Player player, String password, String email) { - return passwordRegisterExecutorProvider.new PasswordRegisterExecutor(player, password, email); - } - - public RegistrationExecutor getPasswordRegisterExecutor(Player player, String password, boolean loginAfterRegister) { - return passwordRegisterExecutorProvider.new ApiPasswordRegisterExecutor(player, password, loginAfterRegister); - } - - public RegistrationExecutor getTwoFactorRegisterExecutor(Player player) { - return passwordRegisterExecutorProvider.new TwoFactorRegisterExecutor(player); - } - - public RegistrationExecutor getEmailRegisterExecutor(Player player, String email) { - return emailRegisterExecutorProvider.new EmailRegisterExecutor(player, email); - } -} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/RegistrationMethod.java b/src/main/java/fr/xephi/authme/process/register/executors/RegistrationMethod.java new file mode 100644 index 000000000..f5f38b866 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/RegistrationMethod.java @@ -0,0 +1,53 @@ +package fr.xephi.authme.process.register.executors; + +/** + * Methods with which a player can be registered. + *

    + * These constants each define a different way of registering a player and define the + * {@link RegistrationParameters parameters} and {@link RegistrationExecutor executor} + * classes which perform this registration method. This is essentially a typed enum + * as passing a constant of this class along with a parameters object to a method can + * be restricted to the correct parameters type. + * + * @param

    the registration parameters type the method uses + */ +public final class RegistrationMethod

    { + + /** + * Password registration. + */ + public static final RegistrationMethod PASSWORD_REGISTRATION = + new RegistrationMethod<>(PasswordRegisterExecutor.class); + + /** + * Registration with two-factor authentication as login means. + */ + public static final RegistrationMethod TWO_FACTOR_REGISTRATION = + new RegistrationMethod<>(TwoFactorRegisterExecutor.class); + + /** + * Email registration: an email address is provided, to which a generated password is sent. + */ + public static final RegistrationMethod EMAIL_REGISTRATION = + new RegistrationMethod<>(EmailRegisterExecutor.class); + + /** + * API registration: player and password are provided via an API method. + */ + public static final RegistrationMethod API_REGISTRATION = + new RegistrationMethod<>(ApiPasswordRegisterExecutor.class); + + + private final Class> executorClass; + + private RegistrationMethod(Class> executorClass) { + this.executorClass = executorClass; + } + + /** + * @return the executor class to perform the registration method + */ + public Class> getExecutorClass() { + return executorClass; + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/RegistrationParameters.java b/src/main/java/fr/xephi/authme/process/register/executors/RegistrationParameters.java new file mode 100644 index 000000000..c92d57ffa --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/RegistrationParameters.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parent of all registration parameters. + */ +public abstract class RegistrationParameters { + + private final Player player; + + /** + * Constructor. + * + * @param player the player to perform the registration for + */ + public RegistrationParameters(Player player) { + this.player = player; + } + + public Player getPlayer() { + return player; + } + + public String getPlayerName() { + return player.getName(); + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterExecutor.java b/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterExecutor.java new file mode 100644 index 000000000..027a5fa68 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterExecutor.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.security.crypts.TwoFactor; +import fr.xephi.authme.service.CommonService; +import org.bukkit.Bukkit; + +import javax.inject.Inject; + +import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; + +/** + * Executor for two-factor registration. + */ +class TwoFactorRegisterExecutor extends AbstractPasswordRegisterExecutor { + + @Inject + private CommonService commonService; + + @Override + public boolean isRegistrationAdmitted(TwoFactorRegisterParams params) { + // nothing to check + return true; + } + + @Override + protected PlayerAuth createPlayerAuthObject(TwoFactorRegisterParams params) { + return createPlayerAuth(params.getPlayer(), params.getHashedPassword(), null); + } + + @Override + public void executePostPersistAction(TwoFactorRegisterParams params) { + super.executePostPersistAction(params); + + // Note ljacqu 20170317: This two-factor registration type is only invoked when the password hash is configured + // to two-factor authentication. Therefore, the hashed password is the result of the TwoFactor EncryptionMethod + // implementation (contains the TOTP secret). + String hash = params.getHashedPassword().getHash(); + String qrCodeUrl = TwoFactor.getQRBarcodeURL(params.getPlayerName(), Bukkit.getIp(), hash); + commonService.send(params.getPlayer(), MessageKey.TWO_FACTOR_CREATE, hash, qrCodeUrl); + } +} diff --git a/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterParams.java b/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterParams.java new file mode 100644 index 000000000..a7a758750 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterParams.java @@ -0,0 +1,23 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for registration with two-factor authentication. + */ +public class TwoFactorRegisterParams extends AbstractPasswordRegisterParams { + + protected TwoFactorRegisterParams(Player player) { + super(player); + } + + /** + * Creates a parameters object. + * + * @param player the player to register + * @return params object with the given player + */ + public static TwoFactorRegisterParams of(Player player) { + return new TwoFactorRegisterParams(player); + } +} diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 764e56768..eabd902eb 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -3,19 +3,18 @@ package fr.xephi.authme.process.unregister; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.data.limbo.LimboCache; +import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.AuthGroupHandler; import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.AsynchronousProcess; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.TeleportationService; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; -import fr.xephi.authme.service.BukkitService; -import fr.xephi.authme.service.TeleportationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; @@ -43,10 +42,7 @@ public class AsynchronousUnregister implements AsynchronousProcess { private BukkitService bukkitService; @Inject - private LimboCache limboCache; - - @Inject - private LimboPlayerTaskManager limboPlayerTaskManager; + private LimboService limboService; @Inject private TeleportationService teleportationService; @@ -111,12 +107,10 @@ public class AsynchronousUnregister implements AsynchronousProcess { teleportationService.teleportOnJoin(player); player.saveData(); - limboCache.deletePlayerData(player); - limboCache.addPlayerData(player); - - limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, false); - applyBlindEffect(player); + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { + limboService.createLimboPlayer(player, false); + applyBlindEffect(player); + }); } authGroupHandler.setGroup(player, AuthGroupType.UNREGISTERED); service.send(player, MessageKey.UNREGISTERED_SUCCESS); @@ -124,13 +118,8 @@ public class AsynchronousUnregister implements AsynchronousProcess { private void applyBlindEffect(final Player player) { if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { - final int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - bukkitService.runTask(new Runnable() { - @Override - public void run() { - player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeout, 2)); - } - }); + int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeout, 2)); } } } diff --git a/src/main/java/fr/xephi/authme/security/HashAlgorithm.java b/src/main/java/fr/xephi/authme/security/HashAlgorithm.java index 732582e03..f12da678d 100644 --- a/src/main/java/fr/xephi/authme/security/HashAlgorithm.java +++ b/src/main/java/fr/xephi/authme/security/HashAlgorithm.java @@ -7,36 +7,36 @@ import fr.xephi.authme.security.crypts.EncryptionMethod; */ public enum HashAlgorithm { - BCRYPT(fr.xephi.authme.security.crypts.BCRYPT.class), - BCRYPT2Y(fr.xephi.authme.security.crypts.BCRYPT2Y.class), - CRAZYCRYPT1(fr.xephi.authme.security.crypts.CRAZYCRYPT1.class), - DOUBLEMD5(fr.xephi.authme.security.crypts.DOUBLEMD5.class), - IPB3(fr.xephi.authme.security.crypts.IPB3.class), - IPB4(fr.xephi.authme.security.crypts.IPB4.class), - JOOMLA(fr.xephi.authme.security.crypts.JOOMLA.class), - MD5(fr.xephi.authme.security.crypts.MD5.class), - MD5VB(fr.xephi.authme.security.crypts.MD5VB.class), - MYBB(fr.xephi.authme.security.crypts.MYBB.class), + BCRYPT(fr.xephi.authme.security.crypts.BCrypt.class), + BCRYPT2Y(fr.xephi.authme.security.crypts.BCrypt2y.class), + CRAZYCRYPT1(fr.xephi.authme.security.crypts.CrazyCrypt1.class), + DOUBLEMD5(fr.xephi.authme.security.crypts.DoubleMd5.class), + IPB3(fr.xephi.authme.security.crypts.Ipb3.class), + IPB4(fr.xephi.authme.security.crypts.Ipb4.class), + JOOMLA(fr.xephi.authme.security.crypts.Joomla.class), + MD5(fr.xephi.authme.security.crypts.Md5.class), + MD5VB(fr.xephi.authme.security.crypts.Md5vB.class), + MYBB(fr.xephi.authme.security.crypts.MyBB.class), PBKDF2(fr.xephi.authme.security.crypts.Pbkdf2.class), PBKDF2DJANGO(fr.xephi.authme.security.crypts.Pbkdf2Django.class), - PHPBB(fr.xephi.authme.security.crypts.PHPBB.class), - PHPFUSION(fr.xephi.authme.security.crypts.PHPFUSION.class), + PHPBB(fr.xephi.authme.security.crypts.PhpBB.class), + PHPFUSION(fr.xephi.authme.security.crypts.PhpFusion.class), @Deprecated - PLAINTEXT(fr.xephi.authme.security.crypts.PLAINTEXT.class), - ROYALAUTH(fr.xephi.authme.security.crypts.ROYALAUTH.class), - SALTED2MD5(fr.xephi.authme.security.crypts.SALTED2MD5.class), - SALTEDSHA512(fr.xephi.authme.security.crypts.SALTEDSHA512.class), - SHA1(fr.xephi.authme.security.crypts.SHA1.class), - SHA256(fr.xephi.authme.security.crypts.SHA256.class), - SHA512(fr.xephi.authme.security.crypts.SHA512.class), - SMF(fr.xephi.authme.security.crypts.SMF.class), + PLAINTEXT(fr.xephi.authme.security.crypts.PlainText.class), + ROYALAUTH(fr.xephi.authme.security.crypts.RoyalAuth.class), + SALTED2MD5(fr.xephi.authme.security.crypts.Salted2Md5.class), + SALTEDSHA512(fr.xephi.authme.security.crypts.SaltedSha512.class), + SHA1(fr.xephi.authme.security.crypts.Sha1.class), + SHA256(fr.xephi.authme.security.crypts.Sha256.class), + SHA512(fr.xephi.authme.security.crypts.Sha512.class), + SMF(fr.xephi.authme.security.crypts.Smf.class), TWO_FACTOR(fr.xephi.authme.security.crypts.TwoFactor.class), - WBB3(fr.xephi.authme.security.crypts.WBB3.class), - WBB4(fr.xephi.authme.security.crypts.WBB4.class), - WHIRLPOOL(fr.xephi.authme.security.crypts.WHIRLPOOL.class), - WORDPRESS(fr.xephi.authme.security.crypts.WORDPRESS.class), - XAUTH(fr.xephi.authme.security.crypts.XAUTH.class), - XFBCRYPT(fr.xephi.authme.security.crypts.XFBCRYPT.class), + WBB3(fr.xephi.authme.security.crypts.Wbb3.class), + WBB4(fr.xephi.authme.security.crypts.Wbb4.class), + WHIRLPOOL(fr.xephi.authme.security.crypts.Whirlpool.class), + WORDPRESS(fr.xephi.authme.security.crypts.Wordpress.class), + XAUTH(fr.xephi.authme.security.crypts.XAuth.class), + XFBCRYPT(fr.xephi.authme.security.crypts.XfBCrypt.class), CUSTOM(null); private final Class clazz; diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 8549b388b..d29471089 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -1,9 +1,9 @@ package fr.xephi.authme.security; -import ch.jalu.injector.Injector; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.PasswordEncryptionEvent; import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.initialization.factory.Factory; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; @@ -29,7 +29,7 @@ public class PasswordSecurity implements Reloadable { private PluginManager pluginManager; @Inject - private Injector injector; + private Factory hashAlgorithmFactory; private HashAlgorithm algorithm; private Collection legacyAlgorithms; @@ -154,7 +154,7 @@ public class PasswordSecurity implements Reloadable { if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) { return null; } - return injector.newInstance(algorithm.getClazz()); + return hashAlgorithmFactory.newInstance(algorithm.getClazz()); } private void hashPasswordForNewAlgorithm(String password, String playerName) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java b/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java similarity index 95% rename from src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java rename to src/main/java/fr/xephi/authme/security/crypts/BCrypt.java index aae6b9109..02e12d459 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java @@ -14,12 +14,12 @@ import javax.inject.Inject; @Recommendation(Usage.RECOMMENDED) // provided the salt length is >= 8 @HasSalt(value = SaltType.TEXT) // length depends on the bcryptLog2Rounds setting -public class BCRYPT implements EncryptionMethod { +public class BCrypt implements EncryptionMethod { private final int bCryptLog2Rounds; @Inject - public BCRYPT(Settings settings) { + public BCrypt(Settings settings) { bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND); } diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT2Y.java b/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java similarity index 95% rename from src/main/java/fr/xephi/authme/security/crypts/BCRYPT2Y.java rename to src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java index 49bd45f8a..cf4807abc 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT2Y.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java @@ -4,7 +4,7 @@ import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; @Recommendation(Usage.RECOMMENDED) -public class BCRYPT2Y extends HexSaltedMethod { +public class BCrypt2y extends HexSaltedMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1.java b/src/main/java/fr/xephi/authme/security/crypts/CrazyCrypt1.java similarity index 95% rename from src/main/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1.java rename to src/main/java/fr/xephi/authme/security/crypts/CrazyCrypt1.java index 595842314..6130c6a12 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1.java +++ b/src/main/java/fr/xephi/authme/security/crypts/CrazyCrypt1.java @@ -6,7 +6,7 @@ import fr.xephi.authme.security.MessageDigestAlgorithm; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -public class CRAZYCRYPT1 extends UsernameSaltMethod { +public class CrazyCrypt1 extends UsernameSaltMethod { private static final char[] CRYPTCHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; diff --git a/src/main/java/fr/xephi/authme/security/crypts/DOUBLEMD5.java b/src/main/java/fr/xephi/authme/security/crypts/DOUBLEMD5.java deleted file mode 100644 index b7823a2b4..000000000 --- a/src/main/java/fr/xephi/authme/security/crypts/DOUBLEMD5.java +++ /dev/null @@ -1,12 +0,0 @@ -package fr.xephi.authme.security.crypts; - -import static fr.xephi.authme.security.HashUtils.md5; - -public class DOUBLEMD5 extends UnsaltedMethod { - - @Override - public String computeHash(String password) { - return md5(md5(password)); - } - -} diff --git a/src/main/java/fr/xephi/authme/security/crypts/DoubleMd5.java b/src/main/java/fr/xephi/authme/security/crypts/DoubleMd5.java new file mode 100644 index 000000000..54158419b --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/crypts/DoubleMd5.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +import static fr.xephi.authme.security.HashUtils.md5; + +@Recommendation(Usage.DEPRECATED) +public class DoubleMd5 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return md5(md5(password)); + } + +} diff --git a/src/main/java/fr/xephi/authme/security/crypts/IPB3.java b/src/main/java/fr/xephi/authme/security/crypts/Ipb3.java similarity index 93% rename from src/main/java/fr/xephi/authme/security/crypts/IPB3.java rename to src/main/java/fr/xephi/authme/security/crypts/Ipb3.java index a4e62461c..15dcd189b 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/IPB3.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Ipb3.java @@ -10,7 +10,7 @@ import static fr.xephi.authme.security.HashUtils.md5; @Recommendation(Usage.ACCEPTABLE) @HasSalt(value = SaltType.TEXT, length = 5) -public class IPB3 extends SeparateSaltMethod { +public class Ipb3 extends SeparateSaltMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/IPB4.java b/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java similarity index 91% rename from src/main/java/fr/xephi/authme/security/crypts/IPB4.java rename to src/main/java/fr/xephi/authme/security/crypts/Ipb4.java index 40ea75166..762897955 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/IPB4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java @@ -11,15 +11,15 @@ import fr.xephi.authme.util.StringUtils; /** - * Implementation for IPB4 (Invision Power Board 4). + * Implementation for Ipb4 (Invision Power Board 4). *

    * The hash uses standard BCrypt with 13 as log2 number of rounds. Additionally, - * IPB4 requires that the salt be stored additionally in the column "members_pass_hash" + * Ipb4 requires that the salt be stored additionally in the column "members_pass_hash" * (even though BCrypt hashes already have the salt in the result). */ @Recommendation(Usage.DOES_NOT_WORK) @HasSalt(value = SaltType.TEXT, length = 22) -public class IPB4 implements EncryptionMethod { +public class Ipb4 implements EncryptionMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/JOOMLA.java b/src/main/java/fr/xephi/authme/security/crypts/Joomla.java similarity index 94% rename from src/main/java/fr/xephi/authme/security/crypts/JOOMLA.java rename to src/main/java/fr/xephi/authme/security/crypts/Joomla.java index ca72674b3..462f5cb28 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/JOOMLA.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Joomla.java @@ -5,7 +5,7 @@ import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; @Recommendation(Usage.ACCEPTABLE) -public class JOOMLA extends HexSaltedMethod { +public class Joomla extends HexSaltedMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/MD5.java b/src/main/java/fr/xephi/authme/security/crypts/MD5.java deleted file mode 100644 index 50bb7d97e..000000000 --- a/src/main/java/fr/xephi/authme/security/crypts/MD5.java +++ /dev/null @@ -1,12 +0,0 @@ -package fr.xephi.authme.security.crypts; - -import fr.xephi.authme.security.HashUtils; - -public class MD5 extends UnsaltedMethod { - - @Override - public String computeHash(String password) { - return HashUtils.md5(password); - } - -} diff --git a/src/main/java/fr/xephi/authme/security/crypts/Md5.java b/src/main/java/fr/xephi/authme/security/crypts/Md5.java new file mode 100644 index 000000000..a5f6d91d2 --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/crypts/Md5.java @@ -0,0 +1,15 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +@Recommendation(Usage.DEPRECATED) +public class Md5 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return HashUtils.md5(password); + } + +} diff --git a/src/main/java/fr/xephi/authme/security/crypts/MD5VB.java b/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java similarity index 93% rename from src/main/java/fr/xephi/authme/security/crypts/MD5VB.java rename to src/main/java/fr/xephi/authme/security/crypts/Md5vB.java index f9c21ae7e..c244ec49d 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/MD5VB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java @@ -2,7 +2,7 @@ package fr.xephi.authme.security.crypts; import static fr.xephi.authme.security.HashUtils.md5; -public class MD5VB extends HexSaltedMethod { +public class Md5vB extends HexSaltedMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/MYBB.java b/src/main/java/fr/xephi/authme/security/crypts/MyBB.java similarity index 93% rename from src/main/java/fr/xephi/authme/security/crypts/MYBB.java rename to src/main/java/fr/xephi/authme/security/crypts/MyBB.java index b25f47695..3f0a477ab 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/MYBB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/MyBB.java @@ -10,7 +10,7 @@ import static fr.xephi.authme.security.HashUtils.md5; @Recommendation(Usage.ACCEPTABLE) @HasSalt(value = SaltType.TEXT, length = 8) -public class MYBB extends SeparateSaltMethod { +public class MyBB extends SeparateSaltMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/PHPBB.java b/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java similarity index 99% rename from src/main/java/fr/xephi/authme/security/crypts/PHPBB.java rename to src/main/java/fr/xephi/authme/security/crypts/PhpBB.java index 074143fd6..e5f7e54ca 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/PHPBB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java @@ -9,7 +9,7 @@ import java.security.MessageDigest; /** * @author stefano */ -public class PHPBB extends HexSaltedMethod { +public class PhpBB extends HexSaltedMethod { private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; diff --git a/src/main/java/fr/xephi/authme/security/crypts/PHPFUSION.java b/src/main/java/fr/xephi/authme/security/crypts/PhpFusion.java similarity index 96% rename from src/main/java/fr/xephi/authme/security/crypts/PHPFUSION.java rename to src/main/java/fr/xephi/authme/security/crypts/PhpFusion.java index 905798ec3..5a49ed4ce 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/PHPFUSION.java +++ b/src/main/java/fr/xephi/authme/security/crypts/PhpFusion.java @@ -14,7 +14,7 @@ import java.security.NoSuchAlgorithmException; @Recommendation(Usage.DO_NOT_USE) @AsciiRestricted -public class PHPFUSION extends SeparateSaltMethod { +public class PhpFusion extends SeparateSaltMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/PLAINTEXT.java b/src/main/java/fr/xephi/authme/security/crypts/PlainText.java similarity index 57% rename from src/main/java/fr/xephi/authme/security/crypts/PLAINTEXT.java rename to src/main/java/fr/xephi/authme/security/crypts/PlainText.java index a294ca917..43d6e8200 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/PLAINTEXT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/PlainText.java @@ -1,12 +1,16 @@ package fr.xephi.authme.security.crypts; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + /** * Plaintext password storage. * * @deprecated Using this is no longer supported. AuthMe will migrate to SHA256 on startup. */ @Deprecated -public class PLAINTEXT extends UnsaltedMethod { +@Recommendation(Usage.DEPRECATED) +public class PlainText extends UnsaltedMethod { @Override public String computeHash(String password) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/ROYALAUTH.java b/src/main/java/fr/xephi/authme/security/crypts/RoyalAuth.java similarity index 88% rename from src/main/java/fr/xephi/authme/security/crypts/ROYALAUTH.java rename to src/main/java/fr/xephi/authme/security/crypts/RoyalAuth.java index db089e360..989ef8383 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/ROYALAUTH.java +++ b/src/main/java/fr/xephi/authme/security/crypts/RoyalAuth.java @@ -2,7 +2,7 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.HashUtils; -public class ROYALAUTH extends UnsaltedMethod { +public class RoyalAuth extends UnsaltedMethod { @Override public String computeHash(String password) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/SHA1.java b/src/main/java/fr/xephi/authme/security/crypts/SHA1.java deleted file mode 100644 index 080910ec7..000000000 --- a/src/main/java/fr/xephi/authme/security/crypts/SHA1.java +++ /dev/null @@ -1,12 +0,0 @@ -package fr.xephi.authme.security.crypts; - -import fr.xephi.authme.security.HashUtils; - -public class SHA1 extends UnsaltedMethod { - - @Override - public String computeHash(String password) { - return HashUtils.sha1(password); - } - -} diff --git a/src/main/java/fr/xephi/authme/security/crypts/SHA512.java b/src/main/java/fr/xephi/authme/security/crypts/SHA512.java deleted file mode 100644 index 81f1e0263..000000000 --- a/src/main/java/fr/xephi/authme/security/crypts/SHA512.java +++ /dev/null @@ -1,12 +0,0 @@ -package fr.xephi.authme.security.crypts; - -import fr.xephi.authme.security.HashUtils; - -public class SHA512 extends UnsaltedMethod { - - @Override - public String computeHash(String password) { - return HashUtils.sha512(password); - } - -} diff --git a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java b/src/main/java/fr/xephi/authme/security/crypts/Salted2Md5.java similarity index 91% rename from src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java rename to src/main/java/fr/xephi/authme/security/crypts/Salted2Md5.java index b2f22ab9b..6d708b3ce 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Salted2Md5.java @@ -14,12 +14,12 @@ import static fr.xephi.authme.security.HashUtils.md5; @Recommendation(Usage.ACCEPTABLE) // presuming that length is something sensible (>= 8) @HasSalt(value = SaltType.TEXT) // length defined by the doubleMd5SaltLength setting -public class SALTED2MD5 extends SeparateSaltMethod { +public class Salted2Md5 extends SeparateSaltMethod { private final int saltLength; @Inject - public SALTED2MD5(Settings settings) { + public Salted2Md5(Settings settings) { saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH); } diff --git a/src/main/java/fr/xephi/authme/security/crypts/SALTEDSHA512.java b/src/main/java/fr/xephi/authme/security/crypts/SaltedSha512.java similarity index 90% rename from src/main/java/fr/xephi/authme/security/crypts/SALTEDSHA512.java rename to src/main/java/fr/xephi/authme/security/crypts/SaltedSha512.java index f0f293439..b5660d658 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SALTEDSHA512.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SaltedSha512.java @@ -6,7 +6,7 @@ import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; @Recommendation(Usage.RECOMMENDED) -public class SALTEDSHA512 extends SeparateSaltMethod { +public class SaltedSha512 extends SeparateSaltMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java b/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java index 7d4b3d952..d0dacda4d 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java @@ -8,15 +8,15 @@ public abstract class SeparateSaltMethod implements EncryptionMethod { @Override public abstract String computeHash(String password, String salt, String name); - @Override - public abstract String generateSalt(); - @Override public HashedPassword computeHash(String password, String name) { String salt = generateSalt(); return new HashedPassword(computeHash(password, salt, name), salt); } + @Override + public abstract String generateSalt(); + @Override public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { return hashedPassword.getHash().equals(computeHash(password, hashedPassword.getSalt(), null)); diff --git a/src/main/java/fr/xephi/authme/security/crypts/Sha1.java b/src/main/java/fr/xephi/authme/security/crypts/Sha1.java new file mode 100644 index 000000000..3430752b5 --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/crypts/Sha1.java @@ -0,0 +1,15 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +@Recommendation(Usage.DEPRECATED) +public class Sha1 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return HashUtils.sha1(password); + } + +} diff --git a/src/main/java/fr/xephi/authme/security/crypts/SHA256.java b/src/main/java/fr/xephi/authme/security/crypts/Sha256.java similarity index 94% rename from src/main/java/fr/xephi/authme/security/crypts/SHA256.java rename to src/main/java/fr/xephi/authme/security/crypts/Sha256.java index ee55d4512..1b77a2e44 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SHA256.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Sha256.java @@ -6,7 +6,7 @@ import fr.xephi.authme.security.crypts.description.Usage; import static fr.xephi.authme.security.HashUtils.sha256; @Recommendation(Usage.RECOMMENDED) -public class SHA256 extends HexSaltedMethod { +public class Sha256 extends HexSaltedMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/Sha512.java b/src/main/java/fr/xephi/authme/security/crypts/Sha512.java new file mode 100644 index 000000000..4e9682ffe --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/crypts/Sha512.java @@ -0,0 +1,15 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +@Recommendation(Usage.DEPRECATED) +public class Sha512 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return HashUtils.sha512(password); + } + +} diff --git a/src/main/java/fr/xephi/authme/security/crypts/SMF.java b/src/main/java/fr/xephi/authme/security/crypts/Smf.java similarity index 85% rename from src/main/java/fr/xephi/authme/security/crypts/SMF.java rename to src/main/java/fr/xephi/authme/security/crypts/Smf.java index 175efc3fa..46114d2e9 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SMF.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Smf.java @@ -2,7 +2,7 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.HashUtils; -public class SMF extends UsernameSaltMethod { +public class Smf extends UsernameSaltMethod { @Override public HashedPassword computeHash(String password, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java b/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java index 698979d8a..23101e22a 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java @@ -17,13 +17,13 @@ public abstract class UsernameSaltMethod implements EncryptionMethod { public abstract HashedPassword computeHash(String password, String name); @Override - public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { - return hashedPassword.getHash().equals(computeHash(password, name).getHash()); + public String computeHash(String password, String salt, String name) { + return computeHash(password, name).getHash(); } @Override - public String computeHash(String password, String salt, String name) { - return computeHash(password, name).getHash(); + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return hashedPassword.getHash().equals(computeHash(password, name).getHash()); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/WBB3.java b/src/main/java/fr/xephi/authme/security/crypts/Wbb3.java similarity index 94% rename from src/main/java/fr/xephi/authme/security/crypts/WBB3.java rename to src/main/java/fr/xephi/authme/security/crypts/Wbb3.java index 546c2dc87..0a042b489 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WBB3.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Wbb3.java @@ -10,7 +10,7 @@ import static fr.xephi.authme.security.HashUtils.sha1; @Recommendation(Usage.ACCEPTABLE) @HasSalt(value = SaltType.TEXT, length = 40) -public class WBB3 extends SeparateSaltMethod { +public class Wbb3 extends SeparateSaltMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/WBB4.java b/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java similarity index 96% rename from src/main/java/fr/xephi/authme/security/crypts/WBB4.java rename to src/main/java/fr/xephi/authme/security/crypts/Wbb4.java index a81503374..d1d4953d1 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WBB4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java @@ -6,7 +6,7 @@ import fr.xephi.authme.security.crypts.description.Usage; import static fr.xephi.authme.security.crypts.BCryptService.hashpw; @Recommendation(Usage.RECOMMENDED) -public class WBB4 extends HexSaltedMethod { +public class Wbb4 extends HexSaltedMethod { @Override public String computeHash(String password, String salt, String name) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/WHIRLPOOL.java b/src/main/java/fr/xephi/authme/security/crypts/Whirlpool.java similarity index 98% rename from src/main/java/fr/xephi/authme/security/crypts/WHIRLPOOL.java rename to src/main/java/fr/xephi/authme/security/crypts/Whirlpool.java index 72d8e8fb6..c1ea747a9 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WHIRLPOOL.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Whirlpool.java @@ -59,9 +59,13 @@ package fr.xephi.authme.security.crypts; * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + import java.util.Arrays; -public class WHIRLPOOL extends UnsaltedMethod { +@Recommendation(Usage.DEPRECATED) +public class Whirlpool extends UnsaltedMethod { /** * The message digest size (in bits) @@ -158,7 +162,7 @@ public class WHIRLPOOL extends UnsaltedMethod { protected final long[] block = new long[8]; protected final long[] state = new long[8]; - public WHIRLPOOL() { + public Whirlpool() { } protected static String display(byte[] array) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/WORDPRESS.java b/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java similarity index 98% rename from src/main/java/fr/xephi/authme/security/crypts/WORDPRESS.java rename to src/main/java/fr/xephi/authme/security/crypts/Wordpress.java index f331d1fc6..768b92c5d 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WORDPRESS.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java @@ -16,7 +16,7 @@ import java.util.Arrays; @HasSalt(value = SaltType.TEXT, length = 9) // Note ljacqu 20151228: Wordpress is actually a salted algorithm but salt generation is handled internally // and isn't exposed to the outside, so we treat it as an unsalted implementation -public class WORDPRESS extends UnsaltedMethod { +public class Wordpress extends UnsaltedMethod { private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private final SecureRandom randomGen = new SecureRandom(); diff --git a/src/main/java/fr/xephi/authme/security/crypts/XAUTH.java b/src/main/java/fr/xephi/authme/security/crypts/XAuth.java similarity index 87% rename from src/main/java/fr/xephi/authme/security/crypts/XAUTH.java rename to src/main/java/fr/xephi/authme/security/crypts/XAuth.java index f2ebf1976..9f921b6ae 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XAUTH.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XAuth.java @@ -4,15 +4,15 @@ import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; @Recommendation(Usage.RECOMMENDED) -public class XAUTH extends HexSaltedMethod { +public class XAuth extends HexSaltedMethod { private static String getWhirlpool(String message) { - WHIRLPOOL w = new WHIRLPOOL(); - byte[] digest = new byte[WHIRLPOOL.DIGESTBYTES]; + Whirlpool w = new Whirlpool(); + byte[] digest = new byte[Whirlpool.DIGESTBYTES]; w.NESSIEinit(); w.NESSIEadd(message); w.NESSIEfinalize(digest); - return WHIRLPOOL.display(digest); + return Whirlpool.display(digest); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/XFBCRYPT.java b/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java similarity index 97% rename from src/main/java/fr/xephi/authme/security/crypts/XFBCRYPT.java rename to src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java index a20ee65aa..bf5545290 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XFBCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java @@ -7,7 +7,7 @@ import fr.xephi.authme.util.StringUtils; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class XFBCRYPT implements EncryptionMethod { +public class XfBCrypt implements EncryptionMethod { public static final String SCHEME_CLASS = "XenForo_Authentication_Core12"; private static final Pattern HASH_PATTERN = Pattern.compile("\"hash\";s.*\"(.*)?\""); diff --git a/src/main/java/fr/xephi/authme/security/crypts/description/Usage.java b/src/main/java/fr/xephi/authme/security/crypts/description/Usage.java index c11c335ac..9ccc52c4f 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/description/Usage.java +++ b/src/main/java/fr/xephi/authme/security/crypts/description/Usage.java @@ -20,6 +20,9 @@ public enum Usage { /** Hash algorithm is not recommended to be used. Use only if required by another system. */ DO_NOT_USE, + /** Algorithm that is or will be no longer supported actively. */ + DEPRECATED, + /** The algorithm does not work properly; do not use. */ DOES_NOT_WORK diff --git a/src/main/java/fr/xephi/authme/service/AntiBotService.java b/src/main/java/fr/xephi/authme/service/AntiBotService.java index 91f80041d..90c62a24b 100644 --- a/src/main/java/fr/xephi/authme/service/AntiBotService.java +++ b/src/main/java/fr/xephi/authme/service/AntiBotService.java @@ -30,7 +30,6 @@ public class AntiBotService implements SettingsDependent { // Settings private int duration; private int sensibility; - private int delay; private int interval; // Service status private AntiBotStatus antiBotStatus; @@ -60,7 +59,6 @@ public class AntiBotService implements SettingsDependent { // Load settings duration = settings.getProperty(ProtectionSettings.ANTIBOT_DURATION); sensibility = settings.getProperty(ProtectionSettings.ANTIBOT_SENSIBILITY); - delay = settings.getProperty(ProtectionSettings.ANTIBOT_DELAY); interval = settings.getProperty(ProtectionSettings.ANTIBOT_INTERVAL); // Stop existing protection @@ -77,6 +75,7 @@ public class AntiBotService implements SettingsDependent { // Delay the schedule on first start if (startup) { + int delay = settings.getProperty(ProtectionSettings.ANTIBOT_DELAY); bukkitService.scheduleSyncDelayedTask(enableTask, delay * TICKS_PER_SECOND); startup = false; } else { diff --git a/src/main/java/fr/xephi/authme/service/BukkitService.java b/src/main/java/fr/xephi/authme/service/BukkitService.java index 14cf0dcb7..4d94b62d5 100644 --- a/src/main/java/fr/xephi/authme/service/BukkitService.java +++ b/src/main/java/fr/xephi/authme/service/BukkitService.java @@ -13,7 +13,6 @@ import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.Event; -import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; @@ -45,7 +44,7 @@ public class BukkitService implements SettingsDependent { @Inject BukkitService(AuthMe authMe, Settings settings) { this.authMe = authMe; - getOnlinePlayersIsCollection = initializeOnlinePlayersIsCollectionField(); + getOnlinePlayersIsCollection = doesOnlinePlayersMethodReturnCollection(); reload(settings); } @@ -172,7 +171,7 @@ public class BukkitService implements SettingsDependent { * @return a BukkitTask that contains the id number * @throws IllegalArgumentException if plugin is null * @throws IllegalStateException if this was already scheduled - * @see BukkitScheduler#runTaskTimer(Plugin, Runnable, long, long) + * @see BukkitScheduler#runTaskTimer(org.bukkit.plugin.Plugin, Runnable, long, long) */ public BukkitTask runTaskTimer(BukkitRunnable task, long delay, long period) { return task.runTaskTimer(authMe, delay, period); @@ -302,11 +301,12 @@ public class BukkitService implements SettingsDependent { /** * Method run upon initialization to verify whether or not the Bukkit implementation - * returns the online players as a Collection. + * returns the online players as a {@link Collection}. * + * @return true if a collection is returned by the bukkit implementation, false otherwise * @see #getOnlinePlayers() */ - private static boolean initializeOnlinePlayersIsCollectionField() { + private static boolean doesOnlinePlayersMethodReturnCollection() { try { Method method = Bukkit.class.getDeclaredMethod("getOnlinePlayers"); return method.getReturnType() == Collection.class; diff --git a/src/main/java/fr/xephi/authme/service/CommonService.java b/src/main/java/fr/xephi/authme/service/CommonService.java index c181f83b0..d2381a455 100644 --- a/src/main/java/fr/xephi/authme/service/CommonService.java +++ b/src/main/java/fr/xephi/authme/service/CommonService.java @@ -65,16 +65,6 @@ public class CommonService { messages.send(sender, key, replacements); } - /** - * Retrieves a message. - * - * @param key the key of the message - * @return the message, split by line - */ - public String[] retrieveMessage(MessageKey key) { - return messages.retrieve(key); - } - /** * Retrieves a message in one piece. * @@ -101,10 +91,10 @@ public class CommonService { * * @param player the player to process * @param group the group to add the player to - * @return true on success, false otherwise */ - public boolean setGroup(Player player, AuthGroupType group) { - return authGroupHandler.setGroup(player, group); + // TODO ljacqu 20170304: Move this out of CommonService + public void setGroup(Player player, AuthGroupType group) { + authGroupHandler.setGroup(player, group); } } diff --git a/src/main/java/fr/xephi/authme/service/JoinMessageService.java b/src/main/java/fr/xephi/authme/service/JoinMessageService.java new file mode 100644 index 000000000..2fa5766c7 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/JoinMessageService.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.util.StringUtils; + +import javax.inject.Inject; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The JoinMessageService class. + */ +public class JoinMessageService { + + private BukkitService bukkitService; + + private Map joinMessages; + + @Inject + JoinMessageService(BukkitService bukkitService) { + this.bukkitService = bukkitService; + joinMessages = new ConcurrentHashMap<>(); + } + + /** + * Store a join message. + * + * @param playerName the player name + * @param string the join message + */ + public void putMessage(String playerName, String string) { + joinMessages.put(playerName, string); + } + + /** + * Broadcast the join message of the specified player. + * + * @param playerName the player name + */ + public void sendMessage(String playerName) { + String joinMessage = joinMessages.remove(playerName); + if (StringUtils.isEmpty(joinMessage)) { + return; + } + bukkitService.broadcastMessage(joinMessage); + } +} diff --git a/src/main/java/fr/xephi/authme/service/MigrationService.java b/src/main/java/fr/xephi/authme/service/MigrationService.java index 043de8988..338ef51a1 100644 --- a/src/main/java/fr/xephi/authme/service/MigrationService.java +++ b/src/main/java/fr/xephi/authme/service/MigrationService.java @@ -5,7 +5,7 @@ import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.security.crypts.SHA256; +import fr.xephi.authme.security.crypts.Sha256; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.SecuritySettings; @@ -20,14 +20,14 @@ public final class MigrationService { } /** - * Hash all passwords to SHA256 and updated the setting if the password hash is set to the deprecated PLAINTEXT. + * Hash all passwords to Sha256 and updated the setting if the password hash is set to the deprecated PLAINTEXT. * * @param settings The settings instance * @param dataSource The data source - * @param authmeSha256 Instance to the AuthMe SHA256 encryption method implementation + * @param authmeSha256 Instance to the AuthMe Sha256 encryption method implementation */ public static void changePlainTextToSha256(Settings settings, DataSource dataSource, - SHA256 authmeSha256) { + Sha256 authmeSha256) { if (HashAlgorithm.PLAINTEXT == settings.getProperty(SecuritySettings.PASSWORD_HASH)) { ConsoleLogger.warning("Your HashAlgorithm has been detected as plaintext and is now deprecated;" + " it will be changed and hashed now to the AuthMe default hashing method"); diff --git a/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java new file mode 100644 index 000000000..f24b45f35 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java @@ -0,0 +1,173 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.RandomStringUtils; +import fr.xephi.authme.util.expiring.Duration; +import fr.xephi.authme.util.expiring.ExpiringMap; +import fr.xephi.authme.util.expiring.ExpiringSet; +import org.bukkit.entity.Player; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.concurrent.TimeUnit; + +import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; + +/** + * Manager for password recovery. + */ +public class PasswordRecoveryService implements Reloadable, HasCleanup { + + @Inject + private CommonService commonService; + + @Inject + private DataSource dataSource; + + @Inject + private EmailService emailService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private RecoveryCodeService recoveryCodeService; + + @Inject + private Messages messages; + + private ExpiringSet emailCooldown; + private ExpiringMap successfulRecovers; + + @PostConstruct + private void initEmailCooldownSet() { + emailCooldown = new ExpiringSet<>( + commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); + successfulRecovers = new ExpiringMap<>( + commonService.getProperty(SecuritySettings.PASSWORD_CHANGE_TIMEOUT), TimeUnit.MINUTES); + } + + /** + * Create a new recovery code and send it to the player + * via email. + * + * @param player The player getting the code. + * @param email The email to send the code to. + */ + public void createAndSendRecoveryCode(Player player, String email) { + if (!checkEmailCooldown(player)) { + return; + } + + String recoveryCode = recoveryCodeService.generateCode(player.getName()); + boolean couldSendMail = emailService.sendRecoveryCode(player.getName(), email, recoveryCode); + if (couldSendMail) { + commonService.send(player, MessageKey.RECOVERY_CODE_SENT); + emailCooldown.add(player.getName().toLowerCase()); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + + /** + * Generate a new password and send it to the player via + * email. This will update the database with the new password. + * + * @param player The player recovering their password. + * @param email The email to send the password to. + */ + public void generateAndSendNewPassword(Player player, String email) { + if (!checkEmailCooldown(player)) { + return; + } + + String name = player.getName(); + String thePass = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH)); + HashedPassword hashNew = passwordSecurity.computeHash(thePass, name); + + ConsoleLogger.info("Generating new password for '" + name + "'"); + + dataSource.updatePassword(name, hashNew); + boolean couldSendMail = emailService.sendPasswordMail(name, email, thePass); + if (couldSendMail) { + commonService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); + emailCooldown.add(player.getName().toLowerCase()); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + + /** + * Allows a player to change their password after + * correctly entering a recovery code. + * + * @param player The player recovering their password. + */ + public void addSuccessfulRecovery(Player player) { + String name = player.getName(); + String address = PlayerUtils.getPlayerIp(player); + + successfulRecovers.put(name, address); + commonService.send(player, MessageKey.RECOVERY_CHANGE_PASSWORD); + } + + /** + * Check if a player is able to have emails sent. + * + * @param player The player to check. + * @return True if the player is not on cooldown. + */ + private boolean checkEmailCooldown(Player player) { + Duration waitDuration = emailCooldown.getExpiration(player.getName().toLowerCase()); + if (waitDuration.getDuration() > 0) { + String durationText = messages.formatDuration(waitDuration); + messages.send(player, MessageKey.EMAIL_COOLDOWN_ERROR, durationText); + return false; + } + return true; + } + + /** + * Checks if a player can change their password after recovery + * using the /email setpassword command. + * + * @param player The player to check. + * @return True if the player can change their password. + */ + public boolean canChangePassword(Player player) { + String name = player.getName(); + String playerAddress = PlayerUtils.getPlayerIp(player); + String storedAddress = successfulRecovers.get(name); + + if (storedAddress == null || !playerAddress.equals(storedAddress)) { + messages.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED); + return false; + } + + return true; + } + + @Override + public void reload() { + emailCooldown.setExpiration( + commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); + successfulRecovers.setExpiration( + commonService.getProperty(SecuritySettings.PASSWORD_CHANGE_TIMEOUT), TimeUnit.MINUTES); + } + + @Override + public void performCleanup() { + emailCooldown.removeExpiredEntries(); + successfulRecovers.removeExpiredEntries(); + } +} diff --git a/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java b/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java index e7fa37ad0..dcccb36de 100644 --- a/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java +++ b/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java @@ -1,38 +1,41 @@ package fr.xephi.authme.service; -import com.google.common.annotations.VisibleForTesting; +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.ExpiringMap; +import fr.xephi.authme.util.expiring.TimedCounter; import javax.inject.Inject; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static fr.xephi.authme.settings.properties.SecuritySettings.RECOVERY_CODE_HOURS_VALID; -import static fr.xephi.authme.util.Utils.MILLIS_PER_HOUR; +import java.util.concurrent.TimeUnit; /** * Manager for recovery codes. */ -public class RecoveryCodeService implements SettingsDependent { - - private Map recoveryCodes = new ConcurrentHashMap<>(); +public class RecoveryCodeService implements SettingsDependent, HasCleanup { + private final ExpiringMap recoveryCodes; + private final TimedCounter playerTries; private int recoveryCodeLength; - private long recoveryCodeExpirationMillis; + private int recoveryCodeExpiration; + private int recoveryCodeMaxTries; @Inject RecoveryCodeService(Settings settings) { - reload(settings); + recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH); + recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID); + recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES); + recoveryCodes = new ExpiringMap<>(recoveryCodeExpiration, TimeUnit.HOURS); + playerTries = new TimedCounter<>(recoveryCodeExpiration, TimeUnit.HOURS); } /** * @return whether recovery codes are enabled or not */ public boolean isRecoveryCodeNeeded() { - return recoveryCodeLength > 0 && recoveryCodeExpirationMillis > 0; + return recoveryCodeLength > 0 && recoveryCodeExpiration > 0; } /** @@ -43,7 +46,9 @@ public class RecoveryCodeService implements SettingsDependent { */ public String generateCode(String player) { String code = RandomStringUtils.generateHex(recoveryCodeLength); - recoveryCodes.put(player, new ExpiringEntry(code, System.currentTimeMillis() + recoveryCodeExpirationMillis)); + + playerTries.put(player, recoveryCodeMaxTries); + recoveryCodes.put(player, code); return code; } @@ -55,11 +60,29 @@ public class RecoveryCodeService implements SettingsDependent { * @return true if the code matches and has not expired, false otherwise */ public boolean isCodeValid(String player, String code) { - ExpiringEntry entry = recoveryCodes.get(player); - if (entry != null) { - return code != null && code.equals(entry.getCode()); - } - return false; + String storedCode = recoveryCodes.get(player); + playerTries.decrement(player); + return storedCode != null && storedCode.equals(code); + } + + /** + * Checks whether a player has tries remaining to enter a code. + * + * @param player The player to check for. + * @return True if the player has tries left. + */ + public boolean hasTriesLeft(String player) { + return playerTries.get(player) > 0; + } + + /** + * Get the number of attempts a player has to enter a code. + * + * @param player The player to check for. + * @return The number of tries left. + */ + public int getTriesLeft(String player) { + return playerTries.get(player); } /** @@ -69,31 +92,20 @@ public class RecoveryCodeService implements SettingsDependent { */ public void removeCode(String player) { recoveryCodes.remove(player); + playerTries.remove(player); } @Override public void reload(Settings settings) { recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH); - recoveryCodeExpirationMillis = settings.getProperty(RECOVERY_CODE_HOURS_VALID) * MILLIS_PER_HOUR; + recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID); + recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES); + recoveryCodes.setExpiration(recoveryCodeExpiration, TimeUnit.HOURS); } - /** - * Entry with an expiration. - */ - @VisibleForTesting - static final class ExpiringEntry { - - private final String code; - private final long expiration; - - ExpiringEntry(String code, long expiration) { - this.code = code; - this.expiration = expiration; - } - - String getCode() { - return System.currentTimeMillis() < expiration ? code : null; - } + @Override + public void performCleanup() { + recoveryCodes.removeExpiredEntries(); + playerTries.removeExpiredEntries(); } - } diff --git a/src/main/java/fr/xephi/authme/service/TeleportationService.java b/src/main/java/fr/xephi/authme/service/TeleportationService.java index 9be5012f4..65fd84044 100644 --- a/src/main/java/fr/xephi/authme/service/TeleportationService.java +++ b/src/main/java/fr/xephi/authme/service/TeleportationService.java @@ -99,19 +99,19 @@ public class TeleportationService implements Reloadable { * * @param player the player * @param auth corresponding PlayerAuth object - * @param limbo corresponding PlayerData object + * @param limbo corresponding LimboPlayer object */ public void teleportOnLogin(final Player player, PlayerAuth auth, LimboPlayer limbo) { if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { return; } - // #856: If PlayerData comes from a persisted file, the Location might be null + // #856: If LimboPlayer comes from a persisted file, the Location might be null String worldName = (limbo != null && limbo.getLocation() != null) ? limbo.getLocation().getWorld().getName() : null; - // The world in PlayerData is from where the player comes, before any teleportation by AuthMe + // The world in LimboPlayer is from where the player comes, before any teleportation by AuthMe if (mustForceSpawnAfterLogin(worldName)) { teleportToSpawn(player, true); } else if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { @@ -148,7 +148,7 @@ public class TeleportationService implements Reloadable { /** * Emits the teleportation event and performs teleportation according to it (potentially modified - * by external listeners). Note that not teleportation is performed if the event's location is empty. + * by external listeners). Note that no teleportation is performed if the event's location is empty. * * @param player the player to teleport * @param event the event to emit and according to which to teleport diff --git a/src/main/java/fr/xephi/authme/service/ValidationService.java b/src/main/java/fr/xephi/authme/service/ValidationService.java index 7905b3671..9511c6e33 100644 --- a/src/main/java/fr/xephi/authme/service/ValidationService.java +++ b/src/main/java/fr/xephi/authme/service/ValidationService.java @@ -1,6 +1,9 @@ package fr.xephi.authme.service; import ch.jalu.configme.properties.Property; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.message.MessageKey; @@ -11,9 +14,10 @@ import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.CollectionUtils; +import fr.xephi.authme.util.PlayerUtils; import fr.xephi.authme.util.Utils; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import javax.annotation.PostConstruct; import javax.inject.Inject; @@ -23,6 +27,8 @@ import java.util.List; import java.util.Set; import java.util.regex.Pattern; +import static fr.xephi.authme.util.StringUtils.isInsideString; + /** * Validation service. */ @@ -39,6 +45,7 @@ public class ValidationService implements Reloadable { private Pattern passwordRegex; private Set unrestrictedNames; + private Multimap restrictedNames; ValidationService() { } @@ -49,6 +56,9 @@ public class ValidationService implements Reloadable { passwordRegex = Utils.safePatternCompile(settings.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX)); // Use Set for more efficient contains() lookup unrestrictedNames = new HashSet<>(settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES)); + restrictedNames = settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS) + ? loadNameRestrictions(settings.getProperty(RestrictionSettings.RESTRICTED_USERS)) + : HashMultimap.create(); } /** @@ -116,9 +126,10 @@ public class ValidationService implements Reloadable { } String countryCode = geoIpService.getCountryCode(hostAddress); - return validateWhitelistAndBlacklist(countryCode, - ProtectionSettings.COUNTRIES_WHITELIST, - ProtectionSettings.COUNTRIES_BLACKLIST); + boolean isCountryAllowed = validateWhitelistAndBlacklist(countryCode, + ProtectionSettings.COUNTRIES_WHITELIST, ProtectionSettings.COUNTRIES_BLACKLIST); + ConsoleLogger.debug("Country code `{0}` for `{1}` is allowed: {2}", countryCode, hostAddress, isCountryAllowed); + return isCountryAllowed; } /** @@ -131,6 +142,24 @@ public class ValidationService implements Reloadable { return unrestrictedNames.contains(name.toLowerCase()); } + /** + * Checks that the player meets any name restriction if present (IP/domain-based). + * + * @param player the player to check + * @return true if the player may join, false if the player does not satisfy the name restrictions + */ + public boolean fulfillsNameRestrictions(Player player) { + Collection restrictions = restrictedNames.get(player.getName().toLowerCase()); + if (Utils.isCollectionEmpty(restrictions)) { + return true; + } + + String ip = PlayerUtils.getPlayerIp(player); + String domain = player.getAddress().getHostName(); + return restrictions.stream() + .anyMatch(restriction -> ip.equals(restriction) || domain.equalsIgnoreCase(restriction)); + } + /** * Verifies whether the given value is allowed according to the given whitelist and blacklist settings. * Whitelist has precedence over blacklist: if a whitelist is set, the value is rejected if not present @@ -144,11 +173,11 @@ public class ValidationService implements Reloadable { private boolean validateWhitelistAndBlacklist(String value, Property> whitelist, Property> blacklist) { List whitelistValue = settings.getProperty(whitelist); - if (!CollectionUtils.isEmpty(whitelistValue)) { + if (!Utils.isCollectionEmpty(whitelistValue)) { return containsIgnoreCase(whitelistValue, value); } List blacklistValue = settings.getProperty(blacklist); - return CollectionUtils.isEmpty(blacklistValue) || !containsIgnoreCase(blacklistValue, value); + return Utils.isCollectionEmpty(blacklistValue) || !containsIgnoreCase(blacklistValue, value); } private static boolean containsIgnoreCase(Collection coll, String needle) { @@ -160,6 +189,26 @@ public class ValidationService implements Reloadable { return false; } + /** + * Loads the configured name restrictions into a Multimap by player name (all-lowercase). + * + * @param configuredRestrictions the restriction rules to convert to a map + * @return map of allowed IPs/domain names by player name + */ + private Multimap loadNameRestrictions(List configuredRestrictions) { + Multimap restrictions = HashMultimap.create(); + for (String restriction : configuredRestrictions) { + if (isInsideString(';', restriction)) { + String[] data = restriction.split(";"); + restrictions.put(data[0].toLowerCase(), data[1]); + } else { + ConsoleLogger.warning("Restricted user rule must have a ';' separating name from restriction," + + " but found: '" + restriction + "'"); + } + } + return restrictions; + } + public static final class ValidationResult { private final MessageKey messageKey; private final String[] args; @@ -195,6 +244,7 @@ public class ValidationService implements Reloadable { public MessageKey getMessageKey() { return messageKey; } + public String[] getArgs() { return args; } diff --git a/src/main/java/fr/xephi/authme/settings/EnumSetProperty.java b/src/main/java/fr/xephi/authme/settings/EnumSetProperty.java index 37ebba815..901c9fc1e 100644 --- a/src/main/java/fr/xephi/authme/settings/EnumSetProperty.java +++ b/src/main/java/fr/xephi/authme/settings/EnumSetProperty.java @@ -12,6 +12,8 @@ import static com.google.common.collect.Sets.newHashSet; /** * Property whose value is a set of entries of a given enum. + * + * @param the enum type */ public class EnumSetProperty> extends Property> { diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index d287e140c..980f2adde 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -19,7 +19,6 @@ import static fr.xephi.authme.util.FileUtils.copyFileFromResource; public class Settings extends SettingsManager { private final File pluginFolder; - private String[] welcomeMessage; private String passwordEmailMessage; private String recoveryCodeEmailMessage; @@ -56,19 +55,9 @@ public class Settings extends SettingsManager { return recoveryCodeEmailMessage; } - /** - * Return the lines to output after an in-game registration. - * - * @return The welcome message - */ - public String[] getWelcomeMessage() { - return welcomeMessage; - } - private void loadSettingsFromFiles() { passwordEmailMessage = readFile("email.html"); recoveryCodeEmailMessage = readFile("recovery_code_email.html"); - welcomeMessage = readFile("welcome.txt").split("\\n"); } @Override diff --git a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java index aac60dde5..ade6833da 100644 --- a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java +++ b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java @@ -71,6 +71,7 @@ public class SettingsMigrationService extends PlainMigrationService { | hasOldHelpHeaderProperty(resource) | hasSupportOldPasswordProperty(resource) | convertToRegistrationType(resource) + | mergeAndMovePermissionGroupSettings(resource) || hasDeprecatedProperties(resource); } @@ -81,7 +82,8 @@ public class SettingsMigrationService extends PlainMigrationService { "VeryGames", "settings.restrictions.allowAllCommandsIfRegistrationIsOptional", "DataSource.mySQLWebsite", "Hooks.customAttributes", "Security.stop.kickPlayersBeforeStopping", "settings.restrictions.keepCollisionsDisabled", "settings.forceCommands", "settings.forceCommandsAsConsole", - "settings.forceRegisterCommands", "settings.forceRegisterCommandsAsConsole"}; + "settings.forceRegisterCommands", "settings.forceRegisterCommandsAsConsole", + "settings.sessions.sessionExpireOnIpChange"}; for (String deprecatedPath : deprecatedProperties) { if (resource.contains(deprecatedPath)) { return true; @@ -251,6 +253,26 @@ public class SettingsMigrationService extends PlainMigrationService { return true; } + private static boolean mergeAndMovePermissionGroupSettings(PropertyResource resource) { + boolean performedChanges; + + // We have two old settings replaced by only one: move the first non-empty one + Property oldUnloggedInGroup = newProperty("settings.security.unLoggedinGroup", ""); + Property oldRegisteredGroup = newProperty("GroupOptions.RegisteredPlayerGroup", ""); + if (!oldUnloggedInGroup.getValue(resource).isEmpty()) { + performedChanges = moveProperty(oldUnloggedInGroup, PluginSettings.REGISTERED_GROUP, resource); + } else { + performedChanges = moveProperty(oldRegisteredGroup, PluginSettings.REGISTERED_GROUP, resource); + } + + // Move paths of other old options + performedChanges |= moveProperty(newProperty("GroupOptions.UnregisteredPlayerGroup", ""), + PluginSettings.UNREGISTERED_GROUP, resource); + performedChanges |= moveProperty(newProperty("permission.EnablePermissionCheck", false), + PluginSettings.ENABLE_PERMISSION_CHECK, resource); + return performedChanges; + } + /** * Checks for an old property path and moves it to a new path if present. * @@ -264,9 +286,10 @@ public class SettingsMigrationService extends PlainMigrationService { Property newProperty, PropertyResource resource) { if (resource.contains(oldProperty.getPath())) { - ConsoleLogger.info("Detected deprecated property " + oldProperty.getPath()); - if (!resource.contains(newProperty.getPath())) { - ConsoleLogger.info("Renamed " + oldProperty.getPath() + " to " + newProperty.getPath()); + if (resource.contains(newProperty.getPath())) { + ConsoleLogger.info("Detected deprecated property " + oldProperty.getPath()); + } else { + ConsoleLogger.info("Renaming " + oldProperty.getPath() + " to " + newProperty.getPath()); resource.setValue(newProperty.getPath(), oldProperty.getValue(resource)); } return true; diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index ac1742fa6..26f7d03b9 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -1,10 +1,9 @@ package fr.xephi.authme.settings; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.FileUtils; @@ -43,11 +42,9 @@ public class SpawnLoader implements Reloadable { * @param pluginFolder The AuthMe data folder * @param settings The setting instance * @param pluginHookService The plugin hooks instance - * @param dataSource The plugin auth database instance */ @Inject - SpawnLoader(@DataFolder File pluginFolder, Settings settings, PluginHookService pluginHookService, - DataSource dataSource) { + SpawnLoader(@DataFolder File pluginFolder, Settings settings, PluginHookService pluginHookService) { // TODO ljacqu 20160312: Check if resource could be copied and handle the case if not File spawnFile = new File(pluginFolder, "spawn.yml"); FileUtils.copyFileFromResource(spawnFile, "spawn.yml"); diff --git a/src/main/java/fr/xephi/authme/settings/WelcomeMessageConfiguration.java b/src/main/java/fr/xephi/authme/settings/WelcomeMessageConfiguration.java new file mode 100644 index 000000000..60666f7a3 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/WelcomeMessageConfiguration.java @@ -0,0 +1,97 @@ +package fr.xephi.authme.settings; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.GeoIpService; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.lazytags.Tag; +import fr.xephi.authme.util.lazytags.TagReplacer; +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static fr.xephi.authme.util.FileUtils.copyFileFromResource; +import static fr.xephi.authme.util.lazytags.TagBuilder.createTag; + +/** + * Configuration for the welcome message (welcome.txt). + */ +public class WelcomeMessageConfiguration implements Reloadable { + + @DataFolder + @Inject + private File pluginFolder; + + @Inject + private Server server; + + @Inject + private GeoIpService geoIpService; + + @Inject + private BukkitService bukkitService; + + @Inject + private PlayerCache playerCache; + + /** List of all supported tags for the welcome message. */ + private final List> availableTags = Arrays.asList( + createTag("&", () -> "\u00a7"), + createTag("{PLAYER}", pl -> pl.getName()), + createTag("{ONLINE}", () -> Integer.toString(bukkitService.getOnlinePlayers().size())), + createTag("{MAXPLAYERS}", () -> Integer.toString(server.getMaxPlayers())), + createTag("{IP}", pl -> PlayerUtils.getPlayerIp(pl)), + createTag("{LOGINS}", () -> Integer.toString(playerCache.getLogged())), + createTag("{WORLD}", pl -> pl.getWorld().getName()), + createTag("{SERVER}", () -> server.getServerName()), + createTag("{VERSION}", () -> server.getBukkitVersion()), + createTag("{COUNTRY}", pl -> geoIpService.getCountryName(PlayerUtils.getPlayerIp(pl)))); + + private TagReplacer messageSupplier; + + @PostConstruct + @Override + public void reload() { + List welcomeMessage = readWelcomeFile(); + messageSupplier = TagReplacer.newReplacer(availableTags, welcomeMessage); + } + + /** + * Returns the welcome message for the given player. + * + * @param player the player for whom the welcome message should be prepared + * @return the welcome message + */ + public List getWelcomeMessage(Player player) { + return messageSupplier.getAdaptedMessages(player); + } + + /** + * @return the lines of the welcome message file + */ + private List readWelcomeFile() { + File welcomeFile = new File(pluginFolder, "welcome.txt"); + if (copyFileFromResource(welcomeFile, "welcome.txt")) { + try { + return Files.readAllLines(welcomeFile.toPath(), StandardCharsets.UTF_8); + } catch (IOException e) { + ConsoleLogger.logException("Failed to read welcome.txt file:", e); + } + } else { + ConsoleLogger.warning("Failed to copy welcome.txt from JAR"); + } + return Collections.emptyList(); + } +} diff --git a/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java b/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java index bc0abf38a..f996640c9 100644 --- a/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java +++ b/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java @@ -5,13 +5,21 @@ import ch.jalu.configme.resource.YamlFileResource; import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.GeoIpService; import fr.xephi.authme.util.FileUtils; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.lazytags.Tag; +import fr.xephi.authme.util.lazytags.WrappedTagReplacer; import org.bukkit.entity.Player; import javax.inject.Inject; import java.io.File; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import static fr.xephi.authme.util.lazytags.TagBuilder.createTag; + /** * Manages configurable commands to be run when various events occur. */ @@ -19,15 +27,20 @@ public class CommandManager implements Reloadable { private final File dataFolder; private final BukkitService bukkitService; + private final GeoIpService geoIpService; private final CommandMigrationService commandMigrationService; + private final List> availableTags = buildAvailableTags(); - private CommandConfig commandConfig; + private WrappedTagReplacer onJoinCommands; + private WrappedTagReplacer onLoginCommands; + private WrappedTagReplacer onRegisterCommands; @Inject - CommandManager(@DataFolder File dataFolder, BukkitService bukkitService, + CommandManager(@DataFolder File dataFolder, BukkitService bukkitService, GeoIpService geoIpService, CommandMigrationService commandMigrationService) { this.dataFolder = dataFolder; this.bukkitService = bukkitService; + this.geoIpService = geoIpService; this.commandMigrationService = commandMigrationService; reload(); } @@ -38,7 +51,7 @@ public class CommandManager implements Reloadable { * @param player the joining player */ public void runCommandsOnJoin(Player player) { - executeCommands(player, commandConfig.getOnJoin()); + executeCommands(player, onJoinCommands.getAdaptedItems(player)); } /** @@ -47,7 +60,7 @@ public class CommandManager implements Reloadable { * @param player the player who has registered */ public void runCommandsOnRegister(Player player) { - executeCommands(player, commandConfig.getOnRegister()); + executeCommands(player, onRegisterCommands.getAdaptedItems(player)); } /** @@ -56,12 +69,12 @@ public class CommandManager implements Reloadable { * @param player the player that logged in */ public void runCommandsOnLogin(Player player) { - executeCommands(player, commandConfig.getOnLogin()); + executeCommands(player, onLoginCommands.getAdaptedItems(player)); } - private void executeCommands(Player player, Map commands) { - for (Command command : commands.values()) { - final String execution = command.getCommand().replace("%p", player.getName()); + private void executeCommands(Player player, List commands) { + for (Command command : commands) { + final String execution = command.getCommand(); if (Executor.CONSOLE.equals(command.getExecutor())) { bukkitService.dispatchConsoleCommand(execution); } else { @@ -77,8 +90,22 @@ public class CommandManager implements Reloadable { SettingsManager settingsManager = new SettingsManager( new YamlFileResource(file), commandMigrationService, CommandSettingsHolder.class); - commandConfig = settingsManager.getProperty(CommandSettingsHolder.COMMANDS); + CommandConfig commandConfig = settingsManager.getProperty(CommandSettingsHolder.COMMANDS); + onJoinCommands = newReplacer(commandConfig.getOnJoin()); + onLoginCommands = newReplacer(commandConfig.getOnLogin()); + onRegisterCommands = newReplacer(commandConfig.getOnRegister()); } + private WrappedTagReplacer newReplacer(Map commands) { + return new WrappedTagReplacer<>(availableTags, commands.values(), Command::getCommand, + (cmd, text) -> new Command(text, cmd.getExecutor())); + } + private List> buildAvailableTags() { + return Arrays.asList( + createTag("%p", pl -> pl.getName()), + createTag("%nick", pl -> pl.getDisplayName()), + createTag("%ip", pl -> PlayerUtils.getPlayerIp(pl)), + createTag("%country", pl -> geoIpService.getCountryName(PlayerUtils.getPlayerIp(pl)))); + } } diff --git a/src/main/java/fr/xephi/authme/settings/commandconfig/CommandSettingsHolder.java b/src/main/java/fr/xephi/authme/settings/commandconfig/CommandSettingsHolder.java index 11557c096..7104cee2b 100644 --- a/src/main/java/fr/xephi/authme/settings/commandconfig/CommandSettingsHolder.java +++ b/src/main/java/fr/xephi/authme/settings/commandconfig/CommandSettingsHolder.java @@ -24,7 +24,12 @@ public final class CommandSettingsHolder implements SettingsHolder { public static Map sectionComments() { String[] comments = { "This configuration file allows you to execute commands on various events.", - "%p in commands will be replaced with the player name.", + "Supported placeholders in commands:", + " %p is replaced with the player name.", + " %nick is replaced with the player's nick name", + " %ip is replaced with the player's IP address", + " %country is replaced with the player's country", + "", "For example, if you want to send a welcome message to a player who just registered:", "onRegister:", " welcome:", diff --git a/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java b/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java index 8cfb29dc4..2fbe7ea24 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java +++ b/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java @@ -20,9 +20,9 @@ public final class AuthMeSettingsRetriever { */ public static ConfigurationData buildConfigurationData() { return ConfigurationDataBuilder.collectData( - DatabaseSettings.class, ConverterSettings.class, PluginSettings.class, - RestrictionSettings.class, EmailSettings.class, HooksSettings.class, - ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class, - RegistrationSettings.class, BackupSettings.class); + DatabaseSettings.class, PluginSettings.class, RestrictionSettings.class, + EmailSettings.class, HooksSettings.class, ProtectionSettings.class, + PurgeSettings.class, SecuritySettings.class, RegistrationSettings.class, + LimboSettings.class, BackupSettings.class, ConverterSettings.class); } } diff --git a/src/main/java/fr/xephi/authme/settings/properties/BackupSettings.java b/src/main/java/fr/xephi/authme/settings/properties/BackupSettings.java index 162bf8aac..57bb5941f 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/BackupSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/BackupSettings.java @@ -6,7 +6,7 @@ import ch.jalu.configme.properties.Property; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class BackupSettings implements SettingsHolder { +public final class BackupSettings implements SettingsHolder { @Comment("Enable or disable automatic backup") public static final Property ENABLED = diff --git a/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java index ae289e546..d2b34c9ac 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java @@ -6,7 +6,7 @@ import ch.jalu.configme.properties.Property; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class ConverterSettings implements SettingsHolder { +public final class ConverterSettings implements SettingsHolder { @Comment("Rakamak file name") public static final Property RAKAMAK_FILE_NAME = diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index ed877f87b..1e4697bf3 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -7,10 +7,10 @@ import fr.xephi.authme.datasource.DataSourceType; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class DatabaseSettings implements SettingsHolder { +public final class DatabaseSettings implements SettingsHolder { @Comment({"What type of database do you want to use?", - "Valid values: sqlite, mysql"}) + "Valid values: SQLITE, MYSQL"}) public static final Property BACKEND = newProperty(DataSourceType.class, "DataSource.backend", DataSourceType.SQLITE); @@ -26,11 +26,15 @@ public class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_PORT = newProperty("DataSource.mySQLPort", "3306"); - @Comment("Username about Database Connection Infos") + @Comment("Connect to MySQL database over SSL") + public static final Property MYSQL_USE_SSL = + newProperty("DataSource.mySQLUseSSL", true); + + @Comment("Username to connect to the MySQL database") public static final Property MYSQL_USERNAME = newProperty("DataSource.mySQLUsername", "authme"); - @Comment("Password about Database Connection Infos") + @Comment("Password to connect to the MySQL database") public static final Property MYSQL_PASSWORD = newProperty("DataSource.mySQLPassword", "12345"); @@ -58,10 +62,6 @@ public class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_COL_PASSWORD = newProperty("DataSource.mySQLColumnPassword", "password"); - @Comment("Request mysql over SSL") - public static final Property MYSQL_USE_SSL = - newProperty("DataSource.mySQLUseSSL", true); - @Comment("Column for storing players passwords salts") public static final Property MYSQL_COL_SALT = newProperty("ExternalBoardOptions.mySQLColumnSalt", ""); diff --git a/src/main/java/fr/xephi/authme/settings/properties/EmailSettings.java b/src/main/java/fr/xephi/authme/settings/properties/EmailSettings.java index 561ce511b..f7522b94f 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/EmailSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/EmailSettings.java @@ -9,7 +9,7 @@ import java.util.List; import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class EmailSettings implements SettingsHolder { +public final class EmailSettings implements SettingsHolder { @Comment("Email SMTP server host") public static final Property SMTP_HOST = @@ -19,6 +19,10 @@ public class EmailSettings implements SettingsHolder { public static final Property SMTP_PORT = newProperty("Email.mailPort", 465); + @Comment("Only affects port 25: enable TLS/STARTTLS?") + public static final Property PORT25_USE_TLS = + newProperty("Email.useTls", true); + @Comment("Email account which sends the mails") public static final Property MAIL_ACCOUNT = newProperty("Email.mailAccount", ""); diff --git a/src/main/java/fr/xephi/authme/settings/properties/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/properties/HooksSettings.java index b1eaa222b..e752057c1 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/HooksSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/HooksSettings.java @@ -9,7 +9,7 @@ import java.util.List; import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class HooksSettings implements SettingsHolder { +public final class HooksSettings implements SettingsHolder { @Comment("Do we need to hook with multiverse for spawn checking?") public static final Property MULTIVERSE = @@ -54,17 +54,22 @@ public class HooksSettings implements SettingsHolder { public static final Property PHPBB_ACTIVATED_GROUP_ID = newProperty("ExternalBoardOptions.phpbbActivatedGroupId", 2); + @Comment("IP Board table prefix defined during the IP Board installation process") + public static final Property IPB_TABLE_PREFIX = + newProperty("ExternalBoardOptions.IPBTablePrefix", "ipb_"); + + @Comment("IP Board default group ID; 3 is the default registered group defined by IP Board") + public static final Property IPB_ACTIVATED_GROUP_ID = + newProperty("ExternalBoardOptions.IPBActivatedGroupId", 3); + + @Comment("XenForo default group ID; 2 is the default registered group defined by Xenforo") + public static final Property XF_ACTIVATED_GROUP_ID = + newProperty("ExternalBoardOptions.XFActivatedGroupId", 2); + @Comment("Wordpress prefix defined during WordPress installation") public static final Property WORDPRESS_TABLE_PREFIX = newProperty("ExternalBoardOptions.wordpressTablePrefix", "wp_"); - @Comment("Unregistered permission group") - public static final Property UNREGISTERED_GROUP = - newProperty("GroupOptions.UnregisteredPlayerGroup", ""); - - @Comment("Registered permission group") - public static final Property REGISTERED_GROUP = - newProperty("GroupOptions.RegisteredPlayerGroup", ""); private HooksSettings() { } diff --git a/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java b/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java new file mode 100644 index 000000000..a48db6b36 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java @@ -0,0 +1,86 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SectionComments; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; +import com.google.common.collect.ImmutableMap; +import fr.xephi.authme.data.limbo.AllowFlightRestoreType; +import fr.xephi.authme.data.limbo.WalkFlySpeedRestoreType; +import fr.xephi.authme.data.limbo.persistence.LimboPersistenceType; +import fr.xephi.authme.data.limbo.persistence.SegmentConfiguration; + +import java.util.Map; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +/** + * Settings for the LimboPlayer feature. + */ +public final class LimboSettings implements SettingsHolder { + + @Comment({ + "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." + }) + public static final Property LIMBO_PERSISTENCE_TYPE = + newProperty(LimboPersistenceType.class, "limbo.persistence.type", LimboPersistenceType.INDIVIDUAL_FILES); + + @Comment({ + "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." + }) + public static final Property SEGMENT_DISTRIBUTION = + newProperty(SegmentConfiguration.class, "limbo.persistence.segmentDistribution", SegmentConfiguration.SIXTEEN); + + @Comment({ + "Whether the player is allowed to fly: RESTORE, ENABLE, DISABLE.", + "RESTORE sets back the old property from the player." + }) + public static final Property RESTORE_ALLOW_FLIGHT = + newProperty(AllowFlightRestoreType.class, "limbo.restoreAllowFlight", AllowFlightRestoreType.RESTORE); + + @Comment({ + "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" + }) + public static final Property RESTORE_FLY_SPEED = + newProperty(WalkFlySpeedRestoreType.class, "limbo.restoreFlySpeed", WalkFlySpeedRestoreType.MAX_RESTORE); + + @Comment({ + "Restore walk speed: RESTORE, DEFAULT, MAX_RESTORE, RESTORE_NO_ZERO.", + "See above for a description of the values." + }) + public static final Property RESTORE_WALK_SPEED = + newProperty(WalkFlySpeedRestoreType.class, "limbo.restoreWalkSpeed", WalkFlySpeedRestoreType.MAX_RESTORE); + + private LimboSettings() { + } + + @SectionComments + public static Map createSectionComments() { + String[] limboExplanation = { + "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." + }; + return ImmutableMap.of("limbo", limboExplanation); + } +} diff --git a/src/main/java/fr/xephi/authme/settings/properties/PluginSettings.java b/src/main/java/fr/xephi/authme/settings/properties/PluginSettings.java index cfd717ebc..d99ffa0b2 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/PluginSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/PluginSettings.java @@ -7,7 +7,7 @@ import fr.xephi.authme.output.LogLevel; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class PluginSettings implements SettingsHolder { +public final class PluginSettings implements SettingsHolder { @Comment({ "Do you want to enable the session feature?", @@ -22,20 +22,11 @@ public class PluginSettings implements SettingsHolder { @Comment({ "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" }) public static final Property SESSIONS_TIMEOUT = newProperty("settings.sessions.timeout", 10); - @Comment({ - "Should the session expire if the player tries to log in with", - "another IP address?" - }) - public static final Property SESSIONS_EXPIRE_ON_IP_CHANGE = - newProperty("settings.sessions.sessionExpireOnIpChange", true); - @Comment({ "Message language, available languages:", "https://github.com/AuthMe/AuthMeReloaded/blob/master/docs/translations.md" @@ -44,13 +35,33 @@ public class PluginSettings implements SettingsHolder { newProperty("settings.messagesLanguage", "en"); @Comment({ - "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." + "Enables switching a player to defined permission groups before they log in.", + "See below for a detailed explanation." }) public static final Property ENABLE_PERMISSION_CHECK = - newProperty("permission.EnablePermissionCheck", false); + newProperty("GroupOptions.enablePermissionCheck", false); + + @Comment({ + "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'" + }) + public static final Property REGISTERED_GROUP = + newProperty("GroupOptions.registeredPlayerGroup", ""); + + @Comment({ + "Similar to above, unregistered players can be set to the following", + "permissions group" + }) + public static final Property UNREGISTERED_GROUP = + newProperty("GroupOptions.unregisteredPlayerGroup", ""); @Comment({ "Log level: INFO, FINE, DEBUG. Use INFO for general messages,", diff --git a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java index 3a19a70e2..2bae7179c 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java @@ -10,7 +10,7 @@ import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class ProtectionSettings implements SettingsHolder { +public final class ProtectionSettings implements SettingsHolder { @Comment("Enable some servers protection (country based login, antibot)") public static final Property ENABLE_PROTECTION = diff --git a/src/main/java/fr/xephi/authme/settings/properties/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/properties/PurgeSettings.java index 0cfa029ac..2c62454c8 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/PurgeSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/PurgeSettings.java @@ -6,7 +6,7 @@ import ch.jalu.configme.properties.Property; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class PurgeSettings implements SettingsHolder { +public final class PurgeSettings implements SettingsHolder { @Comment("If enabled, AuthMe automatically purges old, unused accounts") public static final Property USE_AUTO_PURGE = diff --git a/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java index 38615b789..7d87e77bd 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java @@ -8,7 +8,7 @@ import fr.xephi.authme.process.register.RegistrationType; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class RegistrationSettings implements SettingsHolder { +public final class RegistrationSettings implements SettingsHolder { @Comment("Enable registration on the server?") public static final Property IS_ENABLED = @@ -42,7 +42,8 @@ public class RegistrationSettings implements SettingsHolder { "EMAIL_MANDATORY = for password register: 2nd argument MUST be an email address" }) public static final Property REGISTER_SECOND_ARGUMENT = - newProperty(RegisterSecondaryArgument.class, "settings.registration.secondArg", RegisterSecondaryArgument.CONFIRMATION); + newProperty(RegisterSecondaryArgument.class, "settings.registration.secondArg", + RegisterSecondaryArgument.CONFIRMATION); @Comment({ "Do we force kick a player after a successful registration?", diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 4e282728b..009fb59a0 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -10,7 +10,7 @@ import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; import static ch.jalu.configme.properties.PropertyInitializer.newLowercaseListProperty; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class RestrictionSettings implements SettingsHolder { +public final class RestrictionSettings implements SettingsHolder { @Comment({ "Can not authenticated players chat?", @@ -81,9 +81,13 @@ public class RestrictionSettings implements SettingsHolder { "Example:", " AllowedRestrictedUser:", " - playername;127.0.0.1"}) - public static final Property> ALLOWED_RESTRICTED_USERS = + public static final Property> RESTRICTED_USERS = newLowercaseListProperty("settings.restrictions.AllowedRestrictedUser"); + @Comment("Ban unknown IPs trying to log in with a restricted username?") + public static final Property BAN_UNKNOWN_IP = + newProperty("settings.restrictions.banUnsafedIP", false); + @Comment("Should unregistered players be kicked immediately?") public static final Property KICK_NON_REGISTERED = newProperty("settings.restrictions.kickNonRegistered", false); @@ -103,19 +107,13 @@ public class RestrictionSettings implements SettingsHolder { public static final Property ALLOW_UNAUTHED_MOVEMENT = newProperty("settings.restrictions.allowMovement", false); - @Comment({ - "Should not authenticated players have speed = 0?", - "This will reset the fly/walk speed to default value after the login."}) - public static final Property REMOVE_SPEED = - newProperty("settings.restrictions.removeSpeed", true); - @Comment({ "After how many seconds should players who fail to login or register", "be kicked? Set to 0 to disable."}) public static final Property TIMEOUT = newProperty("settings.restrictions.timeout", 30); - @Comment("Regex syntax of allowed characters in the player name.") + @Comment("Regex pattern of allowed characters in the player name.") public static final Property ALLOWED_NICKNAME_CHARACTERS = newProperty("settings.restrictions.allowedNicknameCharacters", "[a-zA-Z0-9_]*"); @@ -140,10 +138,6 @@ public class RestrictionSettings implements SettingsHolder { public static final Property DISPLAY_OTHER_ACCOUNTS = newProperty("settings.restrictions.displayOtherAccounts", true); - @Comment("Ban ip when the ip is not the ip registered in database") - public static final Property BAN_UNKNOWN_IP = - newProperty("settings.restrictions.banUnsafedIP", false); - @Comment("Spawn priority; values: authme, essentials, multiverse, default") public static final Property SPAWN_PRIORITY = newProperty("settings.restrictions.spawnPriority", "authme,essentials,multiverse,default"); diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index 5d3d870ce..33482dbb2 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -12,7 +12,7 @@ import java.util.Set; import static ch.jalu.configme.properties.PropertyInitializer.newLowercaseListProperty; import static ch.jalu.configme.properties.PropertyInitializer.newProperty; -public class SecuritySettings implements SettingsHolder { +public final class SecuritySettings implements SettingsHolder { @Comment({"Stop the server if we can't contact the sql database", "Take care with this, if you set this to false,", @@ -40,6 +40,10 @@ public class SecuritySettings implements SettingsHolder { public static final Property CAPTCHA_LENGTH = newProperty("Security.captcha.captchaLength", 5); + @Comment("Minutes after which login attempts count is reset for a player") + public static final Property CAPTCHA_COUNT_MINUTES_BEFORE_RESET = + newProperty("Security.captcha.captchaCountReset", 60); + @Comment("Minimum length of password") public static final Property MIN_PASSWORD_LENGTH = newProperty("settings.security.minPasswordLength", 5); @@ -48,22 +52,6 @@ public class SecuritySettings implements SettingsHolder { public static final Property MAX_PASSWORD_LENGTH = newProperty("settings.security.passwordMaxLength", 30); - @Comment({ - "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" - }) - public static final Property UNLOGGEDIN_GROUP = - newProperty("settings.security.unLoggedinGroup", "unLoggedinGroup"); - @Comment({ "Possible values: SHA256, BCRYPT, BCRYPT2Y, PBKDF2, SALTEDSHA512, WHIRLPOOL,", "MYBB, IPB3, PHPBB, PHPFUSION, SMF, XENFORO, XAUTH, JOOMLA, WBB3, WBB4, MD5VB,", @@ -98,7 +86,8 @@ public class SecuritySettings implements SettingsHolder { "- 'password'", "- 'help'"}) public static final Property> UNSAFE_PASSWORDS = - newLowercaseListProperty("settings.security.unsafePasswords", "123456", "password", "qwerty", "12345", "54321", "123456789", "help"); + newLowercaseListProperty("settings.security.unsafePasswords", + "123456", "password", "qwerty", "12345", "54321", "123456789", "help"); @Comment("Tempban a user's IP address if they enter the wrong password too many times") public static final Property TEMPBAN_ON_MAX_LOGINS = @@ -126,6 +115,23 @@ public class SecuritySettings implements SettingsHolder { public static final Property RECOVERY_CODE_HOURS_VALID = newProperty("Security.recoveryCode.validForHours", 4); + @Comment("Max number of tries to enter recovery code") + public static final Property RECOVERY_CODE_MAX_TRIES = + newProperty("Security.recoveryCode.maxTries", 3); + + @Comment({"How long a player has after password recovery to change their password", + "without logging in. This is in minutes.", + "Default: 2 minutes"}) + public static final Property PASSWORD_CHANGE_TIMEOUT = + newProperty("Security.recoveryCode.passwordChangeTimeout", 2); + + @Comment({ + "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." + }) + public static final Property EMAIL_RECOVERY_COOLDOWN_SECONDS = + newProperty("Security.emailRecovery.cooldown", 60); + private SecuritySettings() { } diff --git a/src/main/java/fr/xephi/authme/task/CleanupTask.java b/src/main/java/fr/xephi/authme/task/CleanupTask.java index 1a5bbdd65..f373c3e58 100644 --- a/src/main/java/fr/xephi/authme/task/CleanupTask.java +++ b/src/main/java/fr/xephi/authme/task/CleanupTask.java @@ -1,7 +1,7 @@ package fr.xephi.authme.task; -import ch.jalu.injector.Injector; import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.factory.SingletonStore; import org.bukkit.scheduler.BukkitRunnable; import javax.inject.Inject; @@ -12,15 +12,14 @@ import javax.inject.Inject; public class CleanupTask extends BukkitRunnable { @Inject - private Injector injector; + private SingletonStore hasCleanupStore; CleanupTask() { } @Override public void run() { - for (HasCleanup service : injector.retrieveAllOfType(HasCleanup.class)) { - service.performCleanup(); - } + hasCleanupStore.retrieveAllOfType() + .forEach(HasCleanup::performCleanup); } } diff --git a/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java b/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java deleted file mode 100644 index 234565670..000000000 --- a/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java +++ /dev/null @@ -1,123 +0,0 @@ -package fr.xephi.authme.task; - -import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.data.limbo.LimboCache; -import fr.xephi.authme.data.limbo.LimboPlayer; -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 org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; - -import javax.inject.Inject; - -import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; - -/** - * Registers tasks associated with a PlayerData. - */ -public class LimboPlayerTaskManager { - - @Inject - private Messages messages; - - @Inject - private Settings settings; - - @Inject - private BukkitService bukkitService; - - @Inject - private LimboCache limboCache; - - @Inject - private PlayerCache playerCache; - - LimboPlayerTaskManager() { - } - - - /** - * Registers a {@link MessageTask} for the given player name. - * - * @param name the name of the player to schedule a repeating message task for - * @param isRegistered whether the name is registered or not - * (false shows "please register", true shows "please log in") - */ - public void registerMessageTask(String name, boolean isRegistered) { - final int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL); - final MessageKey key = getMessageKey(isRegistered); - if (interval > 0) { - final LimboPlayer limboPlayer = limboCache.getPlayerData(name); - if (limboPlayer == null) { - ConsoleLogger.info("PlayerData for '" + name + "' is not available"); - } else { - cancelTask(limboPlayer.getMessageTask()); - MessageTask messageTask = new MessageTask(name, messages.retrieve(key), bukkitService, playerCache); - bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, interval * TICKS_PER_SECOND); - limboPlayer.setMessageTask(messageTask); - } - } - } - - /** - * Registers a {@link TimeoutTask} for the given player according to the configuration. - * - * @param player the player to register a timeout task for - */ - public void registerTimeoutTask(Player player) { - final int timeout = settings.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - if (timeout > 0) { - final LimboPlayer limboPlayer = limboCache.getPlayerData(player.getName()); - if (limboPlayer == null) { - ConsoleLogger.info("PlayerData for '" + player.getName() + "' is not available"); - } else { - cancelTask(limboPlayer.getTimeoutTask()); - String message = messages.retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR); - BukkitTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout); - limboPlayer.setTimeoutTask(task); - } - } - } - - /** - * 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 MessageKey getMessageKey(boolean isRegistered) { - if (isRegistered) { - return MessageKey.LOGIN_MESSAGE; - } else { - return MessageKey.REGISTER_MESSAGE; - } - } - - /** - * Null-safe method to cancel a potentially existing task. - * - * @param task the task to cancel (or null) - */ - private static void cancelTask(BukkitTask task) { - if (task != null) { - task.cancel(); - } - } - - /** - * Null-safe method to cancel a potentially existing task. - * - * @param task the task to cancel (or null) - */ - private static void cancelTask(BukkitRunnable task) { - if (task != null) { - task.cancel(); - } - } -} diff --git a/src/main/java/fr/xephi/authme/task/MessageTask.java b/src/main/java/fr/xephi/authme/task/MessageTask.java index 74ef45f1a..cf4366d9d 100644 --- a/src/main/java/fr/xephi/authme/task/MessageTask.java +++ b/src/main/java/fr/xephi/authme/task/MessageTask.java @@ -1,7 +1,5 @@ package fr.xephi.authme.task; -import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.service.BukkitService; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; @@ -10,20 +8,16 @@ import org.bukkit.scheduler.BukkitRunnable; */ public class MessageTask extends BukkitRunnable { - private final String name; + private final Player player; private final String[] message; - private final BukkitService bukkitService; - private final PlayerCache playerCache; private boolean isMuted; /* * Constructor. */ - public MessageTask(String name, String[] lines, BukkitService bukkitService, PlayerCache playerCache) { - this.name = name; + public MessageTask(Player player, String[] lines) { + this.player = player; this.message = lines; - this.bukkitService = bukkitService; - this.playerCache = playerCache; isMuted = false; } @@ -33,21 +27,8 @@ public class MessageTask extends BukkitRunnable { @Override public void run() { - if (playerCache.isAuthenticated(name)) { - cancel(); - } - - if (isMuted) { - return; - } - - for (Player player : bukkitService.getOnlinePlayers()) { - if (player.getName().equalsIgnoreCase(name)) { - for (String ms : message) { - player.sendMessage(ms); - } - break; - } + if (!isMuted) { + player.sendMessage(message); } } } diff --git a/src/main/java/fr/xephi/authme/task/purge/PurgeService.java b/src/main/java/fr/xephi/authme/task/purge/PurgeService.java index 7a1d19fa4..fe6d2fbf8 100644 --- a/src/main/java/fr/xephi/authme/task/purge/PurgeService.java +++ b/src/main/java/fr/xephi/authme/task/purge/PurgeService.java @@ -3,20 +3,19 @@ package fr.xephi.authme.task.purge; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.PurgeSettings; -import fr.xephi.authme.service.BukkitService; -import fr.xephi.authme.util.CollectionUtils; +import fr.xephi.authme.util.Utils; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; -import org.bukkit.command.ConsoleCommandSender; import javax.inject.Inject; import java.util.Calendar; import java.util.Collection; import java.util.Set; -// TODO: move into services. -sgdc3 +import static fr.xephi.authme.util.Utils.logAndSendMessage; /** * Initiates purge tasks. @@ -75,7 +74,7 @@ public class PurgeService { public void runPurge(CommandSender sender, long until, boolean includeEntriesWithLastLoginZero) { //todo: note this should may run async because it may executes a SQL-Query Set toPurge = dataSource.getRecordsToPurge(until, includeEntriesWithLastLoginZero); - if (CollectionUtils.isEmpty(toPurge)) { + if (Utils.isCollectionEmpty(toPurge)) { logAndSendMessage(sender, "No players to purge"); return; } @@ -119,12 +118,4 @@ public class PurgeService { void executePurge(Collection players, Collection names) { purgeExecutor.executePurge(players, names); } - - private static void logAndSendMessage(CommandSender sender, String message) { - ConsoleLogger.info(message); - // Make sure sender is not console user, which will see the message from ConsoleLogger already - if (sender != null && !(sender instanceof ConsoleCommandSender)) { - sender.sendMessage(message); - } - } } diff --git a/src/main/java/fr/xephi/authme/util/CollectionUtils.java b/src/main/java/fr/xephi/authme/util/CollectionUtils.java deleted file mode 100644 index 2388fc608..000000000 --- a/src/main/java/fr/xephi/authme/util/CollectionUtils.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.xephi.authme.util; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * Utils class for collections. - */ -public final class CollectionUtils { - - // Utility class - private CollectionUtils() { - } - - /** - * Get a range from a list based on start and count parameters in a safe way. - * - * @param element - * @param list The List - * @param start The start index - * @param count The number of elements to add - * - * @return The sublist consisting at most of {@code count} elements (less if the parameters - * exceed the size of the list) - */ - public static List getRange(List list, int start, int count) { - if (start >= list.size() || count <= 0) { - return new ArrayList<>(); - } else if (start < 0) { - start = 0; - } - int end = Math.min(list.size(), start + count); - return list.subList(start, end); - } - - /** - * Get all elements from a list starting from the given index. - * - * @param element - * @param list The List - * @param start The start index - * - * @return The sublist of all elements from index {@code start} and on; empty list - * if the start index exceeds the list's size - */ - public static List getRange(List list, int start) { - if (start >= list.size()) { - return new ArrayList<>(); - } - return getRange(list, start, list.size() - start); - } - - /** - * Null-safe way to check whether a collection is empty or not. - * - * @param coll The collection to verify - * @return True if the collection is null or empty, false otherwise - */ - public static boolean isEmpty(Collection coll) { - return coll == null || coll.isEmpty(); - } -} diff --git a/src/main/java/fr/xephi/authme/util/PlayerUtils.java b/src/main/java/fr/xephi/authme/util/PlayerUtils.java index 7c7302eb8..3c4e067b2 100644 --- a/src/main/java/fr/xephi/authme/util/PlayerUtils.java +++ b/src/main/java/fr/xephi/authme/util/PlayerUtils.java @@ -6,7 +6,7 @@ import org.bukkit.entity.Player; /** * Player utilities. */ -public class PlayerUtils { +public final class PlayerUtils { // Utility class private PlayerUtils() { diff --git a/src/main/java/fr/xephi/authme/util/RandomStringUtils.java b/src/main/java/fr/xephi/authme/util/RandomStringUtils.java index 0ad582abe..2dce3c64a 100644 --- a/src/main/java/fr/xephi/authme/util/RandomStringUtils.java +++ b/src/main/java/fr/xephi/authme/util/RandomStringUtils.java @@ -8,7 +8,7 @@ import java.util.Random; */ public final class RandomStringUtils { - private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final char[] CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); private static final Random RANDOM = new SecureRandom(); private static final int HEX_MAX_INDEX = 16; private static final int LOWER_ALPHANUMERIC_INDEX = 36; @@ -24,7 +24,7 @@ public final class RandomStringUtils { * @return The random string */ public static String generate(int length) { - return generate(length, LOWER_ALPHANUMERIC_INDEX); + return generateString(length, LOWER_ALPHANUMERIC_INDEX); } /** @@ -35,7 +35,7 @@ public final class RandomStringUtils { * @return The random hexadecimal string */ public static String generateHex(int length) { - return generate(length, HEX_MAX_INDEX); + return generateString(length, HEX_MAX_INDEX); } /** @@ -46,16 +46,16 @@ public final class RandomStringUtils { * @return The random string */ public static String generateLowerUpper(int length) { - return generate(length, CHARS.length()); + return generateString(length, CHARS.length); } - private static String generate(int length, int maxIndex) { + private static String generateString(int length, int maxIndex) { if (length < 0) { throw new IllegalArgumentException("Length must be positive but was " + length); } StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; ++i) { - sb.append(CHARS.charAt(RANDOM.nextInt(maxIndex))); + sb.append(CHARS[RANDOM.nextInt(maxIndex)]); } return sb.toString(); } diff --git a/src/main/java/fr/xephi/authme/util/StringUtils.java b/src/main/java/fr/xephi/authme/util/StringUtils.java index a4fc733eb..1f200c0f0 100644 --- a/src/main/java/fr/xephi/authme/util/StringUtils.java +++ b/src/main/java/fr/xephi/authme/util/StringUtils.java @@ -42,7 +42,7 @@ public final class StringUtils { * * @return True if the string contains at least one of the items */ - public static boolean containsAny(String str, String... pieces) { + public static boolean containsAny(String str, Iterable pieces) { if (str == null) { return false; } @@ -76,4 +76,20 @@ public final class StringUtils { public static String formatException(Throwable th) { return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); } + + /** + * Check that the given needle is in the middle of the haystack, i.e. that the haystack + * contains the needle and that it is not at the very start or end. + * + * @param needle the needle to search for + * @param haystack the haystack to search in + * + * @return true if the needle is in the middle of the word, false otherwise + */ + // Note ljacqu 20170314: `needle` is restricted to char type intentionally because something like + // isInsideString("11", "2211") would unexpectedly return true... + public static boolean isInsideString(char needle, String haystack) { + int index = haystack.indexOf(needle); + return index > 0 && index < haystack.length() - 1; + } } diff --git a/src/main/java/fr/xephi/authme/util/Utils.java b/src/main/java/fr/xephi/authme/util/Utils.java index b3ec53c7b..4d6983df5 100644 --- a/src/main/java/fr/xephi/authme/util/Utils.java +++ b/src/main/java/fr/xephi/authme/util/Utils.java @@ -1,7 +1,10 @@ package fr.xephi.authme.util; import fr.xephi.authme.ConsoleLogger; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import java.util.Collection; import java.util.regex.Pattern; /** @@ -50,6 +53,32 @@ public final class Utils { } } + /** + * Sends a message to the given sender (null safe), and logs the message to the console. + * This method is aware that the command sender might be the console sender and avoids + * displaying the message twice in this case. + * + * @param sender the sender to inform + * @param message the message to log and send + */ + public static void logAndSendMessage(CommandSender sender, String message) { + ConsoleLogger.info(message); + // Make sure sender is not console user, which will see the message from ConsoleLogger already + if (sender != null && !(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(message); + } + } + + /** + * Null-safe way to check whether a collection is empty or not. + * + * @param coll The collection to verify + * @return True if the collection is null or empty, false otherwise + */ + public static boolean isCollectionEmpty(Collection coll) { + return coll == null || coll.isEmpty(); + } + /** * Return the available core count of the JVM. * diff --git a/src/main/java/fr/xephi/authme/util/expiring/Duration.java b/src/main/java/fr/xephi/authme/util/expiring/Duration.java new file mode 100644 index 000000000..ecc86299e --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/expiring/Duration.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.util.expiring; + +import java.util.concurrent.TimeUnit; + +/** + * Represents a duration in time, defined by a time unit and a duration. + */ +public class Duration { + + private final long duration; + private final TimeUnit unit; + + /** + * Constructor. + * + * @param duration the duration + * @param unit the time unit in which {@code duration} is expressed + */ + public Duration(long duration, TimeUnit unit) { + this.duration = duration; + this.unit = unit; + } + + /** + * Creates a Duration object for the given duration and unit in the most suitable time unit. + * For example, {@code createWithSuitableUnit(120, TimeUnit.SECONDS)} will return a Duration + * object of 2 minutes. + *

    + * This method only considers the time units days, hours, minutes, and seconds for the objects + * it creates. Conversion is done with {@link TimeUnit#convert} and so always rounds the + * results down. + *

    + * Further examples: + * createWithSuitableUnit(299, TimeUnit.MINUTES); // 4 hours + * createWithSuitableUnit(700, TimeUnit.MILLISECONDS); // 0 seconds + * + * @param sourceDuration the duration + * @param sourceUnit the time unit the duration is expressed in + * @return Duration object using the most suitable time unit + */ + public static Duration createWithSuitableUnit(long sourceDuration, TimeUnit sourceUnit) { + long durationMillis = Math.abs(TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit)); + + TimeUnit targetUnit; + if (durationMillis > 1000L * 60L * 60L * 24L) { + targetUnit = TimeUnit.DAYS; + } else if (durationMillis > 1000L * 60L * 60L) { + targetUnit = TimeUnit.HOURS; + } else if (durationMillis > 1000L * 60L) { + targetUnit = TimeUnit.MINUTES; + } else { + targetUnit = TimeUnit.SECONDS; + } + + long durationInTargetUnit = targetUnit.convert(sourceDuration, sourceUnit); + return new Duration(durationInTargetUnit, targetUnit); + } + + /** + * @return the duration + */ + public long getDuration() { + return duration; + } + + /** + * @return the time unit in which the duration is expressed + */ + public TimeUnit getTimeUnit() { + return unit; + } +} diff --git a/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java b/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java new file mode 100644 index 000000000..3bf19351b --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java @@ -0,0 +1,138 @@ +package fr.xephi.authme.util.expiring; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Map with expiring entries. Following a configured amount of time after + * an entry has been inserted, the map will act as if the entry does not + * exist. + *

    + * Time starts counting directly after insertion. Inserting a new entry with + * a key that already has a value will "reset" the expiration. Although the + * expiration can be redefined later on, only entries which are inserted + * afterwards will use the new expiration. + *

    + * An expiration of {@code <= 0} will make the map expire all entries + * immediately after insertion. Note that the map does not remove expired + * entries automatically; this is only done when calling + * {@link #removeExpiredEntries()}. + * + * @param the key type + * @param the value type + */ +public class ExpiringMap { + + private final Map> entries = new ConcurrentHashMap<>(); + private long expirationMillis; + + /** + * Constructor. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public ExpiringMap(long duration, TimeUnit unit) { + setExpiration(duration, unit); + } + + /** + * Returns the value associated with the given key, + * if available and not expired. + * + * @param key the key to look up + * @return the associated value, or {@code null} if not available + */ + public V get(K key) { + ExpiringEntry value = entries.get(key); + if (value == null) { + return null; + } else if (System.currentTimeMillis() > value.getExpiration()) { + entries.remove(key); + return null; + } + return value.getValue(); + } + + /** + * Inserts a value for the given key. Overwrites a previous value + * for the key if it exists. + * + * @param key the key to insert a value for + * @param value the value to insert + */ + public void put(K key, V value) { + long expiration = System.currentTimeMillis() + expirationMillis; + entries.put(key, new ExpiringEntry<>(value, expiration)); + } + + /** + * Removes the value for the given key, if available. + * + * @param key the key to remove the value for + */ + public void remove(K key) { + entries.remove(key); + } + + /** + * Removes all entries which have expired from the internal structure. + */ + public void removeExpiredEntries() { + entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue().getExpiration()); + } + + /** + * Sets a new expiration duration. Note that already present entries + * will still make use of the old expiration. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public void setExpiration(long duration, TimeUnit unit) { + this.expirationMillis = unit.toMillis(duration); + } + + /** + * Returns whether this map is empty. This reflects the state of the + * internal map, which may contain expired entries only. The result + * may change after running {@link #removeExpiredEntries()}. + * + * @return true if map is really empty, false otherwise + */ + public boolean isEmpty() { + return entries.isEmpty(); + } + + /** + * @return the internal map + */ + protected Map> getEntries() { + return entries; + } + + /** + * Class holding a value paired with an expiration timestamp. + * + * @param the value type + */ + protected static final class ExpiringEntry { + + private final V value; + private final long expiration; + + ExpiringEntry(V value, long expiration) { + this.value = value; + this.expiration = expiration; + } + + V getValue() { + return value; + } + + long getExpiration() { + return expiration; + } + } +} diff --git a/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java b/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java new file mode 100644 index 000000000..fea8fb313 --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java @@ -0,0 +1,125 @@ +package fr.xephi.authme.util.expiring; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Set whose entries expire after a configurable amount of time. Once an entry + * has expired, the set will act as if the entry no longer exists. Time starts + * counting after the entry has been inserted. + *

    + * Internally, expired entries are not guaranteed to be cleared automatically. + * A cleanup of all expired entries may be triggered with + * {@link #removeExpiredEntries()}. Adding an entry that is already present + * effectively resets its expiration. + * + * @param the type of the entries + */ +public class ExpiringSet { + + private Map entries = new ConcurrentHashMap<>(); + private long expirationMillis; + + /** + * Constructor. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public ExpiringSet(long duration, TimeUnit unit) { + setExpiration(duration, unit); + } + + /** + * Adds an entry to the set. + * + * @param entry the entry to add + */ + public void add(E entry) { + entries.put(entry, System.currentTimeMillis() + expirationMillis); + } + + /** + * Returns whether this set contains the given entry, if it hasn't expired. + * + * @param entry the entry to check + * @return true if the entry is present and not expired, false otherwise + */ + public boolean contains(E entry) { + Long expiration = entries.get(entry); + if (expiration == null) { + return false; + } else if (expiration > System.currentTimeMillis()) { + return true; + } else { + entries.remove(entry); + return false; + } + } + + /** + * Removes the given entry from the set (if present). + * + * @param entry the entry to remove + */ + public void remove(E entry) { + entries.remove(entry); + } + + /** + * Removes all entries from the set. + */ + public void clear() { + entries.clear(); + } + + /** + * Removes all entries which have expired from the internal structure. + */ + public void removeExpiredEntries() { + entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue()); + } + + /** + * Returns the duration of the entry until it expires (provided it is not removed or re-added). + * If the entry does not exist, a duration of -1 seconds is returned. + * + * @param entry the entry whose duration before it expires should be returned + * @return duration the entry will remain in the set (if there are not modifications) + */ + public Duration getExpiration(E entry) { + Long expiration = entries.get(entry); + if (expiration == null) { + return new Duration(-1, TimeUnit.SECONDS); + } + long stillPresentMillis = expiration - System.currentTimeMillis(); + if (stillPresentMillis < 0) { + entries.remove(entry); + return new Duration(-1, TimeUnit.SECONDS); + } + return Duration.createWithSuitableUnit(stillPresentMillis, TimeUnit.MILLISECONDS); + } + + /** + * Sets a new expiration duration. Note that already present entries + * will still make use of the old expiration. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public void setExpiration(long duration, TimeUnit unit) { + this.expirationMillis = unit.toMillis(duration); + } + + /** + * Returns whether this map is empty. This reflects the state of the + * internal map, which may contain expired entries only. The result + * may change after running {@link #removeExpiredEntries()}. + * + * @return true if map is really empty, false otherwise + */ + public boolean isEmpty() { + return entries.isEmpty(); + } +} diff --git a/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java b/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java new file mode 100644 index 000000000..d6d8bba1d --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.util.expiring; + +import java.util.concurrent.TimeUnit; + +/** + * Keeps a count per key which expires after a configurable amount of time. + *

    + * Once the expiration of an entry has been reached, the counter resets + * to 0. The counter returns 0 rather than {@code null} for any given key. + * + * @param the type of the key + */ +public class TimedCounter extends ExpiringMap { + + /** + * Constructor. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public TimedCounter(long duration, TimeUnit unit) { + super(duration, unit); + } + + @Override + public Integer get(K key) { + Integer value = super.get(key); + return value == null ? 0 : value; + } + + /** + * Increments the value stored for the provided key. + * + * @param key the key to increment the counter for + */ + public void increment(K key) { + put(key, get(key) + 1); + } + + /** + * Decrements the value stored for the provided key. + * This method will NOT update the expiration. + * + * @param key the key to increment the counter for + */ + public void decrement(K key) { + ExpiringEntry e = getEntries().get(key); + + if (e != null) { + if (e.getValue() <= 0) { + remove(key); + } else { + getEntries().put(key, new ExpiringEntry<>(e.getValue() - 1, e.getExpiration())); + } + } + } + + /** + * Calculates the total of all non-expired entries in this counter. + * + * @return the total of all valid entries + */ + public int total() { + long currentTime = System.currentTimeMillis(); + return getEntries().values().stream() + .filter(entry -> currentTime <= entry.getExpiration()) + .map(ExpiringEntry::getValue) + .reduce(0, Integer::sum); + } +} diff --git a/src/main/java/fr/xephi/authme/util/lazytags/DependentTag.java b/src/main/java/fr/xephi/authme/util/lazytags/DependentTag.java new file mode 100644 index 000000000..5fb0cd3d3 --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/lazytags/DependentTag.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.function.Function; + +/** + * Replaceable tag whose value depends on an argument. + * + * @param the argument type + */ +public class DependentTag implements Tag { + + private final String name; + private final Function replacementFunction; + + /** + * Constructor. + * + * @param name the tag (placeholder) that will be replaced + * @param replacementFunction the function producing the replacement + */ + public DependentTag(String name, Function replacementFunction) { + this.name = name; + this.replacementFunction = replacementFunction; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(A argument) { + return replacementFunction.apply(argument); + } +} diff --git a/src/main/java/fr/xephi/authme/util/lazytags/SimpleTag.java b/src/main/java/fr/xephi/authme/util/lazytags/SimpleTag.java new file mode 100644 index 000000000..a5bb58a26 --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/lazytags/SimpleTag.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.function.Supplier; + +/** + * Tag to be replaced that does not depend on an argument. + * + * @param type of the argument (not used in this implementation) + */ +public class SimpleTag implements Tag { + + private final String name; + private final Supplier replacementFunction; + + public SimpleTag(String name, Supplier replacementFunction) { + this.name = name; + this.replacementFunction = replacementFunction; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(A argument) { + return replacementFunction.get(); + } +} diff --git a/src/main/java/fr/xephi/authme/util/lazytags/Tag.java b/src/main/java/fr/xephi/authme/util/lazytags/Tag.java new file mode 100644 index 000000000..2c7c6ba53 --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/lazytags/Tag.java @@ -0,0 +1,22 @@ +package fr.xephi.authme.util.lazytags; + +/** + * Represents a tag in a text to be replaced with a value (which may depend on some argument). + * + * @param argument type the replacement may depend on + */ +public interface Tag { + + /** + * @return the tag to replace + */ + String getName(); + + /** + * Returns the value to replace the tag with for the given argument. + * + * @param argument the argument to evaluate the replacement for + * @return the replacement + */ + String getValue(A argument); +} diff --git a/src/main/java/fr/xephi/authme/util/lazytags/TagBuilder.java b/src/main/java/fr/xephi/authme/util/lazytags/TagBuilder.java new file mode 100644 index 000000000..677b30e2f --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/lazytags/TagBuilder.java @@ -0,0 +1,21 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Utility class for creating tags. + */ +public final class TagBuilder { + + private TagBuilder() { + } + + public static Tag createTag(String name, Function replacementFunction) { + return new DependentTag<>(name, replacementFunction); + } + + public static Tag createTag(String name, Supplier replacementFunction) { + return new SimpleTag<>(name, replacementFunction); + } +} diff --git a/src/main/java/fr/xephi/authme/util/lazytags/TagReplacer.java b/src/main/java/fr/xephi/authme/util/lazytags/TagReplacer.java new file mode 100644 index 000000000..a9d193199 --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/lazytags/TagReplacer.java @@ -0,0 +1,102 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Replaces tags lazily by first determining which tags are being used + * and only applying those replacements afterwards. + * + * @param the argument type + */ +public final class TagReplacer { + + private final List> tags; + private final Collection messages; + + /** + * Private constructor. Use {@link #newReplacer(Collection, Collection)}. + * + * @param tags the tags that are being used in the messages + * @param messages the messages + */ + private TagReplacer(List> tags, Collection messages) { + this.tags = tags; + this.messages = messages; + } + + /** + * Creates a new instance of this class, which will provide the given + * messages adapted with the provided tags. + * + * @param allTags all available tags + * @param messages the messages to use + * @param the argument type + * @return new tag replacer instance + */ + public static TagReplacer newReplacer(Collection> allTags, Collection messages) { + List> usedTags = determineUsedTags(allTags, messages); + return new TagReplacer<>(usedTags, messages); + } + + /** + * Returns the messages with the tags applied for the given argument. + * + * @param argument the argument to get the messages for + * @return the adapted messages + */ + public List getAdaptedMessages(A argument) { + // Note ljacqu 20170121: Using a Map might seem more natural here but we avoid doing so for performance + // Although the performance gain here is probably minimal... + List tagValues = new LinkedList<>(); + for (Tag tag : tags) { + tagValues.add(new TagValue(tag.getName(), tag.getValue(argument))); + } + + List adaptedMessages = new LinkedList<>(); + for (String line : messages) { + String adaptedLine = line; + for (TagValue tagValue : tagValues) { + adaptedLine = adaptedLine.replace(tagValue.tag, tagValue.value); + } + adaptedMessages.add(adaptedLine); + } + return adaptedMessages; + } + + /** + * Determines which tags are used somewhere in the given list of messages. + * + * @param allTags all available tags + * @param messages the messages + * @param argument type + * @return tags used at least once + */ + private static List> determineUsedTags(Collection> allTags, Collection messages) { + return allTags.stream() + .filter(tag -> messages.stream().anyMatch(msg -> msg.contains(tag.getName()))) + .collect(Collectors.toList()); + } + + /** (Tag, value) pair. */ + private static final class TagValue { + + /** The tag to replace. */ + private final String tag; + /** The value to replace with. */ + private final String value; + + TagValue(String tag, String value) { + this.tag = tag; + this.value = value; + } + + @Override + public String toString() { + return "TagValue[tag='" + tag + "', value='" + value + "']"; + } + } + +} diff --git a/src/main/java/fr/xephi/authme/util/lazytags/WrappedTagReplacer.java b/src/main/java/fr/xephi/authme/util/lazytags/WrappedTagReplacer.java new file mode 100644 index 000000000..ce9487e21 --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/lazytags/WrappedTagReplacer.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Applies tags lazily to the String property of an item. This class wraps + * a {@link TagReplacer} with the extraction of the String property and + * the creation of new items with the adapted string property. + * + * @param the item type + * @param the argument type to evaluate the replacements + */ +public class WrappedTagReplacer { + + private final Collection items; + private final BiFunction itemCreator; + private final TagReplacer tagReplacer; + + /** + * Constructor. + * + * @param allTags all available tags + * @param items the items to apply the replacements on + * @param stringGetter getter of the String property to adapt on the items + * @param itemCreator a function taking (T, String): the original item and the adapted String, returning a new item + */ + public WrappedTagReplacer(Collection> allTags, + Collection items, + Function stringGetter, + BiFunction itemCreator) { + this.items = items; + this.itemCreator = itemCreator; + + List stringItems = items.stream().map(stringGetter).collect(Collectors.toList()); + tagReplacer = TagReplacer.newReplacer(allTags, stringItems); + } + + /** + * Creates adapted items for the given argument. + * + * @param argument the argument to adapt the items for + * @return the adapted items + */ + public List getAdaptedItems(A argument) { + List adaptedStrings = tagReplacer.getAdaptedMessages(argument); + List adaptedItems = new LinkedList<>(); + + Iterator originalItemsIter = items.iterator(); + Iterator newStringsIter = adaptedStrings.iterator(); + while (originalItemsIter.hasNext() && newStringsIter.hasNext()) { + adaptedItems.add(itemCreator.apply(originalItemsIter.next(), newStringsIter.next())); + } + return adaptedItems; + } +} diff --git a/src/main/resources/commands.yml b/src/main/resources/commands.yml index f23a68a5d..e0330bf9d 100644 --- a/src/main/resources/commands.yml +++ b/src/main/resources/commands.yml @@ -1,6 +1,11 @@ # This configuration file allows you to execute commands on various events. -# %p in commands will be replaced with the player name. +# Supported placeholders in commands: +# %p is replaced with the player name. +# %nick is replaced with the player's nick name +# %ip is replaced with the player's IP address +# %country is replaced with the player's country +# # For example, if you want to send a welcome message to a player who just registered: # onRegister: # welcome: @@ -20,7 +25,6 @@ # executor: CONSOLE # # Supported command events: onLogin, onJoin, onRegister -onLogin: - welcome: - command: 'msg %p Welcome back!' - executor: 'PLAYER' +onJoin: {} +onLogin: {} +onRegister: {} diff --git a/src/main/resources/messages/help_br.yml b/src/main/resources/messages/help_br.yml index 1a143c8e9..be4983ab7 100644 --- a/src/main/resources/messages/help_br.yml +++ b/src/main/resources/messages/help_br.yml @@ -4,6 +4,7 @@ # ------------------------------------------------------- # Lista de textos usados na seção de ajuda: common: + header: '==========[ Ajuda AuthMeReloaded ]==========' optional: 'Opcional' hasPermission: 'Você tem permissão' noPermission: 'Sem Permissão' diff --git a/src/main/resources/messages/help_de.yml b/src/main/resources/messages/help_de.yml index 46292e160..b1841ef41 100644 --- a/src/main/resources/messages/help_de.yml +++ b/src/main/resources/messages/help_de.yml @@ -1,4 +1,5 @@ common: + header: '==========[ AuthMeReloaded Hilfe ]==========' optional: 'Optional' hasPermission: 'Du hast Berechtigung' noPermission: 'Keine Berechtigung' diff --git a/src/main/resources/messages/help_fr.yml b/src/main/resources/messages/help_fr.yml index 318b451e9..7d53ae799 100644 --- a/src/main/resources/messages/help_fr.yml +++ b/src/main/resources/messages/help_fr.yml @@ -1,9 +1,9 @@ -# Traduction des messages d'aide d'AuthMe (pour la commande "/authme help ()" ou "/register help" par exemple) +# Traduction des messages d'aide d'AuthMe (par exemple, pour les messages de "/authme help ()" ou de "/register help") # ------------------------------------------------------- # Liste de texte dans les sections d'aide common: - header: '==========[ AIDE & INFOS - AuthMe ]==========' + header: '==========[ AuthMe - AIDE & INFOS ]==========' optional: 'Optionnel' hasPermission: 'Vous avez la permission' noPermission: 'Pas de permission' diff --git a/src/main/resources/messages/help_pt.yml b/src/main/resources/messages/help_pt.yml new file mode 100644 index 000000000..2b2ead6c3 --- /dev/null +++ b/src/main/resources/messages/help_pt.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AJUDA DO AuthMeReloaded ]==========' + optional: 'Opcional' + hasPermission: 'Tu tens permissão' + noPermission: 'Não tens permissão' + default: 'Padrão' + result: 'Resultado' + defaultPermissions: + notAllowed: 'Sem permissão' + opOnly: 'Só OP' + allowed: 'Toda gente é permitida' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Comando' + description: 'Breve descrição' + detailedDescription: 'Descrição detalhada' + arguments: 'Argumentos' + permissions: 'Permissões' + alternatives: 'Alternativas' + children: 'Comandos' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Registar um jogador' + detailedDescription: 'Registar um jogador com uma senha especifica.' + arg1: + label: 'jogador' + description: 'Nome de jogador' + arg2: + label: 'senha' + description: 'Senha' diff --git a/src/main/resources/messages/help_ru.yml b/src/main/resources/messages/help_ru.yml new file mode 100644 index 000000000..20d6bd9ae --- /dev/null +++ b/src/main/resources/messages/help_ru.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded Справка ]==========' + optional: 'Опционально' + hasPermission: 'У вас есть такие права' + noPermission: 'Нет прав' + default: 'По-умолчанию' + result: 'Результат' + defaultPermissions: + notAllowed: 'Нет прав' + opOnly: 'Только Операторы' + allowed: 'Разрешено всем' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Команда' + description: 'Краткое описание' + detailedDescription: 'Детальное описание' + arguments: 'Аргументы' + permissions: 'Разрешения' + alternatives: 'Альтернативы' + children: 'Команды' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Регистрация новго игрока' + detailedDescription: 'Регистрация игрока с указанным именем и паролем.' + arg1: + label: 'player' + description: 'Имя игрока' + arg2: + label: 'password' + description: 'Пароль' diff --git a/src/main/resources/messages/messages_bg.yml b/src/main/resources/messages/messages_bg.yml index 977e8eaf5..183f279c0 100644 --- a/src/main/resources/messages/messages_bg.yml +++ b/src/main/resources/messages/messages_bg.yml @@ -1,94 +1,107 @@ # Registration -reg_msg: '&cМоля регистрирай се с "/register <парола> <парола>"' +reg_msg: '&3Моля регистрирайте се с: /register парола парола' usage_reg: '&cКоманда: /register парола парола' -reg_only: '&fСамо за регистрирани! Моля посети http://example.com за регистрация' -# TODO kicked_admin_registered: 'An admin just registered you; please log in again' -registered: '&cУспешно премахната регистрация!' +reg_only: '&4Само регистрирани потребители могат да влизат в сървъра! Моля посетете http://example.com, за да се регистрирате!' +kicked_admin_registered: 'Ти беше регистриран от администратора, моля влезте отново' +registered: '&2Успешна регистрация!' reg_disabled: '&cРегистрациите са изключени!' -user_regged: '&cПотребителското име е заето!' +user_regged: '&cПотребителското име е заетo!' # Password errors on registration -password_error: '&fПаролата не съвпада' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' -# TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' -# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' -pass_len: '&cВашета парола не е достатъчно дълга или къса.' +password_error: '&cПаролите не съвпадат, провете ги отново!' +password_error_nick: '&cНе можеш да използваш потребителското си име за парола, моля изберете друга парола.' +password_error_unsafe: '&cИзбраната парола не е безопасна, моля изберете друга парола.' +password_error_chars: '&4Паролата съдържа непозволени символи. Позволени символи: REG_EX' +pass_len: '&cПаролата е твърде къса или прекалено дълга! Моля опитайте с друга парола.' # Login usage_log: '&cКоманда: /login парола' wrong_pwd: '&cГрешна парола!' -login: '&cВход успешен!' -login_msg: '&cМоля влез с "/login парола"' -timeout: '&fВремето изтече, опитай отново!' +login: '&2Успешен вход!' +login_msg: '&cМоля влезте с: /login парола' +timeout: '&4Времето за вход изтече, беше кикнат от сървъра. Моля опитайте отново!' # Errors -unknown_user: '&cПотребителя не е регистриран' -# TODO denied_command: '&cIn order to use this command you must be authenticated!' -# TODO denied_chat: '&cIn order to chat you must be authenticated!' +unknown_user: '&cПотребителското име не е регистрирано!' +denied_command: '&cЗа да използваш тази команда трябва да си си влезнал в акаунта!' +denied_chat: '&cЗа да пишеш в чата трябва даи сиси влезнал в акаунта!' not_logged_in: '&cНе си влязъл!' -# TODO tempban_max_logins: '&cYou have been temporarily banned for failing to log in too many times.' -# TODO: Missing tags %reg_count, %max_acc, %reg_names -max_reg: '&fТи достигна максималния брой регистрации!' -no_perm: '&cНямаш Достъп!' -error: '&fПолучи се грешка; Моля свържете се с админ' -unsafe_spawn: '&fТвоята локация когато излезе не беше безопасна, телепортиран си на Spawn!' -kick_forvip: '&cVIP влезе докато сървъра е пълен, ти беше изгонен!' +tempban_max_logins: '&cТи беше баннат временно, понеже си сгрешил паролата прекалено много пъти.' +max_reg: '&cТи си достигнал максималният брой регистрации (%reg_count/%max_acc %reg_names)!' +no_perm: '&4Нямаш нужните права за това действие!' +error: '&4Получи се неочаквана грешка, моля свържете се с администратора!' +kick_forvip: '&3VIP потребител влезе докато сървъра беше пълен, ти беше изгонен!' # AntiBot -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -antibot_auto_enabled: '[AuthMe] AntiBotMod автоматично включен, открита е потенциална атака!' -antibot_auto_disabled: '[AuthMe] AntiBotMod автоматично изключване след %m Минути.' +kick_antibot: 'Защитата от ботове е включена! Трябва да изчакаш няколко минути преди да влезеш в сървъра.' +antibot_auto_enabled: '&4Защитата за ботове е включена заради потенциална атака!' +antibot_auto_disabled: '&2Защитата за ботове ще се изключи след %m минута/и!' # Other messages -unregistered: '&cУспешно от-регистриран!' -# TODO accounts_owned_self: 'You own %count accounts:' -# TODO accounts_owned_other: 'The player %name has %count accounts:' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' -vb_nonActiv: '&fТвоята регистрация не е активирана, моля провери своя Имейл!' +unregistered: '&cРегистрацията е премахната успешно!' +accounts_owned_self: 'Претежаваш %count акаунт/а:' +accounts_owned_other: 'Потребителят %name има %count акаунт/а:' +two_factor_create: '&2Кода е %code. Можеш да го провериш оттука: %url' +recovery_code_sent: 'Възстановяващият код беше изпратен на твоят email адрес.' +# TODO: Missing tags %count +recovery_code_incorrect: 'Възстановяващият код е неправилен! Използвайте: /email recovery имейл, за да генерирате нов' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' +vb_nonActiv: '&cТвоят акаунт все още не е актириван, моля провете своят email адрес!' usage_unreg: '&cКоманда: /unregister парола' -pwd_changed: '&cПаролата е променена!' -logged_in: '&cВече сте влязъл!' -logout: '&cУспешен изход от регистрацията!' -reload: '&fКонфигурацията презаредена!' -usage_changepassword: '&fКоманда: /changepassword СтараПарола НоваПарола' +pwd_changed: '&2Паротала е променена успешно!' +logged_in: '&cВече си вписан!' +logout: '&2Излязохте успешно!' +reload: '&2Конфигурацията и база данните бяха презаредени правилно!' +usage_changepassword: '&cКоманда: /changepassword Стара-Парола Нова-Парола' # Session messages -# TODO invalid_session: '&cYour IP has been changed and your session data has expired!' -valid_session: '&aСесията продължена!' +invalid_session: '&cТвоят IP се е променил и сесията беше прекратена.' +valid_session: '&2Сесията е продължена.' # Error messages when joining -name_len: '&cТвоя никнейм е твърде малък или голям' -regex: '&cТвоя никнейм съдържа забранени знацхи. Позволените са: REG_EX' -country_banned: 'Твоята държава е забранена в този сървър!' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' -kick_fullserver: '&cСървъра е пълен, Съжеляваме!' -same_nick: '&fПотребител с този никнейм е в игра' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO same_ip_online: 'A player with the same IP is already in game!' +name_len: '&4Потребителското име е прекалено късо или дълга. Моля опитайте с друго потребителско име!' +regex: '&4Потребителското име съдържа забранени знаци. Позволени знаци: REG_EX' +country_banned: '&4Твоята държава е забранена в този сървър!' +not_owner_error: 'Ти не си собственика на този акаунт. Моля избери друго потребителско име!' +kick_fullserver: '&4Сървъра е пълен, моля опитайте отново!' +same_nick: '&4Вече има потребител, който играете в сървъра със същото потребителско име!' +invalid_name_case: 'Трябва да влезеш с %valid, а не с %invalid.' +same_ip_online: 'Вече има потребител със същото IP в сървъра!' # Email -usage_email_add: '&fКоманда: /email add ' -usage_email_change: '&fКоманда: /email change <СтарИмейл> <НовИмейл> ' -usage_email_recovery: '&fКоманда: /email recovery <имейл>' -new_email_invalid: '[AuthMe] Новия имейл е грешен!' -old_email_invalid: '[AuthMe] Стария имейл е грешен!' -email_invalid: '[AuthMe] Грешен имейл' -email_added: '[AuthMe] Имейла добавен !' -email_confirm: '[AuthMe] Потвърди своя имейл !' -email_changed: '[AuthMe] Имейла е сменен !' -email_send: '[AuthMe] Изпраен е имейл !' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO email_show: '&2Your current email address is: &f%email' -# TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' -# TODO email_already_used: '&4The email address is already being used' -# TODO email_send_failure: 'The email could not be sent. Please contact an administrator.' -# TODO show_no_email: '&2You currently don''t have email address associated with this account.' -add_email: '&cМоля добави своя имейл с : /email add имейл имейл' -recovery_email: '&cЗабравихте своята парола? Моля използвай /email recovery <имейл>' +usage_email_add: '&cКоманда: /email add имейл имейл' +usage_email_change: '&cКоманда: /email change Стар-Имейл Нов-Имейл' +usage_email_recovery: '&cКоманда: /email recovery имейл' +new_email_invalid: '&cНовият имейл е грешен, опитайте отново!' +old_email_invalid: '&cСтарият имейл е грешен, опитайте отново!' +email_invalid: '&cИмейла е невалиден, опитайте с друг!' +email_added: '&2Имейл адреса е добавен!' +email_confirm: '&cМоля потвърди своя имейл адрес!' +email_changed: '&2Имейл адреса е сменен!' +email_send: '&2Възстановяващият имейл е изпратен успешно. Моля провете пощата си!' +email_show: '&2Твоят имейл адрес е: &f%email' +incomplete_email_settings: 'Грешка: Не всички настройки са написани за изпращане на имейл адрес. Моля свържете се с администратора!' +email_already_used: '&4Имейл адреса вече се използва, опитайте с друг.' +email_send_failure: 'Съобщението не беше изпратено. Моля свържете се с администратора.' +show_no_email: '&2Няма добавен имейл адрес към акаунта.' +add_email: '&3Моля добавете имейл адрес към своят акаунт: /email add имейл имейл' +recovery_email: '&3Забравена парола? Използвайте: /email recovery имейл' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cВече е бил изпратен имейл адрес. Трябва а изчакаш %time преди да пратиш нов.' # Captcha -usage_captcha: '&cYou need to type a captcha, please type: /captcha ' -wrong_captcha: '&cГрешен код, използвай : /captcha THE_CAPTCHA' -valid_captcha: '&cТвоя код е валиден!' +usage_captcha: '&3Моля въведе цифрите/буквите от капчата: /captcha ' +wrong_captcha: '&cКода е грешен, използвайте: "/captcha THE_CAPTCHA" в чата!' +valid_captcha: '&2Кода е валиден!' + +# Time units +second: 'секунда' +seconds: 'секунди' +minute: 'минута' +minutes: 'минути' +hour: 'час' +hours: 'часа' +day: 'ден' +days: 'дена' diff --git a/src/main/resources/messages/messages_br.yml b/src/main/resources/messages/messages_br.yml index a6117b3be..7adea7166 100644 --- a/src/main/resources/messages/messages_br.yml +++ b/src/main/resources/messages/messages_br.yml @@ -34,7 +34,6 @@ tempban_max_logins: '&cVocê foi temporariamente banido por tentar logar muitas max_reg: '&cVocê excedeu o número máximo de inscrições (%reg_count/%max_acc %reg_names) do seu IP!' no_perm: '&4Você não tem permissão para executar esta ação!' error: '&4Ocorreu um erro inesperado, por favor contacte um administrador!' -unsafe_spawn: '&cVocê deslogou em um local inseguro e foi teleportado parao Spawn do servidor!' kick_forvip: '&3Um jogador VIP juntou-se ao servidor enquanto ele estava cheio!' # AntiBot @@ -48,7 +47,11 @@ accounts_owned_self: 'Você tem %count contas:' accounts_owned_other: 'O jogador %name tem %count contas:' two_factor_create: '&2O seu código secreto é %code. Você pode verificá-lo a partir daqui %url' recovery_code_sent: 'Um código de recuperação para redefinir sua senha foi enviada para o seu e-mail.' +# TODO: Missing tags %count recovery_code_incorrect: 'O código de recuperação esta incorreto! Use /email recovery [email] para gerar um novo!' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cA sua conta ainda não está ativada, por favor, verifique seus e-mails!' usage_unreg: '&cUse: /unregister ' pwd_changed: '&2Senha alterada com sucesso!' @@ -82,7 +85,6 @@ email_added: '&2Email adicionado com sucesso à sua conta!' email_confirm: '&cPor favor confirme seu endereço de email!' email_changed: '&2Troca de email com sucesso.!' email_send: '&2Recuperação de email enviada com sucesso! Por favor, verifique sua caixa de entrada de e-mail!' -email_exists: '&cUm e-mail de recuperação já foi enviado! Você pode descartá-lo e enviar um novo usando o comando abaixo:' email_show: '&2O seu endereço de e-mail atual é: &f%email' incomplete_email_settings: 'Erro: Nem todas as configurações necessárias estão definidas para o envio de e-mails. Entre em contato com um administrador.' email_already_used: '&4O endereço de e-mail já está sendo usado' @@ -90,8 +92,20 @@ email_send_failure: '&cO e-mail não pôde ser enviado, reporte isso a um admini show_no_email: '&2Você atualmente não têm endereço de e-mail associado a esta conta.' add_email: '&3Por favor, adicione seu e-mail para a sua conta com o comando "/email add "' recovery_email: '&3Esqueceu sua senha? Por favor, use o comando "/email recovery "' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3Para iniciar sessão você tem que resolver um código captcha, utilize o comando "/captcha "' wrong_captcha: '&cCaptcha errado, por favor, escreva "/captcha THE_CAPTCHA" no chat!' valid_captcha: '&2Código Captcha resolvido corretamente!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_cz.yml b/src/main/resources/messages/messages_cz.yml index 8b653fb43..47d101efc 100644 --- a/src/main/resources/messages/messages_cz.yml +++ b/src/main/resources/messages/messages_cz.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cByl jsi dočasně zabanován za příliš mnoho neúspě max_reg: '&cPřekročil(a) jsi limit pro počet účtů (%reg_count/%max_acc %reg_names) z jedné IP adresy.' no_perm: '&cNa tento příkaz nemáš dostatečné pravomoce.' error: '&cVyskytla se chyba - kontaktujte prosím administrátora ...' -unsafe_spawn: '&cTvoje pozice při posledním odpojení byla nebezpečná, teleportuji tě proto na spawn!' kick_forvip: '&cOmlouváme se, ale VIP hráč se připojil na plný server!' # AntiBot @@ -44,7 +43,11 @@ accounts_owned_self: 'Vlastníš tyto účty (%count):' accounts_owned_other: 'Hráč %name vlastní tyto účty (%count):' two_factor_create: '&2Tvůj tajný kód je %code. Můžeš ho oskenovat zde %url' recovery_code_sent: 'Kód pro obnovení hesla byl odeslán na váš email.' +# TODO: Missing tags %count recovery_code_incorrect: 'Kód pro není správný! Použijte příkaz /email recovery [email] pro vygenerování nového.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cTvůj účet není aktivovaný, zkontroluj si svůj E-mail.' usage_unreg: '&cPoužij: "/unregister TvojeHeslo".' pwd_changed: '&cHeslo změněno!' @@ -78,7 +81,6 @@ email_added: '[AuthMe] Email přidán!' email_confirm: '[AuthMe] Potvrď prosím svůj email!' email_changed: '[AuthMe] Email změněn!' email_send: '[AuthMe] Email pro obnovení hesla odeslán!' -email_exists: '&cNový email byl odeslán! Můžeš ho zahodit a poslat jiný pomocí tohoto příkazu:' email_show: '&2Váš aktuální email je: &f%email' incomplete_email_settings: 'Chyba: chybí některé důležité informace pro odeslání emailu. Kontaktujte prosím admina.' email_already_used: '&4Tato emailová adresa je již používána' @@ -86,8 +88,20 @@ email_send_failure: 'Email nemohl být odeslán. Kontaktujte prosím admina.' show_no_email: '&2K tomuto účtu nemáte přidanou žádnou emailovou adresu.' add_email: '&cPřidej prosím svůj email pomocí : /email add TvůjEmail TvůjEmail' recovery_email: '&cZapomněl jsi heslo? Napiš: /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&cPoužij: /captcha ' wrong_captcha: '&cŠpatné opsana Captcha, pouzij prosim: /captcha THE_CAPTCHA' valid_captcha: '&cZadaná captcha je v pořádku!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 474c8237a..ec0fd224e 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cDu bist wegen zu vielen fehlgeschlagenen Login-Versuchen max_reg: '&cDu hast die maximale Anzahl an Accounts erreicht (%reg_count/%max_acc %reg_names).' no_perm: '&4Du hast keine Rechte, um diese Aktion auszuführen!' error: '&4Ein Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.' -unsafe_spawn: '&cDeine Logoutposition war unsicher, du wurdest zum Spawn teleportiert' kick_forvip: '&3Ein VIP-Spieler hat den vollen Server betreten!' # AntiBot @@ -44,7 +43,11 @@ accounts_owned_self: 'Du besitzt %count Accounts:' accounts_owned_other: 'Der Spieler %name hat %count Accounts:' two_factor_create: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' recovery_code_sent: 'Ein Wiederherstellungscode zum Zurücksetzen deines Passworts wurde an deine E-Mail-Adresse geschickt.' +# TODO: Missing tags %count recovery_code_incorrect: 'Der Wiederherstellungscode stimmt nicht! Nutze /email recovery [email] um einen neuen zu generieren.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cDein Account wurde noch nicht aktiviert. Bitte prüfe deine E-Mails!' usage_unreg: '&cBenutze: /unregister ' pwd_changed: '&2Passwort geändert!' @@ -78,7 +81,6 @@ email_added: '&2E-Mail hinzugefügt!' email_confirm: '&cBitte bestätige deine E-Mail!' email_changed: '&2E-Mail aktualisiert!' email_send: '&2Wiederherstellungs-E-Mail wurde gesendet!' -email_exists: '&cEine Wiederherstellungs-E-Mail wurde bereits versandt! Nutze folgenden Befehl um eine neue E-Mail zu versenden:' email_show: '&2Deine aktuelle E-Mail-Adresse ist: &f%email' incomplete_email_settings: 'Fehler: Es wurden nicht alle notwendigen Einstellungen vorgenommen, um E-Mails zu senden. Bitte kontaktiere einen Administrator.' email_already_used: '&4Diese E-Mail-Adresse wird bereits genutzt.' @@ -86,8 +88,20 @@ email_send_failure: 'Die E-Mail konnte nicht gesendet werden. Bitte kontaktiere show_no_email: '&2Du hast zur Zeit keine E-Mail-Adresse für deinen Account hinterlegt.' add_email: '&3Bitte hinterlege deine E-Mail-Adresse: /email add ' recovery_email: '&3Passwort vergessen? Nutze "/email recovery " für ein neues Passwort' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3Um dich einzuloggen, tippe dieses Captcha so ein: /captcha ' wrong_captcha: '&cFalsches Captcha, bitte nutze: /captcha THE_CAPTCHA' valid_captcha: '&2Das Captcha ist korrekt!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index d1b147002..fc815663f 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cYou have been temporarily banned for failing to log in to max_reg: '&cYou have exceeded the maximum number of registrations (%reg_count/%max_acc %reg_names) for your connection!' no_perm: '&4You don''t have the permission to perform this action!' error: '&4An unexpected error occurred, please contact an administrator!' -unsafe_spawn: '&cYour quit location was unsafe, you have been teleported to the world''s spawnpoint.' kick_forvip: '&3A VIP player has joined the server when it was full!' # AntiBot @@ -44,7 +43,10 @@ accounts_owned_self: 'You own %count accounts:' accounts_owned_other: 'The player %name has %count accounts:' two_factor_create: '&2Your secret code is %code. You can scan it from here %url' recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -recovery_code_incorrect: 'The recovery code is not correct! Use "/email recovery [email]" to generate a new one' +recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +recovery_code_correct: 'Recovery code entered correctly!' +recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cYour account isn''t activated yet, please check your emails!' usage_unreg: '&cUsage: /unregister ' pwd_changed: '&2Password changed successfully!' @@ -78,7 +80,6 @@ email_added: '&2Email address successfully added to your account!' email_confirm: '&cPlease confirm your email address!' email_changed: '&2Email address changed correctly!' email_send: '&2Recovery email sent successfully! Please check your email inbox!' -email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' email_show: '&2Your current email address is: &f%email' incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' email_already_used: '&4The email address is already being used' @@ -86,8 +87,20 @@ email_send_failure: 'The email could not be sent. Please contact an administrato show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&3Please add your email to your account with the command: /email add ' recovery_email: '&3Forgot your password? Please use the command: /email recovery ' +change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3To login you have to solve a captcha code, please use the command: /captcha ' wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' valid_captcha: '&2Captcha code solved correctly!' + +# Time units +second: 'second' +seconds: 'seconds' +minute: 'minute' +minutes: 'minutes' +hour: 'hour' +hours: 'hours' +day: 'day' +days: 'days' diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index e0c0c96dd..71018829b 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -33,7 +33,6 @@ tempban_max_logins: '&cHas sido expulsado temporalmente por intentar iniciar ses max_reg: '&fHas excedido la cantidad máxima de registros para tu cuenta' no_perm: '&cNo tienes permiso' error: '&fHa ocurrido un error. Por favor contacta al administrador.' -unsafe_spawn: '&fTu lugar de desconexión es inseguro, teletransportándote al punto inicial del mundo' kick_forvip: '&c¡Un jugador VIP ha ingresado al servidor lleno!' # AntiBot @@ -47,7 +46,11 @@ accounts_owned_self: 'Eres propietario de %count cuentas:' accounts_owned_other: 'El jugador %name tiene %count cuentas:' two_factor_create: '&2Tu código secreto es %code. Lo puedes escanear desde aquí %url' recovery_code_sent: 'El código de recuperación para recuperar tu contraseña se ha enviado a tu correo.' +# TODO: Missing tags %count recovery_code_incorrect: '¡El código de recuperación no es correcto! Usa "/email recovery [email]" para generar uno nuevo' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fTu cuenta no está activada aún, ¡revisa tu correo!' usage_unreg: '&cUso: /unregister contraseña' pwd_changed: '&c¡Contraseña cambiada!' @@ -81,7 +84,6 @@ email_added: '[AuthMe] Email agregado !' email_confirm: '[AuthMe] Confirma tu Email !' email_changed: '[AuthMe] Email cambiado !' email_send: '[AuthMe] Correo de recuperación enviado !' -email_exists: '&c¡El correo de recuperación ya ha sido enviado! Puedes descartarlo y enviar uno nuevo utilizando el siguiente comando:' email_show: '&2Tu dirección de E-Mail actual es: &f%email' incomplete_email_settings: 'Error: no todos los ajustes necesarios se han configurado para enviar correos. Por favor, contacta con un administrador.' email_already_used: '&4La dirección Email ya está siendo usada' @@ -89,8 +91,20 @@ email_send_failure: 'No se ha podido enviar el correo electrónico. Por favor, c show_no_email: '&2No tienes ningun E-Mail asociado en esta cuenta.' add_email: '&cPor favor agrega tu e-mail con: /email add tuEmail confirmarEmail' recovery_email: '&c¿Olvidaste tu contraseña? Por favor usa /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cEl correo ha sido enviado recientemente. Debes esperar %time antes de volver a enviar uno nuevo.' # Captcha usage_captcha: '&cUso: /captcha ' wrong_captcha: '&cCaptcha incorrecto, por favor usa: /captcha THE_CAPTCHA' valid_captcha: '&c¡ Captcha ingresado correctamente !' + +# Time units +second: 'segundo' +seconds: 'segundos' +minute: 'minuto' +minutes: 'minutos' +hour: 'hora' +hours: 'horas' +day: 'día' +days: 'días' diff --git a/src/main/resources/messages/messages_eu.yml b/src/main/resources/messages/messages_eu.yml index 53b0704d6..28ba966ab 100644 --- a/src/main/resources/messages/messages_eu.yml +++ b/src/main/resources/messages/messages_eu.yml @@ -31,7 +31,6 @@ not_logged_in: '&cSaioa hasi gabe!' max_reg: '&fKontuko 2 erabiltzaile bakarrik izan ditzakezu' no_perm: '&cBaimenik ez' error: '&fErrorea; Mesedez jarri kontaktuan administratzaile batekin' -unsafe_spawn: '&fSpawn-era telegarraiatu zara' kick_forvip: '&cVIP erabiltzaile bat sartu da zerbitzaria beteta zegoenean!' # AntiBot @@ -45,7 +44,10 @@ unregistered: '&cZure erregistroa ezabatu duzu!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fZure kontua aktibatu gabe dago, konfirmatu zure emaila!' usage_unreg: '&cErabili: /unregister password' pwd_changed: '&cPasahitza aldatu duzu!' @@ -79,7 +81,6 @@ email_added: '[AuthMe] Emaila gehitu duzu !' email_confirm: '[AuthMe] Konfirmatu zure emaila !' email_changed: '[AuthMe] Emaila aldatua!' email_send: '[AuthMe] Berreskuratze emaila bidalita !' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' # TODO email_already_used: '&4The email address is already being used' @@ -87,8 +88,20 @@ email_send: '[AuthMe] Berreskuratze emaila bidalita !' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&cMesedez gehitu zure emaila : /email add yourEmail confirmEmail' recovery_email: '&cPasahitza ahaztu duzu? Erabili /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha -# TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command "/captcha "' +# TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command: /captcha ' # TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' # TODO valid_captcha: '&2Captcha code solved correctly!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_fi.yml b/src/main/resources/messages/messages_fi.yml index bf359e8c9..0f491ce62 100644 --- a/src/main/resources/messages/messages_fi.yml +++ b/src/main/resources/messages/messages_fi.yml @@ -31,7 +31,6 @@ not_logged_in: '&cEt ole kirjautunut sisään!' max_reg: '&fSinulla ei ole oikeuksia tehdä enempää pelaajatilejä!' no_perm: '&cEi oikeuksia' error: '&fVirhe: Ota yhteys palveluntarjoojaan!' -unsafe_spawn: '&fHengenvaarallinen poistumispaikka! Siirsimme sinut spawnille!' kick_forvip: '&cVIP pelaaja liittyi täyteen palvelimeen!' # AntiBot @@ -45,7 +44,10 @@ unregistered: '&cPelaajatili poistettu onnistuneesti!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fKäyttäjäsi ei ole vahvistettu!' usage_unreg: '&cKäyttötapa: /unregister password' pwd_changed: '&cSalasana vaihdettu!!' @@ -79,7 +81,6 @@ email_added: '[AuthMe] Sähköposti lisätty!' email_confirm: '[AuthMe] Vahvistuta sähköposti!' email_changed: '[AuthMe] Sähköposti vaihdettu!' email_send: '[AuthMe] Palautus sähköposti lähetetty!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' # TODO email_already_used: '&4The email address is already being used' @@ -87,8 +88,20 @@ email_send: '[AuthMe] Palautus sähköposti lähetetty!' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&cLisää sähköpostisi: /email add sähköpostisi sähköpostisiUudelleen' recovery_email: '&cUnohtuiko salasana? Käytä komentoa: /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&cKäyttötapa: /captcha ' wrong_captcha: '&cVäärä varmistus, käytä : /captcha THE_CAPTCHA' valid_captcha: '&cSinun varmistus onnistui.!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 1bd884568..517250ac4 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -1,7 +1,7 @@ # Traduction par: André & Twonox -# Pour afficher une guillemet, mettez deux guillemets consécutivement (ex: "J''ai" au lieu de "J'ai"). -# Pour passer à la ligne, vous pouvez utiliser "%nl%" +# Pour afficher une apostrophe, vous devez en mettre deux consécutivement (ex: «J''ai» au lieu de «J'ai») +# Pour passer à la ligne, utilisez: %nl% # Inscription reg_msg: '&cPour vous inscrire, utilisez "/register "' @@ -10,7 +10,7 @@ reg_only: 'Seul les joueurs enregistrés sont admis!%nl%Veuillez vous rendre sur kicked_admin_registered: 'Un admin vient de vous inscrire, veuillez vous reconnecter.' registered: '&aInscription effectué !' reg_disabled: '&cL''inscription est désactivé.' -user_regged: '&cCet utilisateur est déjà inscrit.' +user_regged: '&cUtilisateur déjà inscrit.' # Erreurs MDP pour l'inscription password_error: '&cLe mot de passe de confirmation ne correspond pas.' @@ -27,7 +27,7 @@ login_msg: '&cPour vous connecter, utilisez "/login "' timeout: 'Vous avez été expulsé car vous êtes trop lent pour vous enregistrer/connecter !' # Erreurs -unknown_user: '&cUtilisateur introuvable.' +unknown_user: '&cUtilisateur non-inscrit.' denied_command: '&cVous devez être connecté pour pouvoir utiliser cette commande.' denied_chat: '&cVous devez être connecté pour pouvoir écrire dans le chat.' not_logged_in: '&cUtilisateur non connecté !' @@ -35,7 +35,6 @@ tempban_max_logins: '&cVous êtes temporairement banni suite à plusieurs échec max_reg: 'Vous avez atteint la limite d''inscription! &o(%reg_count/%max_acc : %reg_names)' no_perm: '&cVous n''êtes pas autorisé à utiliser cette commande.' error: '&cUne erreur est apparue, veuillez contacter un administrateur.' -unsafe_spawn: 'Téléportation dans un endroit sûr.' kick_forvip: 'Un joueur VIP a rejoint le serveur à votre place (serveur plein).' # AntiBot @@ -49,7 +48,11 @@ accounts_owned_self: 'Vous avez %count comptes:' accounts_owned_other: 'Le joueur %name a %count comptes:' two_factor_create: '&aVotre code secret est &2%code&a. Vous pouvez le scanner depuis &2%url' recovery_code_sent: 'Un code de récupération a été envoyé à votre adresse email afin de réinitialiser votre mot de passe.' +# TODO: Missing tags %count recovery_code_incorrect: '&cLe code de réinitialisation est incorrect!%nl%Faites "/email recovery [email]" pour en générer un nouveau.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fCe compte n''est pas actif, consultez vos emails !' usage_unreg: '&cPour supprimer votre compte, utilisez "/unregister "' pwd_changed: '&aMot de passe changé avec succès !' @@ -73,26 +76,37 @@ invalid_name_case: 'Veuillez vous connecter avec "%valid" et non pas avec "%inva same_ip_online: 'Un joueur avec la même adresse IP joue déjà !' # Email -usage_email_add: '&fUsage: /email add ' +usage_email_add: '&fUsage: /email add ' usage_email_change: '&fUsage: /email change ' usage_email_recovery: '&fUsage: /email recovery ' -new_email_invalid: '[AuthMe] Nouvel email invalide !' -old_email_invalid: '[AuthMe] Ancien email invalide !' -email_invalid: '[AuthMe] Email invalide' -email_added: '[AuthMe] Email ajouté !' -email_confirm: '[AuthMe] Confirmez votre email !' -email_changed: '[AuthMe] Email changé !' -email_send: '[AuthMe] Email de récupération envoyé !' -email_exists: '&cUn email de restauration a déjà été envoyé ! Vous pouvez le jeter et vous en faire envoyez un nouveau en utilisant :' +new_email_invalid: '&cNouvel email invalide !' +old_email_invalid: '&cAncien email invalide !' +email_invalid: '&cL''email inscrit est invalide !' +email_added: '&aEmail enregistré. En cas de perte de MDP, faites "/email recover "' +email_confirm: '&cLa confirmation de l''email est manquante ou éronnée.' +email_changed: '&aVotre email a été mis à jour.' +email_send: '&aEmail de récupération envoyé !' email_show: '&2Votre adresse email actuelle est: &f%email' incomplete_email_settings: '&cErreur : Tous les paramètres requis ne sont pas présent pour l''envoi de mail, veuillez contacter un admin.' email_already_used: '&cCette adresse email est déjà utilisée !' email_send_failure: '&cL''email n''a pas pu être envoyé. Veuillez contacter un admin.' -show_no_email: 'Vous n''avez aucune adresse email enregistré sur votre compte.' -add_email: '&cMerci d''ajouter votre email : /email add ' -recovery_email: '&cVous avez oublié votre Mot de Passe? Utilisez /email recovery ' +show_no_email: '&c&oVous n''avez aucune adresse mail enregistré sur votre compte.' +add_email: '&cRajoutez un email de récupération: /email add ' +recovery_email: '&cVous avez oublié votre Mot de Passe? Utilisez "/email recovery "' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cUn email de récupération a déjà été envoyé récemment. Veuillez attendre %time pour le demander de nouveau.' # Captcha usage_captcha: '&cTrop de tentatives de connexion échouées, utilisez: /captcha ' -wrong_captcha: '&cCaptcha incorrect, écrivez de nouveau : /captcha THE_CAPTCHA' +wrong_captcha: '&cCaptcha incorrect, écrivez de nouveau: /captcha THE_CAPTCHA' valid_captcha: '&aCaptché validé! Veuillez maintenant vous connecter.' + +# Unités de temps +second: 'seconde' +seconds: 'secondes' +minute: 'minute' +minutes: 'minutes' +hour: 'heure' +hours: 'heures' +day: 'jour' +days: 'jours' diff --git a/src/main/resources/messages/messages_gl.yml b/src/main/resources/messages/messages_gl.yml index 2739a1a8f..cdf5ddbe4 100644 --- a/src/main/resources/messages/messages_gl.yml +++ b/src/main/resources/messages/messages_gl.yml @@ -31,7 +31,6 @@ not_logged_in: '&cNon te identificaches!' max_reg: '&fExcediches o máximo de rexistros para a túa Conta' no_perm: '&cNon tes o permiso' error: '&fOcurriu un erro; contacta cun administrador' -unsafe_spawn: '&fA localización dende a que saíches era insegura, teletransportándote ao spawn do mundo' kick_forvip: '&cUn xogador VIP uniuse ao servidor cheo!' # AntiBot @@ -45,7 +44,10 @@ unregistered: '&cFeito! Xa non estás rexistrado!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fA túa conta aínda non está activada, comproba a túa bandexa de correo!!' usage_unreg: '&cUso: /unregister ' pwd_changed: '&cCambiouse o contrasinal!' @@ -79,7 +81,6 @@ email_added: '[AuthMe] Correo engadido!' email_confirm: '[AuthMe] Confirma o teu correo!' email_changed: '[AuthMe] Cambiouse o correo!' email_send: '[AuthMe] Enviouse o correo de confirmación!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' # TODO email_already_used: '&4The email address is already being used' @@ -87,8 +88,20 @@ email_send: '[AuthMe] Enviouse o correo de confirmación!' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&cPor favor, engade o teu correo electrónico con: /email add ' recovery_email: '&cOlvidaches o contrasinal? Por favor, usa /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&cNecesitas escribir un captcha, por favor escribe: /captcha ' wrong_captcha: '&cCaptcha equivocado, por favor usa: /captcha THE_CAPTCHA' valid_captcha: '&cO teu captcha é válido !' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index f5667eb89..bd3579872 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cIdeiglenesen ki lettél tiltva mert túl sok alkalommal r max_reg: '&cElérted a maximálisan beregisztrálható karakterek számát. (%reg_count/%max_acc %reg_names)!' no_perm: '&cNincs jogod ehhez!' error: 'Hiba lépett fel! Lépj kapcsolatba a szerver tulajával sürgősen!' -unsafe_spawn: 'A kilépési helyzeted nem biztonságos, ezért elteleportálunk a kezdő pozícióra.' kick_forvip: '&3VIP játékos csatlakozott a szerverhez!' # AntiBot @@ -44,7 +43,11 @@ accounts_owned_self: '%count db regisztrációd van:' accounts_owned_other: 'A %name nevű játékosnak, %count db regisztrációja van:' two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' recovery_code_sent: 'A jelszavad visszaállításához szükséges kódot sikeresen kiküldtük az email címedre!' +# TODO: Missing tags %count recovery_code_incorrect: 'A visszaállító kód helytelen volt! Használd a következő parancsot: /email recovery [email címed] egy új generálásához' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a megadott emailed!' usage_unreg: '&cHasználat: "/unregister "' pwd_changed: '&cJelszó sikeresen megváltoztatva!' @@ -78,7 +81,6 @@ email_added: '&2Az email címed rögzítése sikeresen megtörtént!' email_confirm: '&cKérlek ellenőrízd az email címedet!' email_changed: '&2Az email cím cseréje sikeresen megtörtént!' email_send: '&2A jelszó visszaállításhoz szükséges emailt elküldtük! Ellenőrízd a leveleidet!' -email_exists: '&cA visszaállító emailt elküldtük! Hiba esetén újra kérheted az alábbi parancs segítségével:' email_show: '&2A jelenlegi email-ed a következő: &f%email' incomplete_email_settings: 'Hiba: nem lett beállítva az össze szükséges beállítás az email küldéshez. Vedd fel a kapcsolatot egy adminnal.' email_already_used: '&4Ez az email cím már használatban van!' @@ -86,8 +88,20 @@ email_already_used: '&4Ez az email cím már használatban van!' show_no_email: '&2Ehhez a felhasználóhoz jelenleg még nincs email hozzárendelve.' add_email: '&3Kérlek rendeld hozzá a felhasználódhoz az email címedet "/email add "' recovery_email: '&3Ha elfelejtetted a jelszavad, használd az: "/email recovery "' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3A bejelentkezéshez CAPTCHA szükséges, kérlek használd a következő parancsot "/captcha "' wrong_captcha: '&cHibás CAPTCHA, kérlek írd be a következő parancsot: "/captcha THE_CAPTCHA"!' valid_captcha: '&2CAPTCHA sikeresen feloldva!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_id.yml b/src/main/resources/messages/messages_id.yml index b4fde4abb..79472486a 100644 --- a/src/main/resources/messages/messages_id.yml +++ b/src/main/resources/messages/messages_id.yml @@ -31,7 +31,6 @@ not_logged_in: '&cKamu belum login!' max_reg: '&Kamu telah mencapai batas maksimum pendaftaran di server ini!' no_perm: '&4Kamu tidak mempunyai izin melakukan ini!' error: '&4Terjadi kesalahan tak dikenal, silahkan hubungi Administrator!' -unsafe_spawn: '&cLokasi quit kamu tidak aman, kamu telah diteleport ke titik spawn world.' kick_forvip: '&3Player VIP mencoba masuk pada saat server sedang penuh!' # AntiBot @@ -45,7 +44,10 @@ unregistered: '&cUnregister berhasil!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cAkunmu belum diaktifkan, silahkan periksa email kamu!' # TODO usage_unreg: '&cUsage: /unregister ' pwd_changed: '&2Berhasil mengubah password!' @@ -79,7 +81,6 @@ email_added: '&2Berhasil menambahkan alamat email ke akunmu!' email_confirm: '&cSilahkan konfirmasi alamat email kamu!' email_changed: '&2Alamat email telah diubah dengan benar!' email_send: '&2Email pemulihan akun telah dikirim! Silahkan periksa kotak masuk emailmu!' -email_exists: '&cEmail pemulihan sudah dikirim! kamu bisa membatalkan dan mengirimkan yg baru dengan command dibawah:' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' # TODO email_already_used: '&4The email address is already being used' @@ -87,8 +88,20 @@ email_exists: '&cEmail pemulihan sudah dikirim! kamu bisa membatalkan dan mengir # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&3Silahkan tambahkan email ke akunmu menggunakan command "/email add "' recovery_email: '&3Lupa password? silahkan gunakan command "/email recovery "' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3Kamu harus menyelesaikan kode captcha untuk login, silahkan gunakan command "/captcha "' wrong_captcha: '&cCaptcha salah, gunakan command "/captcha THE_CAPTCHA" pada chat!' valid_captcha: '&2Kode captcha terselesaikan!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index cdf6fed82..a4654309f 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -32,7 +32,6 @@ tempban_max_logins: '&cSei stato temporaneamente bandito per aver fallito l''aut max_reg: '&cHai raggiunto il numero massimo di registrazioni (%reg_count/%max_acc %reg_names) per questo indirizzo IP!' no_perm: '&4Non hai il permesso di eseguire questa operazione.' error: '&4Qualcosa è andato storto, riporta questo errore ad un amministratore!' -unsafe_spawn: '&cIl tuo punto di disconnessione risulta ostruito o insicuro, sei stato teletrasportato al punto di rigenerazione!' kick_forvip: '&3Un utente VIP è entrato mentre il server era pieno e ha preso il tuo posto!' # AntiBot @@ -46,7 +45,11 @@ accounts_owned_self: 'Possiedi %count account:' accounts_owned_other: 'Il giocatore %name possiede %count account:' two_factor_create: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' recovery_code_sent: 'Una email contenente il codice di recupero per reimpostare la tua password è stata appena inviata al tuo indirizzo email.' +# TODO: Missing tags %count recovery_code_incorrect: 'Il codice di recupero inserito non è corretto! Scrivi "/email recovery " per generarne uno nuovo' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cIl tuo account non è stato ancora verificato, controlla fra le tue email per scoprire come attivarlo!' usage_unreg: '&cUtilizzo: /unregister ' pwd_changed: '&2Password cambiata correttamente!' @@ -79,17 +82,28 @@ email_invalid: '&cL''indirizzo email inserito non è valido, riprova!' email_added: '&2Indirizzo email aggiunto correttamente al tuo account!' email_confirm: '&cPer favore, conferma il tuo indirizzo email!' email_changed: '&2Indirizzo email cambiato correttamente!' -email_send: '&2Una email contenente la tua nuova password è stata appena inviata al tuo indirizzo email!' -email_exists: '&cL''email contenente la tua nuova password è già stata inviata! Se vuoi, puoi annullarla e mandarne un''altra con il seguente comando:' +email_send: '&2Una email di recupero è stata appena inviata al tuo indirizzo email!' email_show: '&2Il tuo indirizzo email al momento è: &f%email' incomplete_email_settings: 'Errore: non tutte le impostazioni richieste per inviare le email sono state impostate. Per favore contatta un amministratore.' email_already_used: '&4L''indirizzo email inserito è già in uso' -email_send_failure: 'Non è stato possibile inviare l''email contenente la tua nuova password. Per favore contatta un amministratore.' +email_send_failure: 'Non è stato possibile inviare l''email di recupero. Per favore contatta un amministratore.' show_no_email: '&2Al momento non hai nessun indirizzo email associato al tuo account.' add_email: '&3Per poter recuperare la password in futuro, aggiungi un indirizzo email al tuo account con il comando: /email add ' recovery_email: '&3Hai dimenticato la tua password? Puoi recuperarla eseguendo il comando: /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cUna email di recupero ti è già stata inviata recentemente. Devi attendere %time prima di poterne richiedere una nuova.' # Captcha usage_captcha: '&3Per poterti autenticare devi risolvere un captcha, per favore scrivi: /captcha ' wrong_captcha: '&cCaptcha sbagliato, per favore riprova scrivendo: "/captcha THE_CAPTCHA" in chat!' valid_captcha: '&2Il captcha inserito è valido!' + +# Unità di tempo +second: 'secondo' +seconds: 'secondi' +minute: 'minuto' +minutes: 'minuti' +hour: 'ora' +hours: 'ore' +day: 'giorno' +days: 'giorni' diff --git a/src/main/resources/messages/messages_ko.yml b/src/main/resources/messages/messages_ko.yml index db7bc8aa5..b96c623dd 100644 --- a/src/main/resources/messages/messages_ko.yml +++ b/src/main/resources/messages/messages_ko.yml @@ -35,7 +35,6 @@ not_logged_in: '&c접속되어있지 않습니다!' max_reg: '&f당신은 가입할 수 있는 계정의 최대 한도를 초과했습니다' no_perm: '&c권한이 없습니다' error: '&f오류가 발생했습니다; 관리자에게 문의해주세요' -unsafe_spawn: '&f당신이 종료한 위치는 안전하지 않았습니다, 세계의 소환지점으로 이동합니다' kick_forvip: '&c서버가 만원인 상태일때 VIP 플레이어들만 입장이 가능합니다!' # AntiBot @@ -49,7 +48,10 @@ unregistered: '&c성공적으로 탈퇴했습니다!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&f당신의 계정은 아직 활성화되어있지 않습니다, 당신의 이메일을 확인해보세요!' usage_unreg: '&c사용법: /unregister 비밀번호' pwd_changed: '&c비밀번호를 변경했습니다!' @@ -83,7 +85,6 @@ email_added: '[AuthMe] 이메일을 추가했습니다!' email_confirm: '[AuthMe] 당신의 이메일을 확인하세요!' email_changed: '[AuthMe] 이메일이 변경되었습니다!' email_send: '[AuthMe] 복구 이메일을 보냈습니다!' -email_exists: '[AuthMe] 당신의 계정에 이미 이메일이 존재합니다. 아래의 명령어를 통해 이메일을 변경하실 수 있습니다' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' # TODO email_already_used: '&4The email address is already being used' @@ -91,8 +92,20 @@ email_exists: '[AuthMe] 당신의 계정에 이미 이메일이 존재합니다. # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&c당신의 이메일을 추가해주세요 : /email add 당신의이메일 이메일재입력' recovery_email: '&c비밀번호를 잊어버리셨다고요? /email recovery <당신의이메일>을 사용하세요' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&c보안문자 입력이 필요합니다, 입력해주세요: /captcha ' wrong_captcha: '&c잘못된 보안문자, 사용해주세요 : /captcha THE_CAPTCHA' valid_captcha: '&c당신의 보안문자는 적합합니다!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_lt.yml b/src/main/resources/messages/messages_lt.yml index a2bcbe257..297510879 100644 --- a/src/main/resources/messages/messages_lt.yml +++ b/src/main/resources/messages/messages_lt.yml @@ -31,7 +31,6 @@ not_logged_in: '&cTu neprisijunges!' max_reg: '&cJus pasiekete maksimalu registraciju skaiciu.' no_perm: '&cNera leidimo' error: '&cAtsirado klaida, praneskite adminstratoriui.' -unsafe_spawn: '&6Atsijungimo vieta nesaugi, perkeliame jus i atsiradimo vieta.' kick_forvip: '&cA VIP prisijunge i pilna serveri!' # AntiBot @@ -45,7 +44,10 @@ unregistered: '&aSekmingai issiregistravote!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&aJusu vartotojas nera patvirtintas, patikrinkite el.pasta.' usage_unreg: '&ePanaikinti registracija: "/unregister slaptazodis"' pwd_changed: '&aSlaptazodis pakeistas' @@ -79,7 +81,6 @@ same_nick: '&cKazkas situo vardu jau zaidzia.' # TODO email_confirm: '&cPlease confirm your email address!' # TODO email_changed: '&2Email address changed correctly!' # TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' # TODO email_already_used: '&4The email address is already being used' @@ -87,8 +88,20 @@ same_nick: '&cKazkas situo vardu jau zaidzia.' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&ePrasau pridekite savo el.pasta : /email add Email confirmEmail' recovery_email: '&cPamirsote slaptazodi? Rasykite: /email recovery el.pastas' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&cPanaudojimas: /captcha ' wrong_captcha: '&cNeteisinga Captcha, naudokite : /captcha THE_CAPTCHA' valid_captcha: '&cJusu captcha Teisinga!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_nl.yml b/src/main/resources/messages/messages_nl.yml index 2ee68f951..3804681d6 100644 --- a/src/main/resources/messages/messages_nl.yml +++ b/src/main/resources/messages/messages_nl.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cJe bent tijdelijk gebanned omdat het inloggen te vaak mis max_reg: 'Je hebt het maximum aantal registraties overschreden (%reg_count/%max_acc: %reg_names).' no_perm: '&cJe hebt geen rechten om deze actie uit te voeren!' error: 'Er is een onverwachte fout opgetreden, neem contact op met een administrator!' -unsafe_spawn: '&cDe locatie waar je de vorige keer het spel verliet was gevaarlijk, je bent geteleporteerd naar de spawn.' kick_forvip: '&cEen VIP-gebruiker heeft ingelogd toen de server vol was!' # AntiBot @@ -44,7 +43,11 @@ accounts_owned_self: 'Je bezit %count accounts:' accounts_owned_other: 'De speler %name heeft %count accounts:' two_factor_create: '&2Je geheime code is %code. Je kunt hem scannen op %url' recovery_code_sent: 'Een herstelcode voor je wachtwoord is naar je mailbox gestuurd.' +# TODO: Missing tags %count recovery_code_incorrect: 'De herstelcode is niet correct! Gebruik "/email recovery [email]" om een nieuwe te krijgen' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: 'Je account is nog niet geactiveerd, controleer je mailbox!' usage_unreg: '&cGebruik: /unregister password' pwd_changed: '&cWachtwoord succesvol aangepast!' @@ -78,7 +81,6 @@ email_added: '&2Het E-mailadres is succesvol toegevoegd aan je account!' email_confirm: '&cVerifiëer je E-mailadres alsjeblieft!' email_changed: '&2Het E-mailadres is succesvol veranderd!' email_send: '&2Een herstel E-mail is verzonden! Check alsjeblieft je mailbox!' -email_exists: '&cEen herstel E-mail voor je wachtwoord is al verzonden! Je kunt een nieuwe laten sturen met het volgende commando:' email_show: '&2Jouw huidige E-mailadres is: %email' incomplete_email_settings: 'Fout; er moeten nog enkele opties ingevuld worden om mails te kunnen sturen. Neem contact op met een administrator.' email_already_used: '&4Dit E-mailadres wordt al gebruikt' @@ -86,8 +88,20 @@ email_send_failure: 'De E-mail kon niet verzonden worden. Neem contact op met ee show_no_email: '&2Je hebt nog geen E-mailadres toegevoegd aan dit account.' add_email: '&3Voeg jouw E-mailadres alsjeblieft toe met: /email add ' recovery_email: '&3Wachtwoord vergeten? Gebruik alsjeblieft het commando: /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3Om in te loggen moet je een captcha-code oplossen, gebruik het commando: /captcha ' wrong_captcha: '&cVerkeerde captcha-code, typ alsjeblieft "/captcha THE_CAPTCHA" in de chat!' valid_captcha: '&2De captcha-code is geldig!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index 3f9e37ae9..0b50688b4 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -31,7 +31,6 @@ tempban_max_logins: '&cZostales tymczasowo zbanowany za duza liczbe nieudanych l max_reg: '&fPrzekroczyles limit zarejestrowanych kont na serwerze.' no_perm: '&4Nie masz uprawnien' error: '&fBlad prosimy napisac do aministracji' -unsafe_spawn: '&fTwoje pozycja jest niebezpieczna. Zostaniesz przeniesiony na bezpieczny spawn.' kick_forvip: '&cA Gracz VIP dolaczyl do gry!' # AntiBot @@ -45,7 +44,11 @@ accounts_owned_self: 'Posiadasz %count kont:' accounts_owned_other: 'Gracz %name posiada %count kont:' two_factor_create: '&2Twoj sekretny kod to %code. Mozesz zeskanowac go tutaj %url' recovery_code_sent: 'Kod odzyskiwania hasla zostal wyslany na adres email przypisany do konta.' +# TODO: Missing tags %count recovery_code_incorrect: 'Kod odzyskiwania hasla jest bledny! Uzyj /email recovery [email] aby wygenerowac nowy.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fTwoje konto nie zostalo aktywowane! Sprawdz maila.' usage_unreg: '&cUzycie: /unregister haslo' pwd_changed: '&fHaslo zostalo zmienione!' @@ -79,7 +82,6 @@ email_added: '[AuthMe] Email dodany!' email_confirm: '[AuthMe] Potwierdz swoj email!' email_changed: '[AuthMe] Email zmieniony!' email_send: '[AuthMe] Email z odzyskaniem wyslany!' -email_exists: '&cEmail z haslem zostal wyslany, aby wyslac go ponownie uzyj:' email_show: '&2Twoj aktualny adres email to: &f%email' incomplete_email_settings: 'Error: Nie wszystkie opcje odpowiedzialne za wysylanie emaili zostaly ustawione. Skontaktuj sie z administracja.' email_already_used: '&4Ten adres email jest aktualnie uzywany!' @@ -87,8 +89,20 @@ email_send_failure: 'Nie mozna wyslac emaila. Skontaktuj sie z administracja.' show_no_email: '&2Nie posiadasz adresu email przypisanego do tego konta.' add_email: '&cProsze dodac swoj email: /email add twojEmail powtorzEmail' recovery_email: '&cZapomniales hasla? Prosze uzyj komendy /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cEmail zostal wyslany, musisz poczekac %time przed wyslaniem nastepnego.' # Captcha usage_captcha: '&cWpisz: /captcha ' wrong_captcha: '&cZly kod, prosze wpisac: /captcha THE_CAPTCHA' valid_captcha: '&cTwoj kod jest nieprawidlowy!' + +# Time units +second: 'sekundy' +seconds: 'sekund' +minute: 'minuty' +minutes: 'minut' +hour: 'godziny' +hours: 'godzin' +day: 'lata' +days: 'lat' diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index 7254f8bbf..ed6070246 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -1,24 +1,24 @@ # Registration -reg_msg: '&cPor favor registe-se com "/register password confirmePassword"' -usage_reg: '&cUse: /register seu@email.com seu@email.com' -reg_only: '&fApenas jogadores registados! Visite http://example.com para se registar' -# TODO kicked_admin_registered: 'An admin just registered you; please log in again' +reg_msg: '&cPor favor registe-se com "/register "' +usage_reg: '&cUse: /register ' +reg_only: '&fApenas jogadores registados podem entrar no servidor! Visite http://example.com para se registar' +kicked_admin_registered: 'Um administrador registou-te, por favor entre novamente' registered: '&cRegistado com sucesso!' -reg_disabled: '&cRegito de novos utilizadores desactivado' +reg_disabled: '&cRegisto de novos utilizadores desactivado' user_regged: '&cUtilizador já registado' # Password errors on registration password_error: '&fAs passwords não coincidem' password_error_nick: '&cNão pode o usar seu nome como senha, por favor, escolha outra ...' password_error_unsafe: '&cA senha escolhida não é segura, por favor, escolha outra ...' -password_error_chars: '&4Sua senha contém caracteres ilegais. caracteres permitidos: REG_EX' -pass_len: '&fPassword demasiado curta' +password_error_chars: '&4Sua senha contém caracteres ilegais. Caracteres permitidos: REG_EX' +pass_len: '&fPassword demasiado curta ou longa! Por favor escolhe outra outra!' # Login -usage_log: '&cUse: /login password' +usage_log: '&cUse: /login ' wrong_pwd: '&cPassword errada!' login: '&bAutenticado com sucesso!' -login_msg: '&cIdentifique-se com "/login password"' +login_msg: '&cIdentifique-se com "/login "' timeout: '&fExcedeu o tempo para autenticação' # Errors @@ -26,12 +26,11 @@ unknown_user: '&cUsername não registado' denied_command: '&cPara utilizar este comando é necessário estar logado!' denied_chat: '&cPara usar o chat deve estar logado!' not_logged_in: '&cNão autenticado!' -# TODO tempban_max_logins: '&cYou have been temporarily banned for failing to log in too many times.' +tempban_max_logins: '&cVocê foi temporariamente banido por falhar muitas vezes o login.' # TODO: Missing tags %reg_names max_reg: '&cAtingiu o numero máximo de %reg_count contas registas, maximo de contas %max_acc' no_perm: '&cSem Permissões' -error: '&fOcorreu um erro; Por favor contacte um admin' -unsafe_spawn: '&fA sua localização na saída não é segura, será tele-portado para a Spawn' +error: '&fOcorreu um erro; Por favor contacte um administrador' kick_forvip: '&cUm jogador VIP entrou no servidor cheio!' # AntiBot @@ -41,18 +40,22 @@ antibot_auto_disabled: '[AuthMe] AntiBotMod desactivado automaticamente após %m # Other messages unregistered: '&cRegisto eliminado com sucesso!' -# TODO accounts_owned_self: 'You own %count accounts:' -# TODO accounts_owned_other: 'The player %name has %count accounts:' +accounts_owned_self: 'Você possui %count contas:' +accounts_owned_other: 'O jogador %name possui %count contas:' two_factor_create: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' -# TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +recovery_code_sent: 'O codigo para redefinir a senha foi enviado para o seu e-mail.' +# TODO: Missing tags %count +recovery_code_incorrect: 'O codigo de recuperação está incorreto! Use "/email recovery [email]" para gerar um novo' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fA sua conta não foi ainda activada, verifique o seu email onde irá receber indicações para activação de conta. ' -usage_unreg: '&cUse: /unregister password' +usage_unreg: '&cUse: /unregister ' pwd_changed: '&cPassword alterada!' logged_in: '&cJá se encontra autenticado!' logout: '&cSaida com sucesso' reload: '&fConfiguração e base de dados foram recarregadas' -usage_changepassword: '&fUse: /changepassword passwordAntiga passwordNova' +usage_changepassword: '&fUse: /changepassword ' # Session messages invalid_session: '&fDados de sessão não correspondem. Por favor aguarde o fim da sessão' @@ -79,16 +82,27 @@ email_added: 'Email adicionado com sucesso!' email_confirm: 'Confirme o seu email!' email_changed: 'Email alterado com sucesso!' email_send: 'Nova palavra-passe enviada para o seu email!' -email_exists: '&cUm e-mail de recuperação já foi enviado! Pode descartá-lo e enviar um novo usando o comando abaixo:' -# TODO email_show: '&2Your current email address is: &f%email' -# TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' +email_show: '&2O seu endereço de email atual é &f%email' +incomplete_email_settings: 'Erro: nem todas as definições necessarias para enviar email foram preenchidas. Por favor contate um administrador.' email_already_used: '&4O endereço de e-mail já está sendo usado' -# TODO email_send_failure: 'The email could not be sent. Please contact an administrator.' -# TODO show_no_email: '&2You currently don''t have email address associated with this account.' -add_email: '&cPor favor adicione o seu email com : /email add seuEmail confirmarSeuEmail' +email_send_failure: 'Não foi possivel enviar o email. Por favor contate um administrador.' +show_no_email: '&2Você atualmente não tem um endereço de email associado a essa conta.' +add_email: '&cPor favor adicione o seu email com : /email add ' recovery_email: '&cPerdeu a sua password? Para a recuperar escreva /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cUm email já foi enviado recentemente.Por favor, espere %time antes de enviar novamente' # Captcha usage_captcha: '&cPrecisa digitar um captcha, escreva: /captcha ' wrong_captcha: '&cCaptcha errado, por favor escreva: /captcha THE_CAPTCHA' valid_captcha: '&cO seu captcha é válido!' + +# Time units +second: 'segundo' +seconds: 'segundos' +minute: 'minuto' +minutes: 'minutos' +hour: 'hora' +hours: 'horas' +day: 'dia' +days: 'dias' diff --git a/src/main/resources/messages/messages_ro.yml b/src/main/resources/messages/messages_ro.yml index d1726d69b..03817b8f6 100644 --- a/src/main/resources/messages/messages_ro.yml +++ b/src/main/resources/messages/messages_ro.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cAi fost interzis temporar deoarece ai incercat sa te aute max_reg: '&cTe-ai inregistrat cu prea multe counturi (%reg_count/%max_acc %reg_names) pentru conexiunea ta!' no_perm: '&4Nu ai permisiunea!' error: '&4A aparut o eroare, te rugam contacteaza un membru din staff!' -unsafe_spawn: '&cLocatia in care esti acum nu este sigura, ai fost teleportat la spawn.' kick_forvip: '&3Un V.I.P a intrat pe server cand era plin!' # AntiBot @@ -44,7 +43,11 @@ accounts_owned_self: 'Detii %count conturi:' accounts_owned_other: 'Jucatorul %name are %count conturi:' two_factor_create: '&2Codul tau secret este %code. Il poti scana de aici %url' recovery_code_sent: 'Un cod de recuperare a parolei a fost trimis catre email-ul tau.' +# TODO: Missing tags %count recovery_code_incorrect: 'Codul de recuperare nu este corect! Foloseste /email recovery [email] pentru a genera unul nou.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cContul tau nu este activat, te rugam verifica-ti email-ul!' usage_unreg: '&cFoloseste comanda: /unregister ' pwd_changed: '&2Parola a fost inregistrata cu succes!' @@ -78,7 +81,6 @@ email_added: '&2Email-ul a fost adaugat cu succes la contul tau!' email_confirm: '&cTe rugam confirma adresa de email!' email_changed: '&2Email-ul a fost schimbat corect!' email_send: ' &2Recuperarea email-ul a fost trimis cu succes! Te rugam sa verificati email-ul in inbox!' -email_exists: '&cRecuperarea email-ului a fost deja trimisa! Puteti sa-l anulati si sa trimiteti unul nou folosind comanda de mai jos:' email_show: '&2Adresa ta curenta de email este: &f%email' incomplete_email_settings: 'Eroare: nu toate setarile necesare pentru trimiterea email-ului sunt facute! Te rugam contacteaza un administrator.' email_already_used: '&4Email-ul a fost deja folosit' @@ -86,8 +88,20 @@ email_already_used: '&4Email-ul a fost deja folosit' show_no_email: '&2Nu ai nici-o adresa de email asociat cu acest cont.' add_email: '&3Te rugam adaugati email-ul la contul tau folosind comanda "/email add "' recovery_email: '&3Ti-ai uitat parola? Te rugam foloseste comanda "/email recovery "' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3Pentru a te autentifica trebuie sa folosesti codul de la captcha, te rugam foloseste comanda "/captcha "' wrong_captcha: '&cCod-ul captcha este gresit, te rugam foloseste comanda "/captcha THE_CAPTCHA"!' valid_captcha: '&2Cod-ul captcha a fost scris corect!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index 873b63979..5b7d15caf 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cВы были временно забанены, пото max_reg: '&cВы превысили максимальное количество регистраций на сервере! (%reg_count/%max_acc %reg_names)' no_perm: '&cНедостаточно прав' error: '&cПроизошла ошибка. Свяжитесь с администратором' -unsafe_spawn: '&eВаше расположение перед выходом было опасным - вы перенесены на спавн' kick_forvip: '&6VIP игрок зашел на переполненный сервер!' # АнтиБот (Защита от ботов) @@ -44,7 +43,11 @@ accounts_owned_self: 'Вы являетесь владельцем %count акк accounts_owned_other: 'Игрок %name имеет %count аккаунтов:' two_factor_create: '&2Ваш секретный код %code. Вы должны просканировать его здесь %url' recovery_code_sent: 'Код восстановления для сброса пароля был отправлен на вашу электронную почту.' +# TODO: Missing tags %count recovery_code_incorrect: 'Код восстановления неверный! Введите /email recovery <Ваш Email>, чтобы отправить новый код' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&6Ваш аккаунт еще не активирован! Проверьте вашу почту!' usage_unreg: '&cИспользование: &e/unregister <Пароль>' pwd_changed: '&2Пароль изменен!' @@ -78,7 +81,6 @@ email_added: '[AuthMe] Email добавлен!' email_confirm: '[AuthMe] Подтвердите ваш Email!' email_changed: '[AuthMe] Email изменен!' email_send: '[AuthMe] Письмо с инструкциями для восстановления было отправлено на ваш Email!' -email_exists: '&cВосстановительное письмо через email отправлено! Вы можете отменить его командой ниже:' email_show: '&2Ваш текущий адрес электронной почты: &f%email' incomplete_email_settings: 'Ошибка: не все необходимые параметры установлены для отправки электронной почты. Пожалуйста, обратитесь к администратору.' email_already_used: '&4Этот email уже используется.' @@ -86,8 +88,20 @@ email_send_failure: 'Письмо не може быть отправлено. show_no_email: '&2В данный момент к вашему аккаунте не привязана электронная почта.' add_email: '&cДобавьте свой email: &e/email add <Ваш Email> <Ваш Email>' recovery_email: '&cЗабыли пароль? Используйте &e/email recovery <Ваш Email>' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cЭлектронное письмо было отправлено недавно. Пожалуйста, подождите %time прежде чем отправить новое письмо.' # Каптча usage_captcha: '&cВы должны ввести код, используйте: &e/captcha ' wrong_captcha: '&cНеверный код, используйте: &e/captcha THE_CAPTCHA' valid_captcha: '&2Вы успешно ввели код!' + +# Единицы времени +second: 'сек.' +seconds: 'сек.' +minute: 'мин.' +minutes: 'мин.' +hour: 'ч.' +hours: 'ч.' +day: 'дн.' +days: 'дн.' diff --git a/src/main/resources/messages/messages_sk.yml b/src/main/resources/messages/messages_sk.yml index 5eb97d929..2f3f1c7b7 100644 --- a/src/main/resources/messages/messages_sk.yml +++ b/src/main/resources/messages/messages_sk.yml @@ -35,7 +35,6 @@ not_logged_in: '&cNie si este prihláseny!' max_reg: '&fDosiahol si maximum registrovanych uctov.' no_perm: '&cZiadne' error: '&fNastala chyba; Kontaktujte administrátora' -unsafe_spawn: '&fTvoj pozícia bol nebezpecná, teleportujem hraca na spawn' # TODO kick_forvip: '&3A VIP player has joined the server when it was full!' # AntiBot @@ -49,7 +48,10 @@ unregistered: '&cUcet bol vymazany!' # TODO accounts_owned_other: 'The player %name has %count accounts:' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&fUcet nie je aktivny. Prezri si svoj e-mail!' usage_unreg: '&cPríkaz: /unregister heslo' pwd_changed: '&cHeslo zmenené!' @@ -83,7 +85,6 @@ same_nick: '&fHrác s tymto nickom uz hrá!' # TODO email_confirm: '&cPlease confirm your email address!' # TODO email_changed: '&2Email address changed correctly!' # TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' # TODO email_already_used: '&4The email address is already being used' @@ -91,8 +92,20 @@ same_nick: '&fHrác s tymto nickom uz hrá!' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&cPridaj svoj e-mail príkazom "/email add email zopakujEmail"' recovery_email: '&cZabudol si heslo? Pouzi príkaz /email recovery ' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha -# TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command "/captcha "' +# TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command: /captcha ' # TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' # TODO valid_captcha: '&2Captcha code solved correctly!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index 480e0e5b2..2997ddd5b 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -1,36 +1,35 @@ -# Registration +# Kayit mesajlari reg_msg: '&3Lutfen kayit komutunu kullanin "/register "' usage_reg: '&cKullanim: /register ' reg_only: '&4Sunucuya kayit sadece internet uzerinden yapilmakta! Lutfen http://ornek.com sitesini kayit icin ziyaret edin!' -# TODO kicked_admin_registered: 'An admin just registered you; please log in again' +kicked_admin_registered: 'Bir yetkili seni kayit etti; tekrardan giris yap' registered: '&2Basariyla kaydoldun!' reg_disabled: '&cOyun icin kayit olma kapatildi!' user_regged: '&cSenin adinda daha once birisi kaydolmus!' -# Password errors on registration +# Kayit aninda sifre hatalari password_error: '&cSifre eslesmiyor, tekrar deneyin!' password_error_nick: '&cSifrenize adinizi koyamazsiniz, lutfen farkli bir sifre secin...' password_error_unsafe: '&cSectiginiz sifre guvenli degil, lutfen farkli bir sifre secin...' -# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +password_error_chars: '&4Sifrenizde izin verilmeyen karakterler bulunmakta. Izin verilen karakterler: REG_EX' pass_len: '&cSenin sifren ya cok kisa yada cok uzun! Lutfen farkli birsey dene!' -# Login +# Oturuma giris usage_log: '&cKullanim: /login ' wrong_pwd: '&cYanlis sifre!' login: '&2Giris basarili!' login_msg: '&cLutfen giris komutunu kullanin "/login "' timeout: '&4Giris izni icin verilen zaman suresini astigin icin sunucudan atildin, tekrar deneyin!' -# Errors +# Hata mesajlari unknown_user: '&cBu oyuncu kayitli degil!' -# TODO denied_command: '&cIn order to use this command you must be authenticated!' -# TODO denied_chat: '&cIn order to chat you must be authenticated!' +denied_command: '&cSuanda bu komutu kullanamazsin!' +denied_chat: '&cSuanda sohbeti kullanamazsin!' not_logged_in: '&cGiris yapmadin!' -# TODO tempban_max_logins: '&cYou have been temporarily banned for failing to log in too many times.' +tempban_max_logins: '&cBir cok kez yanlis giris yaptiginiz icin gecici olarak banlandiniz.' max_reg: '&cSen maksimum kayit sinirini astin (%reg_count/%max_acc %reg_names)!' no_perm: '&4Bunu yapmak icin iznin yok!' error: '&4Beklenmedik bir hata olustu, yetkili ile iletisime gecin!' -unsafe_spawn: '&cOyundan ciktigin yer guvenli degil, baslangic noktasina isinlaniyorsun.' kick_forvip: '&3Bir VIP oyuna giris yaptigi icin atildin!' # AntiBot @@ -38,13 +37,17 @@ kick_antibot: 'AntiBot koruma modu aktif! Birkac dakika sonra tekrar girmeyi den antibot_auto_enabled: '&4[AntiBotServis] Saldiri oldugu icin AntiBot aktif edildi!' antibot_auto_disabled: '&2[AntiBotServis] AntiBot, %m dakika sonra deaktif edilecek!' -# Other messages +# Baska mesajlar unregistered: '&cKayit basariyla kaldirildi!' -# TODO accounts_owned_self: 'You own %count accounts:' -# TODO accounts_owned_other: 'The player %name has %count accounts:' +accounts_owned_self: 'Sen %count hesaba sahipsin:' +accounts_owned_other: 'Oyuncu %name %count hesaba sahip:' two_factor_create: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' -# TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +recovery_code_sent: 'Sifre sifirlama kodu eposta adresinize gonderildi.' +# TODO: Missing tags %count +recovery_code_incorrect: 'Kod dogru degil! Kullanim "/email recovery [eposta]" ile yeni bir kod olustur' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cHeabiniz henuz aktif edilmemis, e-postanizi kontrol edin!' usage_unreg: '&cKullanim: /unregister ' pwd_changed: '&2Sifre basariyla degistirildi!' @@ -53,11 +56,11 @@ logout: '&2Basariyla cikis yaptin!' reload: '&2Ayarlar ve veritabani yenilendi!' usage_changepassword: '&cKullanim: /changepassword ' -# Session messages +# Otomatik giris invalid_session: '&cIP adresin degistirildi ve oturum suren doldu!' -valid_session: '&2Oturum icin yeniden giris gerekiyor.' +valid_session: '&2Oturuma girisiniz otomatikmen yapilmistir.' -# Error messages when joining +# Servore giris aninda hata mesajlari name_len: '&4Senin ismin ya cok kisa yada cok uzun!' regex: '&4Senin isminde uygunsuz karakterler bulunmakta. Izin verilen karakterler: REG_EX' country_banned: '&4Senin bolgen sunucudan yasaklandi!' @@ -65,7 +68,7 @@ not_owner_error: 'Bu hesabin sahibi degilsin. Lutfen farkli bir isim sec!' kick_fullserver: '&4Sunucu suanda dolu, daha sonra tekrar deneyin!' same_nick: '&4Senin isminde bir oyuncu suncuda bulunmakta!' invalid_name_case: 'Oyuna %valid isminde katilmalisin. %invalid ismini kullanarak katilamazsin.' -# TODO same_ip_online: 'A player with the same IP is already in game!' +same_ip_online: 'Oyunda sizin ipnizden giren biri bulunmakta!' # Email usage_email_add: '&cKullanim: /email add ' @@ -78,16 +81,27 @@ email_added: '&2Eposta basariyla kullaniciniza eklendi!' email_confirm: '&cLutfen tekrar epostanizi giriniz!' email_changed: '&2Epostaniz basariyla degistirildi!' email_send: '&2Sifreniz epostaniza gonderildi! Lutfen eposta kutunuzu kontrol edin!' -email_exists: '&cSifreniz zaten epostanize gonderildi! Bunu iptal etmek veya yeni bir sifre gondermek icin assagidaki komutu kullanabilirsin:' -# TODO email_show: '&2Your current email address is: &f%email' -# TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' +email_show: '&2Suanki eposta adresin: &f%email' +incomplete_email_settings: 'Hata: Gonderilen epostada bazi ayarlar tamamlanmis degil. Yetkili ile iletisime gec.' email_already_used: '&4Eposta adresi zaten kullaniliyor.' -# TODO email_send_failure: 'The email could not be sent. Please contact an administrator.' -# TODO show_no_email: '&2You currently don''t have email address associated with this account.' +email_send_failure: 'Eposta gonderilemedi. Yetkili ile iletisime gec.' +show_no_email: '&2Bu hesapla iliskili bir eposta bulunmuyor.' add_email: '&3Lutfen hesabinize eposta adresinizi komut ile ekleyin "/email add "' recovery_email: '&3Sifreni mi unuttun ? Komut kullanarak ogrenebilirsin "/email recovery "' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&cKisa bir sure once eposta gonderildi. Yeni bir eposta almak icin %time beklemelisin.' # Captcha usage_captcha: '&3Giris yapmak icin guvenlik kodunu komut yazarak girin "/captcha "' wrong_captcha: '&cYanlis guvenlik kodu, kullanim sekli "/captcha THE_CAPTCHA" sohbete yazin!' valid_captcha: '&2Guvenlik kodu dogrulandi!' + +# Zaman birimleri +second: 'saniye' +seconds: 'saniye' +minute: 'dakika' +minutes: 'dakika' +hour: 'saat' +hours: 'saat' +day: 'gun' +days: 'gun' diff --git a/src/main/resources/messages/messages_uk.yml b/src/main/resources/messages/messages_uk.yml index d1d53187f..fdd80bb47 100644 --- a/src/main/resources/messages/messages_uk.yml +++ b/src/main/resources/messages/messages_uk.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cВаш IP тимчасово заблоковано, із max_reg: '&cВичерпано ліміт реєстрацій (%reg_count/%max_acc %reg_names) для вашого підключення!' no_perm: '&4У вас недостатньо прав, щоб застосувати цю команду!' error: '&4[AuthMe] Error. Будь ласка, повідомте адміністратора!' -unsafe_spawn: '&cВаше останнє місцезнаходження не є безпечним. Вас було переміщено на точку спавна.' kick_forvip: '&3Вас кікнуто, внаслідок того, що VIP гравець зайшов на сервер коли небуло вільних місць.' # AntiBot @@ -44,7 +43,10 @@ accounts_owned_self: 'Кількість ваших твінк‒акаунті accounts_owned_other: 'Кількість твінк‒акаунтів гравця %name: %count' two_factor_create: '&2Ваш секретний код — %code %nl%&2Можете зкопіювати його за цим посиланням — %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cВаш акаунт ще не активовано. Будь ласка, провірте свою електронну пошту!' usage_unreg: '&cСинтаксис: /unregister <пароль>' pwd_changed: '&2Пароль успішно змінено!' @@ -78,7 +80,6 @@ email_added: '&2Електронну пошту успішно прив’яза email_confirm: '&cАдреси не співпадають.' email_changed: '&2E-mail успішно змінено.' email_send: '&2Лист для відновлення доступу надіслано. Будь ласка, провірте свою пошту!' -email_exists: '&cЛист для відновлення доступу вже було надіслано! Ви можете деактуалізувати його, використавши наступну команду:' # TODO email_show: '&2Your current email address is: &f%email' incomplete_email_settings: '&4[AuthMe] Error: Не всі необхідні налаштування є встановленими, щоб надсилати електронну пошту. Будь ласка, повідомте адміністратора!' email_already_used: '&4До цієї електронної пошти прив’язано забагато акаунтів!' @@ -86,8 +87,20 @@ email_already_used: '&4До цієї електронної пошти прив # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&3Не забудьте прив’язати електронну пошту до свого акаунта, за допомогою команди "/email add "' recovery_email: 'Забули пароль? Можете скористатись командою &9/email recovery &f<&9ваш e-mail&f>' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3Для продовження доведеться ввести капчу — "/captcha "' wrong_captcha: '&cНевірно введена капча! Спробуйте ще раз — "/captcha THE_CAPTCHA"' valid_captcha: '&2Капчу прийнято.' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index 4cb86ae86..9c08e4b8b 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&cBạn đã bị tạm thời khóa truy cập do đăng n max_reg: '&cBạn đã vượt quá giới hạn tối đa đăng ký tài khoản (%reg_count/%max_acc %reg_names) cho những lần kết nối tài khoản!' no_perm: '&4Bạn không có quyền truy cập lệnh này!' error: '&4Lỗi! Vui lòng liên hệ quản trị viên hoặc admin' -unsafe_spawn: '&cBạn đang ở vị trí không an toàn, bạn đã được dịch chuyển về Spawn.' kick_forvip: '&eChỉ có thành viên VIP mới được tham gia khi máy chủ đầy!' # AntiBot @@ -44,7 +43,11 @@ accounts_owned_self: 'Bạn sở hữu %count tài khoản:' accounts_owned_other: 'Người chơi %name có %count tài khoản:' two_factor_create: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' recovery_code_sent: 'Một mã khôi phục mật khẩu đã được gửi đến địa chỉ email của bạn.' +# TODO: Missing tags %count recovery_code_incorrect: 'Mã khôi phục không đúng! Dùng lệnh /email recovery [email] để tạo một mã mới' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&cTài khoản của bạn chưa được kích hoạt, vui lòng kiểm tra email!' usage_unreg: '&cSử dụng: /unregister ' pwd_changed: '&2Thay đổi mật khẩu thành công!' @@ -78,7 +81,6 @@ email_added: '&2Địa chỉ email đã thêm vào tài khoản của bạn thà email_confirm: '&cVui lòng xác nhận địa chỉ email của bạn!' email_changed: '&2Địa chỉ email đã thay đổi!' email_send: '&2Email phục hồi đã được gửi thành công! Vui lòng kiểm tra hộp thư đến trong email của bạn.' -email_exists: '&cEmail phục hồi đã được gửi, bạn có thể hủy bỏ và gửi thư mới bằng cách sử dụng lệnh sau:' email_show: '&2Địa chỉ email hiện tại của bạn là: &f%email' incomplete_email_settings: 'Lỗi: các thiết lập để gửi thư không được cài đặt đầy đủ. Vui lòng liên hệ với quản trị viên để thông báo lỗi.' email_already_used: '&4Địa chỉ email đã được sử dụng' @@ -86,8 +88,20 @@ email_send_failure: 'Không thể gửi thư. Vui lòng liên hệ với ban qu show_no_email: '&2Hiện tại bạn chưa liên kết bất kỳ email nào với tài khoản này.' add_email: '&eVui lòng thêm email của bạn với lệnh "/email add "' recovery_email: '&aBạn quên mật khẩu? Vui lòng gõ lệnh "/email recovery "' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&eĐể đăng nhập vui lòng hãy gõ mã Captcha, gõ lệnh "/captcha "' wrong_captcha: '&cSai mã captcha, Vui lòng nhấn "/captcha THE_CAPTCHA" trong kênh chát!' valid_captcha: '&2Mã captcha đã được xác nhận!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_zhcn.yml b/src/main/resources/messages/messages_zhcn.yml index f52f9b41b..dcc315226 100644 --- a/src/main/resources/messages/messages_zhcn.yml +++ b/src/main/resources/messages/messages_zhcn.yml @@ -2,7 +2,7 @@ reg_msg: '&8[&6玩家系统&8] &c请输入“/register <密码> <再输入一次以确定密码>”以注册' usage_reg: '&8[&6玩家系统&8] &c正确用法:“/register <密码> <再输入一次以确定密码>”' reg_only: '&8[&6玩家系统&8] &f只允许注册过的玩家进服!请到 https://example.cn 注册' -# TODO kicked_admin_registered: 'An admin just registered you; please log in again' +kicked_admin_registered: '有一位管理员刚刚为您完成了注册,请重新登录' registered: '&8[&6玩家系统&8] &c已成功注册!' reg_disabled: '&8[&6玩家系统&8] &c目前服务器暂时禁止注册,请到服务器论坛以得到更多资讯' user_regged: '&8[&6玩家系统&8] &c此用户已经在此服务器注册过' @@ -11,7 +11,7 @@ user_regged: '&8[&6玩家系统&8] &c此用户已经在此服务器注册过' password_error: '&8[&6玩家系统&8] &f密码不相同' password_error_nick: '&8[&6玩家系统&8] &f你不能使用你的名字作为密码。 ' password_error_unsafe: '&8[&6玩家系统&8] &f你不能使用安全性过低的码。 ' -# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +password_error_chars: '&4您的密码包含了非法字符。可使用的字符: REG_EX' pass_len: '&8[&6玩家系统&8] &你的密码没有达到要求!' # Login @@ -23,15 +23,14 @@ timeout: '&8[&6玩家系统&8] &f给你登录的时间已经过了' # Errors unknown_user: '&8[&6玩家系统&8] &c此用户名还未注册过' -# TODO denied_command: '&cIn order to use this command you must be authenticated!' -# TODO denied_chat: '&cIn order to chat you must be authenticated!' +denied_command: '&c您需要先通过验证才能使用该命令!' +denied_chat: '&c您需要先通过验证才能聊天!' not_logged_in: '&8[&6玩家系统&8] &c你还未登录!' -# TODO tempban_max_logins: '&cYou have been temporarily banned for failing to log in too many times.' +tempban_max_logins: '&c由于您登录失败次数过多,已被暂时禁止登录。' # TODO: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&8[&6玩家系统&8] &f你不允许再为你的IP在服务器注册更多用户了!' no_perm: '&8[&6玩家系统&8] &c没有权限' error: '&8[&6玩家系统&8] &f发现错误,请联系管理员' -unsafe_spawn: '&8[&6玩家系统&8] &f你退出服务器时的位置不安全,正在传送你到此世界的出生点' kick_forvip: '&8[&6玩家系统&8] &cA VIP玩家加入了已满的服务器!' # AntiBot @@ -41,11 +40,15 @@ antibot_auto_disabled: '&8[&6玩家系统&8] &f防机器人程序由于异常连 # Other messages unregistered: '&8[&6玩家系统&8] &c成功删除此用户!' -# TODO accounts_owned_self: 'You own %count accounts:' -# TODO accounts_owned_other: 'The player %name has %count accounts:' +accounts_owned_self: '您拥有 %count 个账户:' +accounts_owned_other: '玩家 %name 拥有 %count 个账户:' two_factor_create: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' -# TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +recovery_code_sent: '一个用于重置您的密码的验证码已发到您的邮箱' +# TODO: Missing tags %count +recovery_code_incorrect: '验证码不正确! 使用 /email recovery [邮箱] 以生成新的验证码' +recovery_tries_exceeded: '您已经达到输入验证码次数的最大允许次数。请使用 "/email recovery [邮箱]" 来生成一个新的' +recovery_code_correct: '验证码正确!' +recovery_change_password: '请使用 /email setpassword <新密码> 立即设置新的密码' vb_nonActiv: '&8[&6玩家系统&8] &f你的帐号还未激活,请查看你的邮箱!' usage_unreg: '&8[&6玩家系统&8] &c正确用法:“/unregister <密码>”' pwd_changed: '&8[&6玩家系统&8] &c密码已成功修改!' @@ -66,7 +69,7 @@ not_owner_error: '&8[&6玩家系统&8] &4警告! &c你并不是此帐户持有 kick_fullserver: '&8[&6玩家系统&8] &c抱歉,服务器已满!' same_nick: '&8[&6玩家系统&8] &f同样的用户名现在在线且已经登录了!' invalid_name_case: '&8[&6玩家系统&8] &c你应该使用「%valid」而并非「%invalid」登入游戏。 ' -# TODO same_ip_online: 'A player with the same IP is already in game!' +same_ip_online: '已有一个同IP玩家在游戏中了!' # Email usage_email_add: '&8[&6玩家系统&8] &f用法: /email add <邮箱> <确认电子邮件> ' @@ -79,16 +82,27 @@ email_added: '&8[&6玩家系统&8] &f邮箱已添加 !' email_confirm: '&8[&6玩家系统&8] &f确认你的邮箱 !' email_changed: '&8[&6玩家系统&8] &f邮箱已改变 !' email_send: '&8[&6玩家系统&8] &f恢复邮件已发送 !' -email_exists: '&8[&6玩家系统&8] &c恢复邮件已发送 ! 你可以丢弃它然後使用以下的指令来发送新的邮件:' -# TODO email_show: '&2Your current email address is: &f%email' -# TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' +email_show: '&2您当前的电子邮件地址为: &f%email' +incomplete_email_settings: '错误:并非所有发送邮件需要的设置都已被设置,请联系管理员' email_already_used: '&8[&6玩家系统&8] &4邮箱已被使用' -# TODO email_send_failure: 'The email could not be sent. Please contact an administrator.' -# TODO show_no_email: '&2You currently don''t have email address associated with this account.' +email_send_failure: '邮件发送失败,请联系管理员' +show_no_email: '&2您当前并没有任何邮箱与该账号绑定' add_email: '&8[&6玩家系统&8] &c请输入“/email add <你的邮箱> <再输入一次以确认>”以把你的邮箱添加到此帐号' recovery_email: '&8[&6玩家系统&8] &c忘了你的密码?请输入:“/email recovery <你的邮箱>”' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +email_cooldown_error: '&c邮件已在几分钟前发送,您需要等待 %time 后才能再次请求发送' # Captcha usage_captcha: '&8[&6玩家系统&8] &c正确用法:/captcha ' wrong_captcha: '&8[&6玩家系统&8] &c错误的验证码,请输入:“/captcha THE_CAPTCHA”' valid_captcha: '&8[&6玩家系统&8] &c你的验证码是有效的!' + +# Time units +second: '秒' +seconds: '秒' +minute: '分钟' +minutes: '分钟' +hour: '小时' +hours: '小时' +day: '天' +days: '天' diff --git a/src/main/resources/messages/messages_zhhk.yml b/src/main/resources/messages/messages_zhhk.yml index a07d52490..ee0eadb01 100644 --- a/src/main/resources/messages/messages_zhhk.yml +++ b/src/main/resources/messages/messages_zhhk.yml @@ -35,7 +35,6 @@ not_logged_in: '&8[&6用戶系統&8] &c你還沒有登入 !' max_reg: '&8[&6用戶系統&8] &f你的IP地址已達到註冊數上限。' no_perm: '&8[&6用戶系統&8] &b嗯~你想幹甚麼?' error: '&8[&6用戶系統&8] &f發生錯誤,請與管理員聯絡。' -unsafe_spawn: '&8[&6用戶系統&8] &f你的登出位置不安全,現在將傳送你到重生點。' kick_forvip: '&c喔!因為有VIP玩家登入了伺服器。' # AntiBot @@ -49,7 +48,10 @@ unregistered: '&8[&6用戶系統&8] &c你已成功刪除會員註冊記錄。' # TODO accounts_owned_other: 'The player %name has %count accounts:' two_factor_create: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&8[&6用戶系統&8] &f你的帳戶還沒有經過電郵驗證 !' usage_unreg: '&8[&6用戶系統&8] &f用法: 《 /unregister <密碼> 》' pwd_changed: '&8[&6用戶系統&8] &c你成功更換了你的密碼 !' @@ -83,7 +85,6 @@ email_added: '&8[&6用戶系統&8] &a已新增你的電郵地址。' email_confirm: '&8[&6用戶系統&8] &5請重覆輸入你的電郵地址。' email_changed: '&8[&6用戶系統&8] &a你的電郵地址已更改。' email_send: '&8[&6用戶系統&8] &a忘記密碼信件已寄出,請查收。' -email_exists: '&8[&6用戶系統&8] &c訊息已發送!如果你收不到該封電郵,可以使用以下指令進行重寄:' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' email_already_used: '&8[&6用戶系統&8] &4這個電郵地址已被使用。' @@ -91,8 +92,20 @@ email_already_used: '&8[&6用戶系統&8] &4這個電郵地址已被使用。' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&8[&6用戶系統&8] &b請為你的帳戶立即添加電郵地址: 《 /email add <電郵地址> <重覆電郵地址> 》' recovery_email: '&8[&6用戶系統&8] &b忘記密碼?請使用 /email recovery <電郵地址> 來更新密碼。' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&8[&6用戶系統&8] &f用法: 《 /captcha 》' wrong_captcha: '&8[&6用戶系統&8] &c你所輸入的驗證碼無效,請使用 《 /captcha THE_CAPTCHA 》 再次輸入。' valid_captcha: '&8[&6用戶系統&8] &c你所輸入的驗證碼無效 !' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_zhmc.yml b/src/main/resources/messages/messages_zhmc.yml index 1d9876ad8..c4d8897eb 100644 --- a/src/main/resources/messages/messages_zhmc.yml +++ b/src/main/resources/messages/messages_zhmc.yml @@ -30,7 +30,6 @@ tempban_max_logins: '&c由於登錄失敗次數過多,您已被暫時禁止。 max_reg: '&c您已超過註冊的最大數量(%reg_count/%max_acc %reg_names)!' no_perm: '&4您沒有執行此操作的權限!' error: '&4發生錯誤!請聯繫伺服器管理員!' -unsafe_spawn: '&c你的登出地點並不安全,你已經被傳送到世界的重生點。' kick_forvip: '&3一名VIP玩家在服務器已滿時已加入伺服器!' # AntiBot @@ -44,7 +43,11 @@ accounts_owned_self: '您擁有 %count 個帳戶:' accounts_owned_other: '玩家 %name 擁有 %count 個帳戶:' two_factor_create: '&2您的密碼是 %code。您可以從這裡掃描 %url' recovery_code_sent: '已將重設密碼的恢復代碼發送到您的電子郵件。' +# TODO: Missing tags %count recovery_code_incorrect: '恢復代碼錯誤!使用指令: "/email recovery [電郵地址]" 生成新的一個恢復代碼。' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&c你的帳戶未激活,請確認電郵!' usage_unreg: '&c使用方法: "/unregister <你的密碼>"' pwd_changed: '&2密碼已更變!' @@ -78,7 +81,6 @@ email_added: '&2電子郵件地址已成功添加到您的帳戶!' email_confirm: '&c請確認你的電郵地址!' email_changed: '&2已正確地更改電子郵件地址!' email_send: '&2帳戶恢復電子郵件已成功發送! 請檢查您的電子郵件收件箱!' -email_exists: '&c備援電子郵件已傳送! 您可以使用以下命令丟棄它並發送一個新的備援電子郵件:' # TODO email_show: '&2Your current email address is: &f%email' incomplete_email_settings: '缺少必要的配置來為發送電子郵件。請聯繫管理員。' email_already_used: '&4此電子郵件地址已被使用' @@ -86,8 +88,20 @@ email_already_used: '&4此電子郵件地址已被使用' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&3請使用命令: /email add [你的電郵地址] [重覆確認你的電郵地址] 將您的電子郵件添加到您的帳戶"' recovery_email: '&3忘記密碼了嗎? 請使用命令: "/email recovery [你的電郵地址]"' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&3T要登錄您必須使用captcha驗證碼,請使用命令: "/captcha "' wrong_captcha: '&c驗證碼錯誤!請按T在聊天中輸入 "/captcha THE_CAPTCHA"' valid_captcha: '&2驗證碼正確!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/messages/messages_zhtw.yml b/src/main/resources/messages/messages_zhtw.yml index b5dc97cf2..747357678 100644 --- a/src/main/resources/messages/messages_zhtw.yml +++ b/src/main/resources/messages/messages_zhtw.yml @@ -35,7 +35,6 @@ not_logged_in: '&b【AuthMe】&6你還沒有登入!' max_reg: '&b【AuthMe】&6你的 IP 位置所註冊的帳號數量已經達到最大。' no_perm: '&b【AuthMe】&6你沒有使用該指令的權限。' error: '&b【AuthMe】&6發生錯誤,請聯繫管理員' -unsafe_spawn: '&b【AuthMe】&6你登出的地點不安全,已傳送你到安全的地點。' kick_forvip: '&b【AuthMe】&6你已經被請出。&c原因 : 有 VIP 玩家登入伺服器' # AntiBot @@ -49,7 +48,10 @@ unregistered: '&b【AuthMe】&6你已經成功取消註冊。' # TODO accounts_owned_other: 'The player %name has %count accounts:' two_factor_create: '&b【AuthMe - 兩步驗證碼】&b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' # TODO recovery_code_sent: 'A recovery code to reset your password has been sent to your email.' -# TODO recovery_code_incorrect: 'The recovery code is not correct! Use /email recovery [email] to generate a new one' +# TODO recovery_code_incorrect: 'The recovery code is not correct! You have %count tries remaining.' +# TODO recovery_tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' +# TODO recovery_code_correct: 'Recovery code entered correctly!' +# TODO recovery_change_password: 'Please use the command /email setpassword to change your password immediately.' vb_nonActiv: '&b【AuthMe】&6你的帳號還沒有經過驗證! 檢查看看你的電子信箱 (Email) 吧!' usage_unreg: '&b【AuthMe】&6用法: &c"/unregister <密碼>"' pwd_changed: '&b【AuthMe】&6密碼變更成功!' @@ -83,7 +85,6 @@ email_added: '&b【AuthMe】&6已添加Email!' email_confirm: '&b【AuthMe】&6請驗證你的Email!' email_changed: '&b【AuthMe】&6Email已變更!' email_send: '&b【AuthMe】&6已經送出重設密碼要求至你的Email , 請查收。' -email_exists: '&b【AuthMe】&6這個帳戶已經有設定電子郵件了' # TODO email_show: '&2Your current email address is: &f%email' # TODO incomplete_email_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' email_already_used: '&b【AuthMe】&4這個電郵地址已被使用。' @@ -91,8 +92,20 @@ email_already_used: '&b【AuthMe】&4這個電郵地址已被使用。' # TODO show_no_email: '&2You currently don''t have email address associated with this account.' add_email: '&b【AuthMe】&6請使用 &c"/email add <你的Email> <再次輸入你的Email>" &6來添加 Email' recovery_email: '&b【AuthMe】&6忘記密碼了嗎? 使用 &c"/email recovery <你的Email>"' +# TODO change_password_expired: 'You cannot change your password using this command anymore.' +# TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' # Captcha usage_captcha: '&b【AuthMe】&6請用 &c"/captcha " &6來輸入你的驗證碼' wrong_captcha: '&b【AuthMe】&6錯誤的驗證碼,請使用 《 /captcha THE_CAPTCHA 》 再試一次吧。' valid_captcha: '&b【AuthMe】&6驗證碼無效!' + +# Time units +# TODO second: 'second' +# TODO seconds: 'seconds' +# TODO minute: 'minute' +# TODO minutes: 'minutes' +# TODO hour: 'hour' +# TODO hours: 'hours' +# TODO day: 'day' +# TODO days: 'days' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 9ca68c314..f04829d41 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -17,7 +17,7 @@ softdepend: commands: authme: description: AuthMe op commands - usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|spawn|setspawn|firstspawn|setfirstspawn|purge|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|spawn|setspawn|firstspawn|setfirstspawn|purge|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|debug login: description: Login command usage: /login @@ -153,6 +153,9 @@ permissions: authme.bypasspurge: description: Permission to bypass the purging process. default: false + authme.debug: + description: Permission to use the /authme debug command. + default: op authme.player.*: description: Gives access to all player commands children: @@ -207,5 +210,5 @@ permissions: description: Command permission to unregister. default: true authme.vip: - description: Permission node to identify VIP users. + description: When the server is full and someone with this permission joins the server, someone will be kicked. default: op diff --git a/src/main/resources/recovery_code_email.html b/src/main/resources/recovery_code_email.html index e5614f4f4..fc7327a1e 100644 --- a/src/main/resources/recovery_code_email.html +++ b/src/main/resources/recovery_code_email.html @@ -2,7 +2,7 @@

    You have requested to reset your password on . To reset it, - please use the recovery code : /email recover [email] . + please use the recovery code : /email code .

    The code expires in hours. diff --git a/src/test/java/fr/xephi/authme/AuthMeInitializationTest.java b/src/test/java/fr/xephi/authme/AuthMeInitializationTest.java index aae925b51..f11d7311a 100644 --- a/src/test/java/fr/xephi/authme/AuthMeInitializationTest.java +++ b/src/test/java/fr/xephi/authme/AuthMeInitializationTest.java @@ -8,6 +8,8 @@ import fr.xephi.authme.api.NewAPI; import fr.xephi.authme.command.CommandHandler; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.factory.FactoryDependencyHandler; +import fr.xephi.authme.initialization.factory.SingletonStoreDependencyHandler; import fr.xephi.authme.listener.BlockListener; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; @@ -91,7 +93,10 @@ public class AuthMeInitializationTest { Settings settings = new Settings(dataFolder, mock(PropertyResource.class), null, buildConfigurationData()); - Injector injector = new InjectorBuilder().addDefaultHandlers("fr.xephi.authme").create(); + Injector injector = new InjectorBuilder() + .addHandlers(new FactoryDependencyHandler(), new SingletonStoreDependencyHandler()) + .addDefaultHandlers("fr.xephi.authme") + .create(); injector.provide(DataFolder.class, dataFolder); injector.register(Server.class, server); injector.register(PluginManager.class, pluginManager); diff --git a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java new file mode 100644 index 000000000..ac0504888 --- /dev/null +++ b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java @@ -0,0 +1,182 @@ +package fr.xephi.authme; + +import ch.jalu.configme.properties.Property; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.listener.PlayerListener; +import fr.xephi.authme.process.register.executors.RegistrationMethod; +import fr.xephi.authme.security.crypts.Whirlpool; +import fr.xephi.authme.util.expiring.ExpiringMap; +import fr.xephi.authme.util.expiring.ExpiringSet; +import fr.xephi.authme.util.expiring.TimedCounter; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * Contains consistency tests across all AuthMe classes. + */ +public class ClassesConsistencyTest { + + /** Contains all production classes. */ + private static final List> ALL_CLASSES = + new ClassCollector(TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT).collectClasses(); + + /** Expiring structure types. */ + private static final Set> EXPIRING_STRUCTURES = ImmutableSet.of( + ExpiringSet.class, ExpiringMap.class, TimedCounter.class); + + /** Immutable types, which are allowed to be used in non-private constants. */ + private static final Set> IMMUTABLE_TYPES = ImmutableSet.of( + /* JDK */ + int.class, long.class, float.class, String.class, File.class, Enum.class, collectionsUnmodifiableList(), + /* AuthMe */ + Property.class, RegistrationMethod.class, + /* Guava */ + ImmutableMap.class, ImmutableList.class); + + /** Classes excluded from the field visibility test. */ + private static final Set> CLASSES_EXCLUDED_FROM_VISIBILITY_TEST = ImmutableSet.of( + Whirlpool.class, // not our implementation, so we don't touch it + Columns.class // uses non-final String constants, which is safe + ); + + /** + * Checks that there aren't two classes with the same name; this is confusing and should be avoided. + */ + @Test + public void shouldNotHaveSameName() { + // given + Set names = new HashSet<>(); + + // when / then + for (Class clazz : ALL_CLASSES) { + if (!names.add(clazz.getSimpleName())) { + fail("Class with name '" + clazz.getSimpleName() + "' already encountered!"); + } + } + } + + /** + * Checks that fields of classes are either private or static final fields of an immutable type. + */ + @Test + public void shouldHaveNonPrivateConstantsOnly() { + // given / when + Set invalidFields = ALL_CLASSES.stream() + .filter(clz -> !CLASSES_EXCLUDED_FROM_VISIBILITY_TEST.contains(clz)) + .map(Class::getDeclaredFields) + .flatMap(Arrays::stream) + .filter(f -> !f.getName().contains("$")) + .filter(f -> hasIllegalFieldVisibility(f)) + .map(f -> formatField(f)) + .collect(Collectors.toSet()); + + // then + if (!invalidFields.isEmpty()) { + fail("Found " + invalidFields.size() + " fields with non-private, mutable fields:\n- " + + String.join("\n- ", invalidFields)); + } + } + + private static boolean hasIllegalFieldVisibility(Field field) { + final int modifiers = field.getModifiers(); + if (Modifier.isPrivate(modifiers)) { + return false; + } else if (!Modifier.isStatic(modifiers) || !Modifier.isFinal(modifiers)) { + return true; + } + + // Field is non-private, static and final + Class valueType; + if (Collection.class.isAssignableFrom(field.getType()) || Map.class.isAssignableFrom(field.getType())) { + // For collections/maps, need to check the actual type to ensure it's an unmodifiable implementation + Object value = ReflectionTestUtils.getFieldValue(field, null); + valueType = value.getClass(); + } else { + valueType = field.getType(); + } + + // Field is static, final, and not private -> check that it is immutable type + return IMMUTABLE_TYPES.stream() + .noneMatch(immutableType -> immutableType.isAssignableFrom(valueType)); + } + + /** + * Prints out the field with (most of) its modifiers. + * + * @param field the field to format + * @return description of the field + */ + private static String formatField(Field field) { + String modifiersText = ""; + int modifiers = field.getModifiers(); + if (Modifier.isPublic(modifiers)) { + modifiersText += "public "; + } else if (Modifier.isProtected(modifiers)) { + modifiersText += "protected "; + } else if (Modifier.isPrivate(modifiers)) { + modifiersText += "private "; + } + + if (Modifier.isStatic(modifiers)) { + modifiersText += "static "; + } + if (Modifier.isFinal(modifiers)) { + modifiersText += "final "; + } + + return String.format("[%s] %s %s %s", field.getDeclaringClass().getSimpleName(), modifiersText.trim(), + field.getType().getSimpleName(), field.getName()); + } + + /** + * Checks that classes with expiring collections (such as {@link ExpiringMap}) implement the {@link HasCleanup} + * interface to regularly clean up expired data. + */ + @Test + public void shouldImplementHasCleanup() { + // given / when / then + for (Class clazz : ALL_CLASSES) { + if (hasExpiringCollectionAsField(clazz)) { + assertThat("Class '" + clazz.getSimpleName() + "' has expiring collections, should implement HasCleanup", + HasCleanup.class.isAssignableFrom(clazz), equalTo(true)); + // System.out.println("Successful check for " + clazz); + } + } + } + + private static boolean hasExpiringCollectionAsField(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (EXPIRING_STRUCTURES.stream().anyMatch(t -> t.isAssignableFrom(field.getType()))) { + return true; + } + } + return false; + } + + /** + * @return the concrete class of the unmodifiable list as returned by {@link Collections#unmodifiableList(List)}. + */ + private static Class collectionsUnmodifiableList() { + return Collections.unmodifiableList(new ArrayList<>()).getClass(); + } +} diff --git a/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java b/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java new file mode 100644 index 000000000..928ad2a9d --- /dev/null +++ b/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java @@ -0,0 +1,51 @@ +package fr.xephi.authme; + +import fr.xephi.authme.util.Utils; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; + +import java.io.File; +import java.util.List; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; + +/** + * Consistency test for the CodeClimate configuration file. + */ +public class CodeClimateConfigTest { + + private static final String CONFIG_FILE = ".codeclimate.yml"; + + @Test + public void shouldHaveExistingClassesInExclusions() { + // given + FileConfiguration configuration = YamlConfiguration.loadConfiguration(new File(CONFIG_FILE)); + List excludePaths = configuration.getStringList("exclude_paths"); + + // when / then + assertThat(excludePaths, not(empty())); + for (String path : excludePaths) { + String className = convertPathToQualifiedClassName(path); + assertThat("No class corresponds to excluded path '" + path + "'", + Utils.isClassLoaded(className), equalTo(true)); + } + } + + private static String convertPathToQualifiedClassName(String path) { + // Note ljacqu 20170323: In the future, we could have legitimate exclusions that don't fulfill these checks, + // in which case this test needs to be adapted accordingly. + if (!path.startsWith(TestHelper.SOURCES_FOLDER)) { + throw new IllegalArgumentException("Unexpected path '" + path + "': expected to start with sources folder"); + } else if (!path.endsWith(".java")) { + throw new IllegalArgumentException("Expected path '" + path + "' to end with '.java'"); + } + + return path.substring(0, path.length() - ".java".length()) // strip ending .java + .substring(TestHelper.SOURCES_FOLDER.length()) // strip starting src/main/java + .replace('/', '.'); // replace '/' to '.' + } +} diff --git a/src/test/java/fr/xephi/authme/ConsoleLoggerTest.java b/src/test/java/fr/xephi/authme/ConsoleLoggerTest.java index 8eae45ec2..7d01a7db1 100644 --- a/src/test/java/fr/xephi/authme/ConsoleLoggerTest.java +++ b/src/test/java/fr/xephi/authme/ConsoleLoggerTest.java @@ -19,8 +19,10 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; +import java.util.logging.Level; import java.util.logging.Logger; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -42,9 +44,6 @@ public class ConsoleLoggerTest { @Mock private Logger logger; - @Mock - private Settings settings; - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -106,7 +105,7 @@ public class ConsoleLoggerTest { ConsoleLogger.warning("Encountered a warning"); // then - verify(logger).info("Debug: Created test"); + verify(logger).info("[DEBUG] Created test"); verify(logger).warning("Encountered a warning"); verifyNoMoreInteractions(logger); assertThat(logFile.length(), equalTo(0L)); @@ -137,6 +136,35 @@ public class ConsoleLoggerTest { assertThat(String.join("", loggedLines), containsString(getClass().getCanonicalName())); } + @Test + public void shouldSupportVariousDebugMethods() throws IOException { + // given + ConsoleLogger.setLoggingOptions(newSettings(true, LogLevel.DEBUG)); + + // when + ConsoleLogger.debug("Got {0} entries", "test"); + ConsoleLogger.debug("Player `{0}` is in world `{1}`", "Bobby", "world"); + ConsoleLogger.debug("The {0} is {1} the {2}", "fox", "behind", "chicken"); + ConsoleLogger.debug("{0} quick {1} jump over {2} lazy {3} (reason: {4})", 5, "foxes", 3, "dogs", null); + ConsoleLogger.debug(() -> "Too little too late"); + + // then + verify(logger).log(Level.INFO, "[DEBUG] Got {0} entries", "test"); + verify(logger).log(Level.INFO, "[DEBUG] Player `{0}` is in world `{1}`", new Object[]{"Bobby", "world"}); + verify(logger).log(Level.INFO, "[DEBUG] The {0} is {1} the {2}", new Object[]{"fox", "behind", "chicken"}); + verify(logger).log(Level.INFO, "[DEBUG] {0} quick {1} jump over {2} lazy {3} (reason: {4})", + new Object[]{"5", "foxes", "3", "dogs", "null"}); + verify(logger).info("[DEBUG] Too little too late"); + + List loggedLines = Files.readAllLines(logFile.toPath(), StandardCharsets.UTF_8); + assertThat(loggedLines, contains( + containsString("[DEBUG] Got {0} entries {test}"), + containsString("[DEBUG] Player `{0}` is in world `{1}` {Bobby, world}"), + containsString("[DEBUG] The {0} is {1} the {2} {fox, behind, chicken}"), + containsString("[DEBUG] {0} quick {1} jump over {2} lazy {3} (reason: {4}) {5, foxes, 3, dogs, null}"), + containsString("[DEBUG] Too little too late"))); + } + @Test public void shouldHaveHiddenConstructor() { TestHelper.validateHasOnlyPrivateEmptyConstructor(ConsoleLogger.class); diff --git a/src/test/java/fr/xephi/authme/ReflectionTestUtils.java b/src/test/java/fr/xephi/authme/ReflectionTestUtils.java index fdd716bd5..bdae313ac 100644 --- a/src/test/java/fr/xephi/authme/ReflectionTestUtils.java +++ b/src/test/java/fr/xephi/authme/ReflectionTestUtils.java @@ -25,7 +25,7 @@ public final class ReflectionTestUtils { */ public static void setField(Class clazz, T instance, String fieldName, Object value) { try { - Field field = getField(clazz, instance, fieldName); + Field field = getField(clazz, fieldName); field.set(instance, value); } catch (IllegalAccessException e) { throw new UnsupportedOperationException( @@ -34,24 +34,30 @@ public final class ReflectionTestUtils { } } - private static Field getField(Class clazz, T instance, String fieldName) { + private static Field getField(Class clazz, String fieldName) { try { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); return field; } catch (NoSuchFieldException e) { - throw new UnsupportedOperationException(format("Could not get field '%s' for instance '%s' of class '%s'", - fieldName, instance, clazz.getName()), e); + throw new UnsupportedOperationException(format("Could not get field '%s' from class '%s'", + fieldName, clazz.getName()), e); } } @SuppressWarnings("unchecked") public static V getFieldValue(Class clazz, T instance, String fieldName) { - Field field = getField(clazz, instance, fieldName); + Field field = getField(clazz, fieldName); + return getFieldValue(field, instance); + } + + @SuppressWarnings("unchecked") + public static V getFieldValue(Field field, Object instance) { + field.setAccessible(true); try { return (V) field.get(instance); } catch (IllegalAccessException e) { - throw new UnsupportedOperationException("Could not get value of field '" + fieldName + "'", e); + throw new UnsupportedOperationException("Could not get value of field '" + field.getName() + "'", e); } } @@ -75,10 +81,11 @@ public final class ReflectionTestUtils { } } - public static Object invokeMethod(Method method, Object instance, Object... parameters) { + @SuppressWarnings("unchecked") + public static V invokeMethod(Method method, Object instance, Object... parameters) { method.setAccessible(true); try { - return method.invoke(instance, parameters); + return (V) method.invoke(instance, parameters); } catch (InvocationTargetException | IllegalAccessException e) { throw new UnsupportedOperationException("Could not invoke method '" + method + "'", e); } diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index 1166cef23..e343199fe 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -6,6 +6,7 @@ import fr.xephi.authme.command.TestCommandsUtil.TestLoginCommand; import fr.xephi.authme.command.TestCommandsUtil.TestRegisterCommand; import fr.xephi.authme.command.TestCommandsUtil.TestUnregisterCommand; 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; @@ -56,7 +57,7 @@ public class CommandHandlerTest { private CommandHandler handler; @Mock - private Injector injector; + private Factory commandFactory; @Mock private CommandMapper commandMapper; @Mock @@ -75,7 +76,7 @@ public class CommandHandlerTest { ExecutableCommand.class, TestLoginCommand.class, TestRegisterCommand.class, TestUnregisterCommand.class)); setInjectorToMockExecutableCommandClasses(); - handler = new CommandHandler(injector, commandMapper, permissionsManager, messages, helpProvider); + handler = new CommandHandler(commandFactory, commandMapper, permissionsManager, messages, helpProvider); } /** @@ -86,7 +87,7 @@ public class CommandHandlerTest { */ @SuppressWarnings("unchecked") private void setInjectorToMockExecutableCommandClasses() { - given(injector.newInstance(any(Class.class))).willAnswer(new Answer() { + given(commandFactory.newInstance(any(Class.class))).willAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Class clazz = invocation.getArgument(0); diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index a4e51e28c..23bb24587 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -1,7 +1,5 @@ package fr.xephi.authme.command; -import fr.xephi.authme.permission.AdminPermission; -import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.util.StringUtils; import org.junit.BeforeClass; import org.junit.Test; @@ -16,7 +14,6 @@ import java.util.Set; import java.util.function.BiConsumer; import java.util.regex.Pattern; -import static fr.xephi.authme.permission.DefaultPermission.OP_ONLY; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; @@ -199,32 +196,6 @@ public class CommandInitializerTest { walkThroughCommands(commands, noArgumentForParentChecker); } - /** - * Test that commands defined with the OP_ONLY default permission have at least one admin permission node. - */ - @Test - public void shouldNotHavePlayerPermissionIfDefaultsToOpOnly() { - // given - BiConsumer adminPermissionChecker = new BiConsumer() { - @Override - public void accept(CommandDescription command, Integer depth) { - PermissionNode permission = command.getPermission(); - if (permission != null && OP_ONLY.equals(permission.getDefaultPermission()) - && !hasAdminNode(permission)) { - fail("The command with labels " + command.getLabels() + " has OP_ONLY default " - + "permission but no permission node on admin level"); - } - } - - private boolean hasAdminNode(PermissionNode permission) { - return permission instanceof AdminPermission; - } - }; - - // when/then - walkThroughCommands(commands, adminPermissionChecker); - } - /** * Tests that multiple CommandDescription instances pointing to the same ExecutableCommand use the same * count of arguments. diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index dd7bad6e4..3a23cdc8c 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -290,6 +290,21 @@ public class CommandMapperTest { assertThat(result.getArguments(), contains(parts.get(2))); } + @Test + public void shouldSupportAuthMePrefix() { + // given + List parts = asList("authme:unregister", "Betty"); + CommandSender sender = mock(CommandSender.class); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); + + // when + FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); + + // then + assertThat(result.getResultStatus(), equalTo(FoundResultStatus.SUCCESS)); + assertThat(result.getCommandDescription(), equalTo(getCommandWithLabel(commands, "unregister"))); + } + @SuppressWarnings("unchecked") @Test public void shouldReturnExecutableCommandClasses() { diff --git a/src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java index 271f11741..bb5694e94 100644 --- a/src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java @@ -26,7 +26,7 @@ import static fr.xephi.authme.command.help.HelpProvider.SHOW_COMMAND; import static fr.xephi.authme.command.help.HelpProvider.SHOW_DESCRIPTION; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java index cf306599d..0474eac1a 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java @@ -1,8 +1,8 @@ package fr.xephi.authme.command.executable.authme; -import ch.jalu.injector.Injector; import fr.xephi.authme.TestHelper; import fr.xephi.authme.datasource.converter.Converter; +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; @@ -48,7 +48,7 @@ public class ConverterCommandTest { private BukkitService bukkitService; @Mock - private Injector injector; + private Factory converterFactory; @BeforeClass public static void initLogger() { @@ -66,7 +66,7 @@ public class ConverterCommandTest { // then verify(sender).sendMessage(argThat(containsString("Converter does not exist"))); verifyNoMoreInteractions(commandService); - verifyZeroInteractions(injector); + verifyZeroInteractions(converterFactory); verifyZeroInteractions(bukkitService); } @@ -100,8 +100,8 @@ public class ConverterCommandTest { // then verify(converter).execute(sender); verifyNoMoreInteractions(converter); - verify(injector).newInstance(converterClass); - verifyNoMoreInteractions(injector); + verify(converterFactory).newInstance(converterClass); + verifyNoMoreInteractions(converterFactory); } @Test @@ -120,14 +120,14 @@ public class ConverterCommandTest { // then verify(converter).execute(sender); verifyNoMoreInteractions(converter); - verify(injector).newInstance(converterClass); - verifyNoMoreInteractions(injector); + verify(converterFactory).newInstance(converterClass); + verifyNoMoreInteractions(converterFactory); verify(commandService).send(sender, MessageKey.ERROR); } private T createMockReturnedByInjector(Class clazz) { T converter = mock(clazz); - given(injector.newInstance(clazz)).willReturn(converter); + given(converterFactory.newInstance(clazz)).willReturn(converter); return converter; } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java index 914199120..5ee2161e1 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java @@ -2,7 +2,6 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.data.limbo.LimboCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.security.PasswordSecurity; @@ -56,9 +55,6 @@ public class RegisterAdminCommandTest { @Mock private ValidationService validationService; - @Mock - private LimboCache limboCache; - @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java index 3d9cf354e..ad49600f5 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.command.executable.authme; -import ch.jalu.injector.Injector; import fr.xephi.authme.AuthMe; import fr.xephi.authme.TestHelper; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSourceType; 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.output.LogLevel; import fr.xephi.authme.service.CommonService; @@ -23,17 +23,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.containsString; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.hamcrest.MockitoHamcrest.argThat; @@ -49,9 +46,6 @@ public class ReloadCommandTest { @Mock private AuthMe authMe; - @Mock - private Injector injector; - @Mock private Settings settings; @@ -61,6 +55,12 @@ public class ReloadCommandTest { @Mock private CommonService commandService; + @Mock + private SingletonStore reloadableStore; + + @Mock + private SingletonStore settingsDependentStore; + @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -83,11 +83,11 @@ public class ReloadCommandTest { mock(Reloadable.class), mock(Reloadable.class), mock(Reloadable.class)); List dependents = Arrays.asList( mock(SettingsDependent.class), mock(SettingsDependent.class)); - given(injector.retrieveAllOfType(Reloadable.class)).willReturn(reloadables); - given(injector.retrieveAllOfType(SettingsDependent.class)).willReturn(dependents); + given(reloadableStore.retrieveAllOfType()).willReturn(reloadables); + given(settingsDependentStore.retrieveAllOfType()).willReturn(dependents); // when - command.executeCommand(sender, Collections.emptyList()); + command.executeCommand(sender, Collections.emptyList()); // then verify(settings).reload(); @@ -99,16 +99,16 @@ public class ReloadCommandTest { public void shouldHandleReloadError() { // given CommandSender sender = mock(CommandSender.class); - doThrow(IllegalStateException.class).when(injector).retrieveAllOfType(Reloadable.class); + doThrow(IllegalStateException.class).when(reloadableStore).retrieveAllOfType(); given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); given(dataSource.getType()).willReturn(DataSourceType.MYSQL); // when - command.executeCommand(sender, Collections.emptyList()); + command.executeCommand(sender, Collections.emptyList()); // then verify(settings).reload(); - verify(injector).retrieveAllOfType(Reloadable.class); + verify(reloadableStore).retrieveAllOfType(); verify(sender).sendMessage(argThat(containsString("Error occurred"))); verify(authMe).stopOrUnload(); } @@ -120,15 +120,16 @@ public class ReloadCommandTest { CommandSender sender = mock(CommandSender.class); given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); given(dataSource.getType()).willReturn(DataSourceType.SQLITE); - given(injector.retrieveAllOfType(Reloadable.class)).willReturn(new ArrayList()); - given(injector.retrieveAllOfType(SettingsDependent.class)).willReturn(new ArrayList()); + given(reloadableStore.retrieveAllOfType()).willReturn(Collections.emptyList()); + given(settingsDependentStore.retrieveAllOfType()).willReturn(Collections.emptyList()); // when - command.executeCommand(sender, Collections.emptyList()); + command.executeCommand(sender, Collections.emptyList()); // then verify(settings).reload(); - verify(injector, times(2)).retrieveAllOfType(any(Class.class)); + verify(reloadableStore).retrieveAllOfType(); + verify(settingsDependentStore).retrieveAllOfType(); verify(sender).sendMessage(argThat(containsString("cannot change database type"))); } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionConsistencyTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionConsistencyTest.java new file mode 100644 index 000000000..c0e35b80a --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionConsistencyTest.java @@ -0,0 +1,52 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.TestHelper; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * Consistency tests for {@link DebugSection} implementors. + */ +public class DebugSectionConsistencyTest { + + private static List> debugClasses; + + @BeforeClass + public static void collectClasses() { + debugClasses = new ClassCollector( + TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT + "command/executable/authme/debug").collectClasses(); + } + + @Test + public void shouldAllBePackagePrivate() { + for (Class clazz : debugClasses) { + if (clazz != DebugCommand.class) { + assertThat(clazz + " should be package-private", + Modifier.isPublic(clazz.getModifiers()), equalTo(false)); + } + } + } + + @Test + public void shouldHaveDifferentSubcommandName() throws IllegalAccessException, InstantiationException { + Set names = new HashSet<>(); + for (Class clazz : debugClasses) { + if (DebugSection.class.isAssignableFrom(clazz) && !clazz.isInterface()) { + DebugSection debugSection = (DebugSection) clazz.newInstance(); + if (!names.add(debugSection.getName())) { + fail("Encountered name '" + debugSection.getName() + "' a second time in " + clazz); + } + } + } + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java new file mode 100644 index 000000000..e8443260d --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java @@ -0,0 +1,69 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.LimboService; +import org.bukkit.Location; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link DebugSectionUtils}. + */ +public class DebugSectionUtilsTest { + + @Test + public void shouldFormatLocation() { + // given / when + String result = DebugSectionUtils.formatLocation(0.0, 10.248592, -18934.2349023, "Main"); + + // then + assertThat(result, equalTo("(0, 10.25, -18934.23) in 'Main'")); + } + + @Test + public void shouldHandleNullWorld() { + // given + Location location = new Location(null, 3.7777, 2.14156, 1); + + // when + String result = DebugSectionUtils.formatLocation(location); + + // then + assertThat(result, equalTo("(3.78, 2.14, 1) in 'null'")); + } + + @Test + public void shouldHandleNullLocation() { + // given / when / then + assertThat(DebugSectionUtils.formatLocation(null), equalTo("null")); + } + + @Test + public void shouldHaveHiddenConstructor() { + TestHelper.validateHasOnlyPrivateEmptyConstructor(DebugSectionUtils.class); + } + + @Test + public void shouldFetchMapInLimboService() { + // given + LimboService limboService = mock(LimboService.class); + Map limboMap = new HashMap<>(); + ReflectionTestUtils.setField(LimboService.class, limboService, "entries", limboMap); + + // when + Map map = DebugSectionUtils.applyToLimboPlayersMap(limboService, Function.identity()); + + // then + assertThat(map, sameInstance(limboMap)); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionCheckerTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionCheckerTest.java new file mode 100644 index 000000000..9f7c6a92f --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionCheckerTest.java @@ -0,0 +1,97 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; + +/** + * Test for {@link HasPermissionChecker}. + */ +@RunWith(MockitoJUnitRunner.class) +public class HasPermissionCheckerTest { + + @InjectMocks + private HasPermissionChecker hasPermissionChecker; + + @Mock + private PermissionsManager permissionsManager; + + @Mock + private BukkitService bukkitService; + + @Test + public void shouldListAllPermissionNodeClasses() { + // given + List> permissionClasses = + new ClassCollector(TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT) + .collectClasses(PermissionNode.class).stream() + .filter(clz -> !clz.isInterface()) + .collect(Collectors.toList()); + + // when / then + assertThat(HasPermissionChecker.PERMISSION_NODE_CLASSES.containsAll(permissionClasses), equalTo(true)); + assertThat(HasPermissionChecker.PERMISSION_NODE_CLASSES, hasSize(permissionClasses.size())); + } + + @Test + public void shouldShowUsageInfo() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + hasPermissionChecker.execute(sender, emptyList()); + + // then + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeast(2)).sendMessage(msgCaptor.capture()); + assertThat( + msgCaptor.getAllValues().stream().anyMatch(msg -> msg.contains("/authme debug perm bobby my.perm.node")), + equalTo(true)); + } + + @Test + public void shouldShowSuccessfulTestWithRegularPlayer() { + // given + String name = "Chuck"; + Player player = mock(Player.class); + given(bukkitService.getPlayerExact(name)).willReturn(player); + PermissionNode permission = AdminPermission.CHANGE_EMAIL; + given(permissionsManager.hasPermission(player, permission)).willReturn(true); + CommandSender sender = mock(CommandSender.class); + + // when + hasPermissionChecker.execute(sender, asList(name, permission.getNode())); + + // then + verify(bukkitService).getPlayerExact(name); + verify(permissionsManager).hasPermission(player, permission); + verify(sender).sendMessage(argThat(containsString("Success: player '" + player.getName() + + "' has permission '" + permission.getNode() + "'"))); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java index dd9240058..bb331b286 100644 --- a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java @@ -2,11 +2,9 @@ package fr.xephi.authme.command.executable.captcha; import fr.xephi.authme.data.CaptchaManager; import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.service.CommonService; -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.message.MessageKey; -import fr.xephi.authme.task.MessageTask; +import fr.xephi.authme.service.CommonService; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,7 +38,7 @@ public class CaptchaCommandTest { private CommonService commandService; @Mock - private LimboCache limboCache; + private LimboService limboService; @Test public void shouldDetectIfPlayerIsLoggedIn() { @@ -82,10 +80,6 @@ public class CaptchaCommandTest { given(captchaManager.isCaptchaRequired(name)).willReturn(true); String captchaCode = "3991"; given(captchaManager.checkCode(name, captchaCode)).willReturn(true); - MessageTask messageTask = mock(MessageTask.class); - LimboPlayer limboPlayer = mock(LimboPlayer.class); - given(limboPlayer.getMessageTask()).willReturn(messageTask); - given(limboCache.getPlayerData(name)).willReturn(limboPlayer); // when command.executeCommand(player, Collections.singletonList(captchaCode)); @@ -96,7 +90,7 @@ public class CaptchaCommandTest { verifyNoMoreInteractions(captchaManager); verify(commandService).send(player, MessageKey.CAPTCHA_SUCCESS); verify(commandService).send(player, MessageKey.LOGIN_MESSAGE); - verify(messageTask).setMuted(false); + verify(limboService).unmuteMessageTask(player); verifyNoMoreInteractions(commandService); } diff --git a/src/test/java/fr/xephi/authme/command/executable/email/ProcessCodeCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/ProcessCodeCommandTest.java new file mode 100644 index 000000000..b5790cc44 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/email/ProcessCodeCommandTest.java @@ -0,0 +1,92 @@ +package fr.xephi.authme.command.executable.email; + +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 org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link ProcessCodeCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class ProcessCodeCommandTest { + + @InjectMocks + private ProcessCodeCommand command; + + @Mock + private CommonService commonService; + + @Mock + private RecoveryCodeService codeService; + + @Mock + private PasswordRecoveryService recoveryService; + + @Test + public void shouldSendErrorForInvalidRecoveryCode() { + // given + String name = "Vultur3"; + Player sender = mock(Player.class); + given(sender.getName()).willReturn(name); + given(codeService.hasTriesLeft(name)).willReturn(true); + given(codeService.isCodeValid(name, "bogus")).willReturn(false); + given(codeService.getTriesLeft(name)).willReturn(2); + + // when + command.executeCommand(sender, Collections.singletonList("bogus")); + + // then + verify(commonService).send(sender, MessageKey.INCORRECT_RECOVERY_CODE, "2"); + verifyNoMoreInteractions(recoveryService); + } + + @Test + public void shouldSendErrorForNoMoreTries() { + // given + String name = "BobbY"; + Player sender = mock(Player.class); + given(sender.getName()).willReturn(name); + given(codeService.hasTriesLeft(name)).willReturn(false); + + // when + command.executeCommand(sender, Collections.singletonList("bogus")); + + // then + verify(commonService).send(sender, MessageKey.RECOVERY_TRIES_EXCEEDED); + verify(codeService).removeCode(name); + verifyNoMoreInteractions(recoveryService); + } + + @Test + public void shouldProcessCorrectCode() { + // given + String name = "Dwight"; + String code = "chickenDinner"; + Player sender = mock(Player.class); + given(sender.getName()).willReturn(name); + given(codeService.hasTriesLeft(name)).willReturn(true); + given(codeService.isCodeValid(name, code)).willReturn(true); + + // when + command.runCommand(sender, Collections.singletonList(code)); + + // then + verify(commonService).send(sender, MessageKey.RECOVERY_CODE_CORRECT); + verify(recoveryService).addSuccessfulRecovery(sender); + verify(codeService).removeCode(name); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java index 27166f95c..8b026fc52 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java @@ -1,36 +1,30 @@ package fr.xephi.authme.command.executable.email; +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; import fr.xephi.authme.TestHelper; 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.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.entity.Player; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import java.util.Arrays; import java.util.Collections; -import static fr.xephi.authme.AuthMeMatchers.stringWithLength; -import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -38,19 +32,19 @@ import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link RecoverEmailCommand}. */ -@RunWith(MockitoJUnitRunner.class) +@RunWith(DelayedInjectionRunner.class) public class RecoverEmailCommandTest { private static final String DEFAULT_EMAIL = "your@email.com"; - @InjectMocks + @InjectDelayed private RecoverEmailCommand command; @Mock private PasswordSecurity passwordSecurity; @Mock - private CommonService commandService; + private CommonService commonService; @Mock private DataSource dataSource; @@ -59,7 +53,10 @@ public class RecoverEmailCommandTest { private PlayerCache playerCache; @Mock - private SendMailSSL sendMailSsl; + private EmailService emailService; + + @Mock + private PasswordRecoveryService recoveryService; @Mock private RecoveryCodeService recoveryCodeService; @@ -69,17 +66,22 @@ public class RecoverEmailCommandTest { TestHelper.setupLogger(); } + @BeforeInjecting + public void initSettings() { + given(commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS)).willReturn(40); + } + @Test public void shouldHandleMissingMailProperties() { // given - given(sendMailSsl.hasAllInformation()).willReturn(false); + given(emailService.hasAllInformation()).willReturn(false); Player sender = mock(Player.class); // when command.executeCommand(sender, Collections.singletonList("some@email.tld")); // then - verify(commandService).send(sender, MessageKey.INCOMPLETE_EMAIL_SETTINGS); + verify(commonService).send(sender, MessageKey.INCOMPLETE_EMAIL_SETTINGS); verifyZeroInteractions(dataSource, passwordSecurity); } @@ -89,16 +91,16 @@ public class RecoverEmailCommandTest { String name = "Bobby"; Player sender = mock(Player.class); given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(true); // when command.executeCommand(sender, Collections.singletonList("bobby@example.org")); // then - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verifyZeroInteractions(dataSource); - verify(commandService).send(sender, MessageKey.ALREADY_LOGGED_IN_ERROR); + verify(commonService).send(sender, MessageKey.ALREADY_LOGGED_IN_ERROR); } @Test @@ -107,7 +109,7 @@ public class RecoverEmailCommandTest { String name = "Player123"; Player sender = mock(Player.class); given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); given(dataSource.getAuth(name)).willReturn(null); @@ -115,10 +117,10 @@ public class RecoverEmailCommandTest { command.executeCommand(sender, Collections.singletonList("someone@example.com")); // then - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verify(dataSource).getAuth(name); verifyNoMoreInteractions(dataSource); - verify(commandService).send(sender, MessageKey.USAGE_REGISTER); + verify(commonService).send(sender, MessageKey.USAGE_REGISTER); } @Test @@ -127,7 +129,7 @@ public class RecoverEmailCommandTest { String name = "Tract0r"; Player sender = mock(Player.class); given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); given(dataSource.getAuth(name)).willReturn(newAuthWithEmail(DEFAULT_EMAIL)); @@ -135,10 +137,10 @@ public class RecoverEmailCommandTest { command.executeCommand(sender, Collections.singletonList(DEFAULT_EMAIL)); // then - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verify(dataSource).getAuth(name); verifyNoMoreInteractions(dataSource); - verify(commandService).send(sender, MessageKey.INVALID_EMAIL); + verify(commonService).send(sender, MessageKey.INVALID_EMAIL); } @Test @@ -147,7 +149,7 @@ public class RecoverEmailCommandTest { String name = "Rapt0r"; Player sender = mock(Player.class); given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); given(dataSource.getAuth(name)).willReturn(newAuthWithEmail("raptor@example.org")); @@ -155,10 +157,10 @@ public class RecoverEmailCommandTest { command.executeCommand(sender, Collections.singletonList("wrong-email@example.com")); // then - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verify(dataSource).getAuth(name); verifyNoMoreInteractions(dataSource); - verify(commandService).send(sender, MessageKey.INVALID_EMAIL); + verify(commonService).send(sender, MessageKey.INVALID_EMAIL); } @Test @@ -167,8 +169,8 @@ public class RecoverEmailCommandTest { String name = "Vultur3"; Player sender = mock(Player.class); given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); - given(sendMailSsl.sendRecoveryCode(anyString(), anyString(), anyString())).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); + given(emailService.sendRecoveryCode(anyString(), anyString(), anyString())).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); String email = "v@example.com"; given(dataSource.getAuth(name)).willReturn(newAuthWithEmail(email)); @@ -180,105 +182,34 @@ public class RecoverEmailCommandTest { command.executeCommand(sender, Collections.singletonList(email.toUpperCase())); // then - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verify(dataSource).getAuth(name); - verify(recoveryCodeService).generateCode(name); - verify(commandService).send(sender, MessageKey.RECOVERY_CODE_SENT); - verify(sendMailSsl).sendRecoveryCode(name, email, code); - } - - @Test - public void shouldSendErrorForInvalidRecoveryCode() { - // given - String name = "Vultur3"; - Player sender = mock(Player.class); - given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); - given(playerCache.isAuthenticated(name)).willReturn(false); - String email = "vulture@example.com"; - PlayerAuth auth = newAuthWithEmail(email); - given(dataSource.getAuth(name)).willReturn(auth); - given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(true); - given(recoveryCodeService.isCodeValid(name, "bogus")).willReturn(false); - - // when - command.executeCommand(sender, Arrays.asList(email, "bogus")); - - // then - verify(sendMailSsl).hasAllInformation(); - verify(dataSource, only()).getAuth(name); - verify(commandService).send(sender, MessageKey.INCORRECT_RECOVERY_CODE); - verifyNoMoreInteractions(sendMailSsl); - } - - @Test - public void shouldResetPasswordAndSendEmail() { - // given - String name = "Vultur3"; - Player sender = mock(Player.class); - given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); - given(sendMailSsl.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); - given(playerCache.isAuthenticated(name)).willReturn(false); - String email = "vulture@example.com"; - String code = "A6EF3AC8"; - PlayerAuth auth = newAuthWithEmail(email); - given(dataSource.getAuth(name)).willReturn(auth); - given(commandService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)).willReturn(20); - given(passwordSecurity.computeHash(anyString(), eq(name))) - .willAnswer(invocation -> new HashedPassword(invocation.getArgument(0))); - given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(true); - given(recoveryCodeService.isCodeValid(name, code)).willReturn(true); - - // when - command.executeCommand(sender, Arrays.asList(email, code)); - - // then - verify(sendMailSsl).hasAllInformation(); - verify(dataSource).getAuth(name); - ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); - verify(passwordSecurity).computeHash(passwordCaptor.capture(), eq(name)); - String generatedPassword = passwordCaptor.getValue(); - assertThat(generatedPassword, stringWithLength(20)); - verify(dataSource).updatePassword(eq(name), any(HashedPassword.class)); - verify(recoveryCodeService).removeCode(name); - verify(sendMailSsl).sendPasswordMail(name, email, generatedPassword); - verify(commandService).send(sender, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); + verify(recoveryService).createAndSendRecoveryCode(sender, email); } @Test public void shouldGenerateNewPasswordWithoutRecoveryCode() { // given - String name = "sh4rK"; + String name = "Vultur3"; Player sender = mock(Player.class); given(sender.getName()).willReturn(name); - given(sendMailSsl.hasAllInformation()).willReturn(true); - given(sendMailSsl.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); + given(emailService.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - String email = "shark@example.org"; + String email = "vulture@example.com"; PlayerAuth auth = newAuthWithEmail(email); given(dataSource.getAuth(name)).willReturn(auth); - given(commandService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)).willReturn(20); - given(passwordSecurity.computeHash(anyString(), eq(name))) - .willAnswer(invocation -> new HashedPassword(invocation.getArgument(0))); given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(false); // when command.executeCommand(sender, Collections.singletonList(email)); // then - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verify(dataSource).getAuth(name); - ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); - verify(passwordSecurity).computeHash(passwordCaptor.capture(), eq(name)); - String generatedPassword = passwordCaptor.getValue(); - assertThat(generatedPassword, stringWithLength(20)); - verify(dataSource).updatePassword(eq(name), any(HashedPassword.class)); - verify(sendMailSsl).sendPasswordMail(name, email, generatedPassword); - verify(commandService).send(sender, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); + verify(recoveryService).generateAndSendNewPassword(sender, email); } - private static PlayerAuth newAuthWithEmail(String email) { return PlayerAuth.builder() .name("name") diff --git a/src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java new file mode 100644 index 000000000..dc5757d15 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java @@ -0,0 +1,100 @@ +package fr.xephi.authme.command.executable.email; + +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 org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link SetPasswordCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class SetPasswordCommandTest { + + @InjectMocks + private SetPasswordCommand command; + + @Mock + private DataSource dataSource; + + @Mock + private CommonService commonService; + + @Mock + private PasswordRecoveryService recoveryService; + + @Mock + private PasswordSecurity passwordSecurity; + + @Mock + private ValidationService validationService; + + @Test + public void shouldChangePassword() { + // given + Player player = mock(Player.class); + String name = "Jerry"; + given(player.getName()).willReturn(name); + given(recoveryService.canChangePassword(player)).willReturn(true); + HashedPassword hashedPassword = passwordSecurity.computeHash("abc123", name); + given(passwordSecurity.computeHash("abc123", name)).willReturn(hashedPassword); + given(validationService.validatePassword("abc123", name)) + .willReturn(new ValidationService.ValidationResult()); + + // when + command.runCommand(player, Collections.singletonList("abc123")); + + // then + verify(validationService).validatePassword("abc123", name); + verify(dataSource).updatePassword(name, hashedPassword); + verify(commonService).send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); + } + + @Test + public void shouldRejectInvalidPassword() { + // given + Player player = mock(Player.class); + String name = "Morgan"; + given(player.getName()).willReturn(name); + String password = "newPW"; + given(validationService.validatePassword(password, name)) + .willReturn(new ValidationService.ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH)); + given(recoveryService.canChangePassword(player)).willReturn(true); + + // when + command.executeCommand(player, Collections.singletonList(password)); + + // then + verify(validationService).validatePassword(password, name); + verify(commonService).send(player, MessageKey.INVALID_PASSWORD_LENGTH, new String[0]); + } + + @Test + public void shouldDoNothingCantChangePass() { + // given + Player player = mock(Player.class); + + // when + command.runCommand(player, Collections.singletonList("abc123")); + + // then + verifyZeroInteractions(validationService); + verifyZeroInteractions(dataSource); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java index 76d1352d8..6224a6065 100644 --- a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java @@ -1,13 +1,17 @@ package fr.xephi.authme.command.executable.register; +import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; -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.RegistrationExecutor; -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.RegistrationParameters; +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; @@ -16,6 +20,9 @@ import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -24,10 +31,16 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.Objects; import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -52,14 +65,11 @@ public class RegisterCommandTest { private Management management; @Mock - private SendMailSSL sendMailSsl; + private EmailService emailService; @Mock private ValidationService validationService; - @Mock - private RegistrationExecutorProvider registrationExecutorProvider; - @BeforeClass public static void setup() { TestHelper.setupLogger(); @@ -82,7 +92,7 @@ public class RegisterCommandTest { // then verify(sender).sendMessage(argThat(containsString("Player only!"))); - verifyZeroInteractions(management, sendMailSsl); + verifyZeroInteractions(management, emailService); } @Test @@ -90,15 +100,14 @@ public class RegisterCommandTest { // given given(commonService.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.TWO_FACTOR); Player player = mock(Player.class); - RegistrationExecutor executor = mock(RegistrationExecutor.class); - given(registrationExecutorProvider.getTwoFactorRegisterExecutor(player)).willReturn(executor); // when command.executeCommand(player, Collections.emptyList()); // then - verify(management).performRegister(player, executor); - verifyZeroInteractions(sendMailSsl); + verify(management).performRegister(eq(RegistrationMethod.TWO_FACTOR_REGISTRATION), + argThat(isEqualTo(TwoFactorRegisterParams.of(player)))); + verifyZeroInteractions(emailService); } @Test @@ -111,7 +120,7 @@ public class RegisterCommandTest { // then verify(commonService).send(player, MessageKey.USAGE_REGISTER); - verifyZeroInteractions(management, sendMailSsl); + verifyZeroInteractions(management, emailService); } @Test @@ -126,13 +135,13 @@ public class RegisterCommandTest { // then verify(commonService).send(player, MessageKey.USAGE_REGISTER); - verifyZeroInteractions(management, sendMailSsl); + verifyZeroInteractions(management, emailService); } @Test public void shouldReturnErrorForMissingEmailConfirmation() { // given - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); given(commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE)).willReturn(RegistrationType.EMAIL); given(commonService.getProperty(RegistrationSettings.REGISTER_SECOND_ARGUMENT)).willReturn(RegisterSecondaryArgument.EMAIL_MANDATORY); given(validationService.validateEmail(anyString())).willReturn(true); @@ -150,7 +159,7 @@ public class RegisterCommandTest { public void shouldThrowErrorForMissingEmailConfiguration() { // given given(commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE)).willReturn(RegistrationType.EMAIL); - given(sendMailSsl.hasAllInformation()).willReturn(false); + given(emailService.hasAllInformation()).willReturn(false); Player player = mock(Player.class); // when @@ -158,7 +167,7 @@ public class RegisterCommandTest { // then verify(commonService).send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS); - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verifyZeroInteractions(management); } @@ -168,7 +177,7 @@ public class RegisterCommandTest { String playerMail = "player@example.org"; given(validationService.validateEmail(playerMail)).willReturn(false); given(commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE)).willReturn(RegistrationType.EMAIL); - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); Player player = mock(Player.class); // when @@ -187,7 +196,7 @@ public class RegisterCommandTest { given(validationService.validateEmail(playerMail)).willReturn(true); given(commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE)).willReturn(RegistrationType.EMAIL); given(commonService.getProperty(RegistrationSettings.REGISTER_SECOND_ARGUMENT)).willReturn(RegisterSecondaryArgument.CONFIRMATION); - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); Player player = mock(Player.class); // when @@ -195,7 +204,7 @@ public class RegisterCommandTest { // then verify(commonService).send(player, MessageKey.USAGE_REGISTER); - verify(sendMailSsl).hasAllInformation(); + verify(emailService).hasAllInformation(); verifyZeroInteractions(management); } @@ -206,18 +215,17 @@ public class RegisterCommandTest { given(validationService.validateEmail(playerMail)).willReturn(true); given(commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE)).willReturn(RegistrationType.EMAIL); given(commonService.getProperty(RegistrationSettings.REGISTER_SECOND_ARGUMENT)).willReturn(RegisterSecondaryArgument.CONFIRMATION); - given(sendMailSsl.hasAllInformation()).willReturn(true); + given(emailService.hasAllInformation()).willReturn(true); Player player = mock(Player.class); - RegistrationExecutor executor = mock(RegistrationExecutor.class); - given(registrationExecutorProvider.getEmailRegisterExecutor(player, playerMail)).willReturn(executor); // when command.executeCommand(player, Arrays.asList(playerMail, playerMail)); // then verify(validationService).validateEmail(playerMail); - verify(sendMailSsl).hasAllInformation(); - verify(management).performRegister(player, executor); + verify(emailService).hasAllInformation(); + verify(management).performRegister(eq(RegistrationMethod.EMAIL_REGISTRATION), + argThat(isEqualTo(EmailRegisterParams.of(player, playerMail)))); } @Test @@ -232,21 +240,20 @@ public class RegisterCommandTest { // then verify(commonService).send(player, MessageKey.PASSWORD_MATCH_ERROR); - verifyZeroInteractions(management, sendMailSsl); + verifyZeroInteractions(management, emailService); } @Test public void shouldPerformPasswordRegistration() { // given Player player = mock(Player.class); - RegistrationExecutor executor = mock(RegistrationExecutor.class); - given(registrationExecutorProvider.getPasswordRegisterExecutor(player, "myPass", null)).willReturn(executor); // when command.executeCommand(player, Collections.singletonList("myPass")); // then - verify(management).performRegister(player, executor); + verify(management).performRegister(eq(RegistrationMethod.PASSWORD_REGISTRATION), + argThat(isEqualTo(PasswordRegisterParams.of(player, "myPass", null)))); } @Test @@ -257,15 +264,14 @@ public class RegisterCommandTest { String email = "email@example.org"; given(validationService.validateEmail(email)).willReturn(true); Player player = mock(Player.class); - RegistrationExecutor executor = mock(RegistrationExecutor.class); - given(registrationExecutorProvider.getPasswordRegisterExecutor(player, "myPass", email)).willReturn(executor); // when command.executeCommand(player, Arrays.asList("myPass", email)); // then verify(validationService).validateEmail(email); - verify(management).performRegister(player, executor); + verify(management).performRegister(eq(RegistrationMethod.PASSWORD_REGISTRATION), + argThat(isEqualTo(PasswordRegisterParams.of(player, "myPass", email)))); } @Test @@ -292,14 +298,64 @@ public class RegisterCommandTest { given(commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE)).willReturn(RegistrationType.PASSWORD); given(commonService.getProperty(RegistrationSettings.REGISTER_SECOND_ARGUMENT)).willReturn(RegisterSecondaryArgument.EMAIL_OPTIONAL); Player player = mock(Player.class); - RegistrationExecutor executor = mock(RegistrationExecutor.class); - given(registrationExecutorProvider.getPasswordRegisterExecutor(eq(player), anyString(), eq(null))).willReturn(executor); // when command.executeCommand(player, Collections.singletonList("myPass")); // then - verify(registrationExecutorProvider).getPasswordRegisterExecutor(player, "myPass", null); - verify(management).performRegister(player, executor); + verify(management).performRegister(eq(RegistrationMethod.PASSWORD_REGISTRATION), + argThat(isEqualTo(PasswordRegisterParams.of(player, "myPass", null)))); + } + + + // TODO ljacqu 20170317: Document and extract as util + + private static

    Matcher

    isEqualTo(P expected) { + return new TypeSafeMatcher

    () { + @Override + protected boolean matchesSafely(RegistrationParameters item) { + assertAreParamsEqual(expected, item); + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("parameters " + expected); + } + }; + } + + private static void assertAreParamsEqual(RegistrationParameters lhs, RegistrationParameters rhs) { + if (lhs.getClass() != rhs.getClass()) { + fail("Params classes don't match, got " + lhs.getClass().getSimpleName() + + " and " + rhs.getClass().getSimpleName()); + } + + List fieldsToCheck = getFields(lhs); + for (Field field : fieldsToCheck) { + Object lhsValue = ReflectionTestUtils.getFieldValue(field, lhs); + Object rhsValue = ReflectionTestUtils.getFieldValue(field, rhs); + if (!Objects.equals(lhsValue, rhsValue)) { + fail("Field '" + field.getName() + "' does not have same value: '" + + lhsValue + "' vs. '" + rhsValue + "'"); + } + } + } + + private static List getFields(RegistrationParameters params) { + List fields = new ArrayList<>(); + Class currentClass = params.getClass(); + while (currentClass != null) { + for (Field f : currentClass.getDeclaredFields()) { + if (!Modifier.isStatic(f.getModifiers())) { + fields.add(f); + } + } + if (currentClass == RegistrationParameters.class) { + break; + } + currentClass = currentClass.getSuperclass(); + } + return fields; } } diff --git a/src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java index e646031f8..d4560640b 100644 --- a/src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java @@ -4,6 +4,7 @@ import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.Management; import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; @@ -13,6 +14,8 @@ import org.mockito.junit.MockitoJUnitRunner; import java.util.Collections; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -71,4 +74,17 @@ public class UnregisterCommandTest { verify(management).performUnregister(player, password); } + @Test + public void shouldStopIfSenderIsNotPlayer() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList("password")); + + // then + verifyZeroInteractions(playerCache, management); + verify(sender).sendMessage(argThat(containsString("/authme unregister "))); + } + } diff --git a/src/test/java/fr/xephi/authme/command/help/HelpMessagesServiceTest.java b/src/test/java/fr/xephi/authme/command/help/HelpMessagesServiceTest.java index 292b0ec97..ea0f372a2 100644 --- a/src/test/java/fr/xephi/authme/command/help/HelpMessagesServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/help/HelpMessagesServiceTest.java @@ -39,10 +39,10 @@ public class HelpMessagesServiceTest { @Mock private MessageFileHandlerProvider messageFileHandlerProvider; - @SuppressWarnings("unchecked") @BeforeInjecting + @SuppressWarnings("unchecked") public void initializeHandler() { - MessageFileHandler handler = new MessageFileHandler(getJarFile(TEST_FILE), "messages/messages_en.yml"); + MessageFileHandler handler = new MessageFileHandler(getJarFile(TEST_FILE), "messages/messages_en.yml", null); given(messageFileHandlerProvider.initializeHandler(any(Function.class))).willReturn(handler); } diff --git a/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java b/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java index 92be450da..6f20d830b 100644 --- a/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/CaptchaManagerTest.java @@ -3,12 +3,10 @@ package fr.xephi.authme.data; import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.expiring.TimedCounter; import org.junit.Test; -import java.util.Map; - import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -57,11 +55,6 @@ public class CaptchaManagerTest { assertThat(manager.checkCode(player, "bogus"), equalTo(true)); } - /** - * Tests {@link CaptchaManager#getCaptchaCode} and {@link CaptchaManager#getCaptchaCodeOrGenerateNew}. - * The former method should never change the code (and so return {@code null} for no code) while the latter should - * generate a new code if no code is yet present. If a code is saved, it should never generate a new one. - */ @Test public void shouldHaveSameCodeAfterGeneration() { // given @@ -70,18 +63,14 @@ public class CaptchaManagerTest { CaptchaManager manager = new CaptchaManager(settings); // when - String code1 = manager.getCaptchaCode(player); + String code1 = manager.getCaptchaCodeOrGenerateNew(player); String code2 = manager.getCaptchaCodeOrGenerateNew(player); - String code3 = manager.getCaptchaCode(player); - String code4 = manager.getCaptchaCodeOrGenerateNew(player); - String code5 = manager.getCaptchaCode(player); + String code3 = manager.getCaptchaCodeOrGenerateNew(player); // then - assertThat(code1, nullValue()); - assertThat(code2.length(), equalTo(5)); - assertThat(code3, equalTo(code2)); - assertThat(code4, equalTo(code2)); - assertThat(code5, equalTo(code2)); + assertThat(code1.length(), equalTo(5)); + assertThat(code2, equalTo(code1)); + assertThat(code3, equalTo(code1)); } @Test @@ -104,7 +93,7 @@ public class CaptchaManagerTest { // then 2 assertThat(manager.isCaptchaRequired(player), equalTo(false)); - assertHasCount(manager, player, null); + assertHasCount(manager, player, 0); } @Test @@ -120,7 +109,7 @@ public class CaptchaManagerTest { // then assertThat(manager.isCaptchaRequired(player), equalTo(false)); - assertHasCount(manager, player, null); + assertHasCount(manager, player, 0); } @Test @@ -149,11 +138,12 @@ public class CaptchaManagerTest { given(settings.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(true); given(settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA)).willReturn(maxTries); given(settings.getProperty(SecuritySettings.CAPTCHA_LENGTH)).willReturn(captchaLength); + given(settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET)).willReturn(30); return settings; } private static void assertHasCount(CaptchaManager manager, String player, Integer count) { - Map playerCounts = ReflectionTestUtils + TimedCounter playerCounts = ReflectionTestUtils .getFieldValue(CaptchaManager.class, manager, "playerCounts"); assertThat(playerCounts.get(player.toLowerCase()), equalTo(count)); } diff --git a/src/test/java/fr/xephi/authme/data/SessionManagerTest.java b/src/test/java/fr/xephi/authme/data/SessionManagerTest.java index f01311af1..738c1c158 100644 --- a/src/test/java/fr/xephi/authme/data/SessionManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/SessionManagerTest.java @@ -3,19 +3,17 @@ package fr.xephi.authme.data; import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.util.expiring.ExpiringSet; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; -import java.util.Map; - -import static org.hamcrest.Matchers.aMapWithSize; -import static org.hamcrest.Matchers.anEmptyMap; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * Test for {@link SessionManager}. @@ -91,24 +89,6 @@ public class SessionManagerTest { assertThat(manager.hasSession(player), equalTo(false)); } - @Test - public void shouldDenySessionIfTimeoutHasExpired() { - // given - int timeout = 20; - Settings settings = mockSettings(true, timeout); - String player = "patrick"; - SessionManager manager = new SessionManager(settings); - Map sessions = getSessionsMap(manager); - // Add session entry for player that just has expired - sessions.put(player, System.currentTimeMillis() - 1000); - - // when - boolean result = manager.hasSession(player); - - // then - assertThat(result, equalTo(false)); - } - @Test public void shouldClearAllSessionsAfterDisable() { // given @@ -121,7 +101,7 @@ public class SessionManagerTest { manager.reload(mockSettings(false, 20)); // then - assertThat(getSessionsMap(manager), anEmptyMap()); + assertThat(getSessionsMap(manager).isEmpty(), equalTo(true)); } @Test @@ -129,18 +109,14 @@ public class SessionManagerTest { // given Settings settings = mockSettings(true, 1); SessionManager manager = new SessionManager(settings); - Map sessions = getSessionsMap(manager); - sessions.put("somebody", System.currentTimeMillis() - 123L); - sessions.put("someone", System.currentTimeMillis() + 4040L); - sessions.put("anyone", System.currentTimeMillis() - 1000L); - sessions.put("everyone", System.currentTimeMillis() + 60000L); + ExpiringSet expiringSet = mockExpiringSet(); + setSessionsMap(manager, expiringSet); // when manager.performCleanup(); // then - assertThat(sessions, aMapWithSize(2)); - assertThat(sessions.keySet(), containsInAnyOrder("someone", "everyone")); + verify(expiringSet).removeExpiredEntries(); } @Test @@ -148,23 +124,28 @@ public class SessionManagerTest { // given Settings settings = mockSettings(false, 1); SessionManager manager = new SessionManager(settings); - Map sessions = getSessionsMap(manager); - sessions.put("somebody", System.currentTimeMillis() - 123L); - sessions.put("someone", System.currentTimeMillis() + 4040L); - sessions.put("anyone", System.currentTimeMillis() - 1000L); - sessions.put("everyone", System.currentTimeMillis() + 60000L); + ExpiringSet expiringSet = mockExpiringSet(); + setSessionsMap(manager, expiringSet); // when manager.performCleanup(); // then - assertThat(sessions, aMapWithSize(4)); // map not changed -> no cleanup performed + verify(expiringSet, never()).removeExpiredEntries(); } - private static Map getSessionsMap(SessionManager manager) { + private static ExpiringSet getSessionsMap(SessionManager manager) { return ReflectionTestUtils.getFieldValue(SessionManager.class, manager, "sessions"); } + private static void setSessionsMap(SessionManager manager, ExpiringSet sessionsMap) { + ReflectionTestUtils.setField(SessionManager.class, manager, "sessions", sessionsMap); + } + + @SuppressWarnings("unchecked") + private static ExpiringSet mockExpiringSet() { + return mock(ExpiringSet.class); + } private static Settings mockSettings(boolean isEnabled, int sessionTimeout) { Settings settings = mock(Settings.class); diff --git a/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java b/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java index 28c2ab9ee..ab1cbb0b5 100644 --- a/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/TempbanManagerTest.java @@ -2,12 +2,12 @@ package fr.xephi.authme.data; import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; -import fr.xephi.authme.data.TempbanManager.TimedCounter; 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.util.expiring.TimedCounter; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,8 +20,7 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; -import static org.hamcrest.Matchers.aMapWithSize; -import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertThat; @@ -195,42 +194,24 @@ public class TempbanManagerTest { @Test public void shouldPerformCleanup() { // given - // `expirationPoint` is the approximate timestamp until which entries should be considered, so subtracting - // from it will create expired entries, and adding a reasonably large number makes it still valid - final long expirationPoint = System.currentTimeMillis() - TEST_EXPIRATION_THRESHOLD; - // 2 current entries with total 6 failed tries - Map map1 = new HashMap<>(); - map1.put("name", newTimedCounter(4, expirationPoint + 20_000)); - map1.put("other", newTimedCounter(2, expirationPoint + 40_000)); - // 0 current entries - Map map2 = new HashMap<>(); - map2.put("someone", newTimedCounter(10, expirationPoint - 5_000)); - map2.put("somebody", newTimedCounter(10, expirationPoint - 8_000)); - // 1 current entry with total 4 failed tries - Map map3 = new HashMap<>(); - map3.put("some", newTimedCounter(5, expirationPoint - 12_000)); - map3.put("test", newTimedCounter(4, expirationPoint + 8_000)); - map3.put("values", newTimedCounter(2, expirationPoint - 80_000)); + Map> counts = new HashMap<>(); + TimedCounter counter1 = mockCounter(); + given(counter1.isEmpty()).willReturn(true); + counts.put("11.11.11.11", counter1); + TimedCounter counter2 = mockCounter(); + given(counter2.isEmpty()).willReturn(false); + counts.put("33.33.33.33", counter2); - String[] addresses = {"123.45.67.89", "127.0.0.1", "192.168.0.1"}; - Map> counterMap = new HashMap<>(); - counterMap.put(addresses[0], map1); - counterMap.put(addresses[1], map2); - counterMap.put(addresses[2], map3); - - TempbanManager manager = new TempbanManager(bukkitService, messages, mockSettings(5, 250)); - ReflectionTestUtils.setField(TempbanManager.class, manager, "ipLoginFailureCounts", counterMap); + TempbanManager manager = new TempbanManager(bukkitService, messages, mockSettings(3, 10)); + ReflectionTestUtils.setField(TempbanManager.class, manager, "ipLoginFailureCounts", counts); // when manager.performCleanup(); // then - assertThat(counterMap.get(addresses[0]), aMapWithSize(2)); - assertHasCount(manager, addresses[0], "name", 4); - assertHasCount(manager, addresses[0], "other", 2); - assertThat(counterMap.get(addresses[1]), anEmptyMap()); - assertThat(counterMap.get(addresses[2]), aMapWithSize(1)); - assertHasCount(manager, addresses[2], "test", 4); + verify(counter1).removeExpiredEntries(); + verify(counter2).removeExpiredEntries(); + assertThat(counts.keySet(), contains("33.33.33.33")); } private static Settings mockSettings(int maxTries, int tempbanLength) { @@ -244,21 +225,20 @@ public class TempbanManagerTest { } private static void assertHasNoEntries(TempbanManager manager, String address) { - Map> playerCounts = ReflectionTestUtils + Map> playerCounts = ReflectionTestUtils .getFieldValue(TempbanManager.class, manager, "ipLoginFailureCounts"); - Map map = playerCounts.get(address); - assertThat(map == null || map.isEmpty(), equalTo(true)); + TimedCounter counter = playerCounts.get(address); + assertThat(counter == null || counter.isEmpty(), equalTo(true)); } private static void assertHasCount(TempbanManager manager, String address, String name, int count) { - Map> playerCounts = ReflectionTestUtils + Map> playerCounts = ReflectionTestUtils .getFieldValue(TempbanManager.class, manager, "ipLoginFailureCounts"); - assertThat(playerCounts.get(address).get(name).getCount(TEST_EXPIRATION_THRESHOLD), equalTo(count)); + assertThat(playerCounts.get(address).get(name), equalTo(count)); } - private static TimedCounter newTimedCounter(int count, long timestamp) { - TimedCounter counter = new TimedCounter(count); - ReflectionTestUtils.setField(TimedCounter.class, counter, "lastIncrementTimestamp", timestamp); - return counter; + @SuppressWarnings("unchecked") + private static TimedCounter mockCounter() { + return mock(TimedCounter.class); } } diff --git a/src/test/java/fr/xephi/authme/data/limbo/AllowFlightRestoreTypeTest.java b/src/test/java/fr/xephi/authme/data/limbo/AllowFlightRestoreTypeTest.java new file mode 100644 index 000000000..8c22cc756 --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/AllowFlightRestoreTypeTest.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.data.limbo; + +import org.bukkit.entity.Player; +import org.junit.Test; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link AllowFlightRestoreType}. + */ +public class AllowFlightRestoreTypeTest { + + @Test + public void shouldRestoreValue() { + // given + LimboPlayer limboWithFly = newLimboWithAllowFlight(true); + LimboPlayer limboWithoutFly = newLimboWithAllowFlight(false); + Player player1 = mock(Player.class); + Player player2 = mock(Player.class); + + // when + AllowFlightRestoreType.RESTORE.restoreAllowFlight(player1, limboWithFly); + AllowFlightRestoreType.RESTORE.restoreAllowFlight(player2, limboWithoutFly); + + // then + verify(player1).setAllowFlight(true); + verify(player2).setAllowFlight(false); + } + + @Test + public void shouldEnableFlight() { + // given + LimboPlayer limboWithFly = newLimboWithAllowFlight(true); + LimboPlayer limboWithoutFly = newLimboWithAllowFlight(false); + Player player1 = mock(Player.class); + Player player2 = mock(Player.class); + + // when + AllowFlightRestoreType.ENABLE.restoreAllowFlight(player1, limboWithFly); + AllowFlightRestoreType.ENABLE.restoreAllowFlight(player2, limboWithoutFly); + + // then + verify(player1).setAllowFlight(true); + verify(player2).setAllowFlight(true); + } + + + @Test + public void shouldDisableFlight() { + // given + LimboPlayer limboWithFly = newLimboWithAllowFlight(true); + LimboPlayer limboWithoutFly = newLimboWithAllowFlight(false); + Player player1 = mock(Player.class); + Player player2 = mock(Player.class); + + // when + AllowFlightRestoreType.DISABLE.restoreAllowFlight(player1, limboWithFly); + AllowFlightRestoreType.DISABLE.restoreAllowFlight(player2, limboWithoutFly); + + // then + verify(player1).setAllowFlight(false); + verify(player2).setAllowFlight(false); + } + + private static LimboPlayer newLimboWithAllowFlight(boolean allowFlight) { + LimboPlayer limbo = mock(LimboPlayer.class); + given(limbo.isCanFly()).willReturn(allowFlight); + return limbo; + } +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboCacheTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboCacheTest.java deleted file mode 100644 index 8eb8e9598..000000000 --- a/src/test/java/fr/xephi/authme/data/limbo/LimboCacheTest.java +++ /dev/null @@ -1,230 +0,0 @@ -package fr.xephi.authme.data.limbo; - -import fr.xephi.authme.ReflectionTestUtils; -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 org.bukkit.Location; -import org.bukkit.entity.Player; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.Map; - -import static org.hamcrest.Matchers.aMapWithSize; -import static org.hamcrest.Matchers.anEmptyMap; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -/** - * Test for {@link LimboCache}. - */ -@RunWith(MockitoJUnitRunner.class) -public class LimboCacheTest { - - @InjectMocks - private LimboCache limboCache; - - @Mock - private Settings settings; - - @Mock - private PermissionsManager permissionsManager; - - @Mock - private SpawnLoader spawnLoader; - - @Mock - private LimboPlayerStorage limboPlayerStorage; - - @Test - public void shouldAddPlayerData() { - // given - Player player = mock(Player.class); - String name = "Bobby"; - given(player.getName()).willReturn(name); - Location location = mock(Location.class); - given(spawnLoader.getPlayerLocationOrSpawn(player)).willReturn(location); - given(player.isOp()).willReturn(true); - float walkSpeed = 2.1f; - given(player.getWalkSpeed()).willReturn(walkSpeed); - given(player.getAllowFlight()).willReturn(true); - float flySpeed = 3.0f; - given(player.getFlySpeed()).willReturn(flySpeed); - given(permissionsManager.hasGroupSupport()).willReturn(true); - String group = "test-group"; - given(permissionsManager.getPrimaryGroup(player)).willReturn(group); - given(limboPlayerStorage.hasData(player)).willReturn(false); - - // when - limboCache.addPlayerData(player); - - // then - LimboPlayer limboPlayer = limboCache.getPlayerData(name); - assertThat(limboPlayer.getLocation(), equalTo(location)); - assertThat(limboPlayer.isOperator(), equalTo(true)); - assertThat(limboPlayer.getWalkSpeed(), equalTo(walkSpeed)); - assertThat(limboPlayer.isCanFly(), equalTo(true)); - assertThat(limboPlayer.getFlySpeed(), equalTo(flySpeed)); - assertThat(limboPlayer.getGroup(), equalTo(group)); - } - - @Test - public void shouldGetPlayerDataFromDisk() { - // given - String name = "player01"; - Player player = mock(Player.class); - given(player.getName()).willReturn(name); - given(limboPlayerStorage.hasData(player)).willReturn(true); - LimboPlayer limboPlayer = mock(LimboPlayer.class); - given(limboPlayerStorage.readData(player)).willReturn(limboPlayer); - float walkSpeed = 2.4f; - given(limboPlayer.getWalkSpeed()).willReturn(walkSpeed); - given(limboPlayer.isCanFly()).willReturn(true); - float flySpeed = 1.0f; - given(limboPlayer.getFlySpeed()).willReturn(flySpeed); - String group = "primary-group"; - given(limboPlayer.getGroup()).willReturn(group); - - // when - limboCache.addPlayerData(player); - - // then - LimboPlayer result = limboCache.getPlayerData(name); - assertThat(result.getWalkSpeed(), equalTo(walkSpeed)); - assertThat(result.isCanFly(), equalTo(true)); - assertThat(result.getFlySpeed(), equalTo(flySpeed)); - assertThat(result.getGroup(), equalTo(group)); - } - - @Test - public void shouldRestorePlayerInfo() { - // given - String name = "Champ"; - Player player = mock(Player.class); - given(player.getName()).willReturn(name); - LimboPlayer limboPlayer = mock(LimboPlayer.class); - given(limboPlayer.isOperator()).willReturn(true); - float walkSpeed = 2.4f; - given(limboPlayer.getWalkSpeed()).willReturn(walkSpeed); - given(limboPlayer.isCanFly()).willReturn(true); - float flySpeed = 1.0f; - given(limboPlayer.getFlySpeed()).willReturn(flySpeed); - String group = "primary-group"; - given(limboPlayer.getGroup()).willReturn(group); - getCache().put(name.toLowerCase(), limboPlayer); - given(settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)).willReturn(true); - given(permissionsManager.hasGroupSupport()).willReturn(true); - - // when - limboCache.restoreData(player); - - // then - verify(player).setOp(true); - verify(player).setWalkSpeed(walkSpeed); - verify(player).setAllowFlight(true); - verify(player).setFlySpeed(flySpeed); - verify(permissionsManager).setGroup(player, group); - verify(limboPlayer).clearTasks(); - } - - @Test - public void shouldResetPlayerSpeed() { - // given - String name = "Champ"; - Player player = mock(Player.class); - given(player.getName()).willReturn(name); - LimboPlayer limboPlayer = mock(LimboPlayer.class); - given(limboPlayer.isOperator()).willReturn(true); - given(limboPlayer.getWalkSpeed()).willReturn(0f); - given(limboPlayer.isCanFly()).willReturn(true); - given(limboPlayer.getFlySpeed()).willReturn(0f); - String group = "primary-group"; - given(limboPlayer.getGroup()).willReturn(group); - getCache().put(name.toLowerCase(), limboPlayer); - given(settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)).willReturn(true); - given(permissionsManager.hasGroupSupport()).willReturn(true); - - // when - limboCache.restoreData(player); - - // then - verify(player).setWalkSpeed(0.2f); - verify(player).setFlySpeed(0.2f); - } - - @Test - public void shouldNotInteractWithPlayerIfNoDataAvailable() { - // given - String name = "player"; - Player player = mock(Player.class); - given(player.getName()).willReturn(name); - - // when - limboCache.restoreData(player); - - // then - verify(player).getName(); - verifyNoMoreInteractions(player); - } - - @Test - public void shouldRemoveAndClearTasks() { - // given - LimboPlayer limboPlayer = mock(LimboPlayer.class); - String name = "abcdef"; - getCache().put(name, limboPlayer); - Player player = mock(Player.class); - given(player.getName()).willReturn(name); - - // when - limboCache.removeFromCache(player); - - // then - assertThat(getCache(), anEmptyMap()); - verify(limboPlayer).clearTasks(); - } - - @Test - public void shouldDeleteFromCacheAndStorage() { - // given - LimboPlayer limboPlayer = mock(LimboPlayer.class); - String name = "SomeName"; - getCache().put(name.toLowerCase(), limboPlayer); - getCache().put("othername", mock(LimboPlayer.class)); - Player player = mock(Player.class); - given(player.getName()).willReturn(name); - - // when - limboCache.deletePlayerData(player); - - // then - assertThat(getCache(), aMapWithSize(1)); - verify(limboPlayer).clearTasks(); - verify(limboPlayerStorage).removeData(player); - } - - @Test - public void shouldReturnIfHasData() { - // given - String name = "tester"; - getCache().put(name, mock(LimboPlayer.class)); - - // when / then - assertThat(limboCache.hasPlayerData(name), equalTo(true)); - assertThat(limboCache.hasPlayerData("someone_else"), equalTo(false)); - } - - private Map getCache() { - return ReflectionTestUtils.getFieldValue(LimboCache.class, limboCache, "cache"); - } -} diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerMatchers.java b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerMatchers.java new file mode 100644 index 000000000..4c4793a34 --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerMatchers.java @@ -0,0 +1,112 @@ +package fr.xephi.authme.data.limbo; + +import org.bukkit.Location; +import org.bukkit.World; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import static java.lang.String.format; + +/** + * Contains matchers for LimboPlayer. + */ +public final class LimboPlayerMatchers { + + private LimboPlayerMatchers() { + } + + public static Matcher isLimbo(LimboPlayer limbo) { + return isLimbo(limbo.isOperator(), limbo.getGroup(), limbo.isCanFly(), + limbo.getWalkSpeed(), limbo.getFlySpeed()); + } + + public static Matcher isLimbo(boolean isOp, String group, boolean canFly, + float walkSpeed, float flySpeed) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(LimboPlayer item) { + return item.isOperator() == isOp && item.getGroup().equals(group) && item.isCanFly() == canFly + && walkSpeed == item.getWalkSpeed() && flySpeed == item.getFlySpeed(); + } + + @Override + public void describeTo(Description description) { + description.appendText(format("Limbo with isOp=%s, group=%s, canFly=%s, walkSpeed=%f, flySpeed=%f", + isOp, group, canFly, walkSpeed, flySpeed)); + } + + @Override + public void describeMismatchSafely(LimboPlayer item, Description description) { + description.appendText(format("Limbo with isOp=%s, group=%s, canFly=%s, walkSpeed=%f, flySpeed=%f", + item.isOperator(), item.getGroup(), item.isCanFly(), item.getWalkSpeed(), item.getFlySpeed())); + } + }; + } + + public static Matcher hasLocation(String world, double x, double y, double z) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(LimboPlayer item) { + Location location = item.getLocation(); + return location.getWorld().getName().equals(world) + && location.getX() == x && location.getY() == y && location.getZ() == z; + } + + @Override + public void describeTo(Description description) { + description.appendText(format("Limbo with location: world=%s, x=%f, y=%f, z=%f", + world, x, y, z)); + } + + @Override + public void describeMismatchSafely(LimboPlayer item, Description description) { + Location location = item.getLocation(); + if (location == null) { + description.appendText("Limbo with location = null"); + } else { + description.appendText(format("Limbo with location: world=%s, x=%f, y=%f, z=%f", + location.getWorld().getName(), location.getX(), location.getY(), location.getZ())); + } + } + }; + } + + public static Matcher hasLocation(World world, double x, double y, double z) { + return hasLocation(world.getName(), x, y, z); + } + + public static Matcher hasLocation(String world, double x, double y, double z, float yaw, float pitch) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(LimboPlayer item) { + Location location = item.getLocation(); + return hasLocation(location.getWorld(), location.getX(), location.getY(), location.getZ()).matches(item) + && location.getYaw() == yaw && location.getPitch() == pitch; + } + + @Override + public void describeTo(Description description) { + description.appendText(format("Limbo with location: world=%s, x=%f, y=%f, z=%f, yaw=%f, pitch=%f", + world, x, y, z, yaw, pitch)); + } + + @Override + public void describeMismatchSafely(LimboPlayer item, Description description) { + Location location = item.getLocation(); + if (location == null) { + description.appendText("Limbo with location = null"); + } else { + description.appendText(format("Limbo with location: world=%s, x=%f, y=%f, z=%f, yaw=%f, pitch=%f", + location.getWorld().getName(), location.getX(), location.getY(), location.getZ(), + location.getYaw(), location.getPitch())); + } + } + }; + } + + public static Matcher hasLocation(Location location) { + return hasLocation(location.getWorld().getName(), location.getX(), location.getY(), location.getZ(), + location.getYaw(), location.getPitch()); + } +} diff --git a/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java similarity index 65% rename from src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java rename to src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java index 51110d91e..8d3b6e85e 100644 --- a/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java @@ -1,15 +1,15 @@ -package fr.xephi.authme.task; +package fr.xephi.authme.data.limbo; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.data.limbo.LimboCache; -import fr.xephi.authme.data.limbo.LimboPlayer; 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 org.junit.BeforeClass; @@ -20,6 +20,11 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -46,9 +51,6 @@ public class LimboPlayerTaskManagerTest { @Mock private BukkitService bukkitService; - @Mock - private LimboCache limboCache; - @Mock private PlayerCache playerCache; @@ -60,16 +62,15 @@ public class LimboPlayerTaskManagerTest { @Test public void shouldRegisterMessageTask() { // given - String name = "bobby"; + Player player = mock(Player.class); LimboPlayer limboPlayer = mock(LimboPlayer.class); - given(limboCache.getPlayerData(name)).willReturn(limboPlayer); MessageKey key = MessageKey.REGISTER_MESSAGE; given(messages.retrieve(key)).willReturn(new String[]{"Please register!"}); int interval = 12; given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(interval); // when - limboPlayerTaskManager.registerMessageTask(name, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); // then verify(limboPlayer).setMessageTask(any(MessageTask.class)); @@ -78,32 +79,16 @@ public class LimboPlayerTaskManagerTest { any(MessageTask.class), eq(2L * TICKS_PER_SECOND), eq((long) interval * TICKS_PER_SECOND)); } - @Test - public void shouldNotScheduleTaskForMissingLimboPlayer() { - // given - String name = "ghost"; - given(limboCache.getPlayerData(name)).willReturn(null); - given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(5); - - // when - limboPlayerTaskManager.registerMessageTask(name, true); - - // then - verify(limboCache).getPlayerData(name); - verifyZeroInteractions(bukkitService); - verifyZeroInteractions(messages); - } - @Test public void shouldNotScheduleTaskForZeroAsInterval() { // given - String name = "Tester1"; + Player player = mock(Player.class); LimboPlayer limboPlayer = mock(LimboPlayer.class); given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(0); // when - limboPlayerTaskManager.registerMessageTask(name, true); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, true); // then verifyZeroInteractions(limboPlayer, bukkitService); @@ -112,19 +97,18 @@ public class LimboPlayerTaskManagerTest { @Test public void shouldCancelExistingMessageTask() { // given - LimboPlayer limboPlayer = mock(LimboPlayer.class); + Player player = mock(Player.class); + LimboPlayer limboPlayer = new LimboPlayer(null, true, "grp", false, 0.1f, 0.0f); MessageTask existingMessageTask = mock(MessageTask.class); - given(limboPlayer.getMessageTask()).willReturn(existingMessageTask); - - String name = "bobby"; - given(limboCache.getPlayerData(name)).willReturn(limboPlayer); + limboPlayer.setMessageTask(existingMessageTask); given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(8); // when - limboPlayerTaskManager.registerMessageTask(name, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); // then - verify(limboPlayer).setMessageTask(any(MessageTask.class)); + assertThat(limboPlayer.getMessageTask(), not(nullValue())); + assertThat(limboPlayer.getMessageTask(), not(sameInstance(existingMessageTask))); verify(messages).retrieve(MessageKey.REGISTER_MESSAGE); verify(existingMessageTask).cancel(); } @@ -132,17 +116,14 @@ public class LimboPlayerTaskManagerTest { @Test public void shouldRegisterTimeoutTask() { // given - String name = "l33tPlayer"; Player player = mock(Player.class); - given(player.getName()).willReturn(name); LimboPlayer limboPlayer = mock(LimboPlayer.class); - given(limboCache.getPlayerData(name)).willReturn(limboPlayer); given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(30); BukkitTask bukkitTask = mock(BukkitTask.class); given(bukkitService.runTaskLater(any(TimeoutTask.class), anyLong())).willReturn(bukkitTask); // when - limboPlayerTaskManager.registerTimeoutTask(player); + limboPlayerTaskManager.registerTimeoutTask(player, limboPlayer); // then verify(limboPlayer).setTimeoutTask(bukkitTask); @@ -150,22 +131,6 @@ public class LimboPlayerTaskManagerTest { verify(messages).retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR); } - @Test - public void shouldNotRegisterTimeoutTaskForMissingLimboPlayer() { - // given - String name = "Phantom_"; - Player player = mock(Player.class); - given(player.getName()).willReturn(name); - given(limboCache.getPlayerData(name)).willReturn(null); - given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(27); - - // when - limboPlayerTaskManager.registerTimeoutTask(player); - - // then - verifyZeroInteractions(bukkitService, messages); - } - @Test public void shouldNotRegisterTimeoutTaskForZeroTimeout() { // given @@ -174,7 +139,7 @@ public class LimboPlayerTaskManagerTest { given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(0); // when - limboPlayerTaskManager.registerTimeoutTask(player); + limboPlayerTaskManager.registerTimeoutTask(player, limboPlayer); // then verifyZeroInteractions(limboPlayer, bukkitService); @@ -183,23 +148,20 @@ public class LimboPlayerTaskManagerTest { @Test public void shouldCancelExistingTimeoutTask() { // given - String name = "l33tPlayer"; Player player = mock(Player.class); - given(player.getName()).willReturn(name); - LimboPlayer limboPlayer = mock(LimboPlayer.class); + LimboPlayer limboPlayer = new LimboPlayer(null, false, "", true, 0.3f, 0.1f); BukkitTask existingTask = mock(BukkitTask.class); - given(limboPlayer.getTimeoutTask()).willReturn(existingTask); - given(limboCache.getPlayerData(name)).willReturn(limboPlayer); + limboPlayer.setTimeoutTask(existingTask); given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(18); BukkitTask bukkitTask = mock(BukkitTask.class); given(bukkitService.runTaskLater(any(TimeoutTask.class), anyLong())).willReturn(bukkitTask); // when - limboPlayerTaskManager.registerTimeoutTask(player); + limboPlayerTaskManager.registerTimeoutTask(player, limboPlayer); // then verify(existingTask).cancel(); - verify(limboPlayer).setTimeoutTask(bukkitTask); + assertThat(limboPlayer.getTimeoutTask(), equalTo(bukkitTask)); verify(bukkitService).runTaskLater(any(TimeoutTask.class), eq(360L)); // 18 * TICKS_PER_SECOND verify(messages).retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR); } diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboServiceHelperTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceHelperTest.java new file mode 100644 index 000000000..43f110331 --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceHelperTest.java @@ -0,0 +1,81 @@ +package fr.xephi.authme.data.limbo; + +import org.bukkit.Location; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link LimboServiceHelper}. + *

    + * Note: some methods are tested directly where they are used via {@link LimboServiceTest}. + */ +@RunWith(MockitoJUnitRunner.class) +public class LimboServiceHelperTest { + + @InjectMocks + private LimboServiceHelper limboServiceHelper; + + @Test + public void shouldMergeLimboPlayers() { + // given + Location newLocation = mock(Location.class); + LimboPlayer newLimbo = new LimboPlayer(newLocation, false, "grp-new", false, 0.0f, 0.0f); + Location oldLocation = mock(Location.class); + LimboPlayer oldLimbo = new LimboPlayer(oldLocation, true, "grp-old", true, 0.1f, 0.8f); + + // when + LimboPlayer result = limboServiceHelper.merge(newLimbo, oldLimbo); + + // then + assertThat(result.getLocation(), equalTo(oldLocation)); + assertThat(result.isOperator(), equalTo(true)); + assertThat(result.getGroup(), equalTo("grp-old")); + assertThat(result.isCanFly(), equalTo(true)); + assertThat(result.getWalkSpeed(), equalTo(0.1f)); + assertThat(result.getFlySpeed(), equalTo(0.8f)); + } + + @Test + public void shouldFallBackToNewLimboForMissingData() { + // given + Location newLocation = mock(Location.class); + LimboPlayer newLimbo = new LimboPlayer(newLocation, false, "grp-new", true, 0.3f, 0.0f); + LimboPlayer oldLimbo = new LimboPlayer(null, false, "", false, 0.1f, 0.1f); + + // when + LimboPlayer result = limboServiceHelper.merge(newLimbo, oldLimbo); + + // then + assertThat(result.getLocation(), equalTo(newLocation)); + assertThat(result.isOperator(), equalTo(false)); + assertThat(result.getGroup(), equalTo("grp-new")); + assertThat(result.isCanFly(), equalTo(true)); + assertThat(result.getWalkSpeed(), equalTo(0.3f)); + assertThat(result.getFlySpeed(), equalTo(0.1f)); + } + + @Test + public void shouldHandleNullInputs() { + // given + LimboPlayer limbo = mock(LimboPlayer.class); + + // when + LimboPlayer result1 = limboServiceHelper.merge(limbo, null); + LimboPlayer result2 = limboServiceHelper.merge(null, limbo); + LimboPlayer result3 = limboServiceHelper.merge(null, null); + + // then + verifyZeroInteractions(limbo); + assertThat(result1, equalTo(limbo)); + assertThat(result2, equalTo(limbo)); + assertThat(result3, nullValue()); + } +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java new file mode 100644 index 000000000..740d7a6b8 --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java @@ -0,0 +1,237 @@ +package fr.xephi.authme.data.limbo; + +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.limbo.persistence.LimboPersistence; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.LimboSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link LimboService}, and {@link LimboServiceHelper}. + */ +@RunWith(DelayedInjectionRunner.class) +public class LimboServiceTest { + + @InjectDelayed + private LimboService limboService; + + @InjectDelayed + private LimboServiceHelper limboServiceHelper; + + @Mock + private SpawnLoader spawnLoader; + + @Mock + private PermissionsManager permissionsManager; + + @Mock + private Settings settings; + + @Mock + private LimboPlayerTaskManager taskManager; + + @Mock + private LimboPersistence limboPersistence; + + @BeforeClass + public static void initLogger() { + TestHelper.setupLogger(); + } + + @Before + public void mockSettings() { + given(settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)).willReturn(false); + } + + @Test + public void shouldCreateLimboPlayer() { + // given + Player player = newPlayer("Bobby", true, 0.3f, false, 0.2f); + Location playerLoc = mock(Location.class); + given(spawnLoader.getPlayerLocationOrSpawn(player)).willReturn(playerLoc); + given(permissionsManager.hasGroupSupport()).willReturn(true); + given(permissionsManager.getPrimaryGroup(player)).willReturn("permgrwp"); + + // when + limboService.createLimboPlayer(player, true); + + // then + verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(true)); + verify(taskManager).registerTimeoutTask(eq(player), any(LimboPlayer.class)); + verify(player).setAllowFlight(false); + verify(player).setFlySpeed(0.0f); + verify(player).setWalkSpeed(0.0f); + + assertThat(limboService.hasLimboPlayer("Bobby"), equalTo(true)); + LimboPlayer limbo = limboService.getLimboPlayer("Bobby"); + assertThat(limbo, not(nullValue())); + assertThat(limbo.isOperator(), equalTo(true)); + assertThat(limbo.getWalkSpeed(), equalTo(0.3f)); + assertThat(limbo.isCanFly(), equalTo(false)); + assertThat(limbo.getFlySpeed(), equalTo(0.2f)); + assertThat(limbo.getLocation(), equalTo(playerLoc)); + assertThat(limbo.getGroup(), equalTo("permgrwp")); + } + + @Test + public void shouldNotKeepOpStatusForUnregisteredPlayer() { + // given + Player player = newPlayer("CharleS", true, 0.1f, true, 0.4f); + Location playerLoc = mock(Location.class); + given(spawnLoader.getPlayerLocationOrSpawn(player)).willReturn(playerLoc); + given(permissionsManager.hasGroupSupport()).willReturn(false); + + // when + limboService.createLimboPlayer(player, false); + + // then + verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(false)); + verify(taskManager).registerTimeoutTask(eq(player), any(LimboPlayer.class)); + verify(permissionsManager, only()).hasGroupSupport(); + verify(player).setAllowFlight(false); + verify(player).setFlySpeed(0.0f); + verify(player).setWalkSpeed(0.0f); + + LimboPlayer limbo = limboService.getLimboPlayer("charles"); + assertThat(limbo, not(nullValue())); + assertThat(limbo.isOperator(), equalTo(false)); + assertThat(limbo.getWalkSpeed(), equalTo(0.1f)); + assertThat(limbo.isCanFly(), equalTo(true)); + assertThat(limbo.getFlySpeed(), equalTo(0.4f)); + assertThat(limbo.getLocation(), equalTo(playerLoc)); + assertThat(limbo.getGroup(), equalTo("")); + } + + @Test + public void shouldClearTasksOnAlreadyExistingLimbo() { + // given + LimboPlayer existingLimbo = mock(LimboPlayer.class); + getLimboMap().put("carlos", existingLimbo); + Player player = newPlayer("Carlos"); + + // when + limboService.createLimboPlayer(player, false); + + // then + verify(existingLimbo).clearTasks(); + LimboPlayer newLimbo = limboService.getLimboPlayer("Carlos"); + assertThat(newLimbo, not(nullValue())); + assertThat(newLimbo, not(sameInstance(existingLimbo))); + } + + @Test + public void shouldRestoreData() { + // given + LimboPlayer limbo = Mockito.spy(convertToLimboPlayer( + newPlayer("John", true, 0.4f, false, 0.0f), null, "")); + getLimboMap().put("john", limbo); + Player player = newPlayer("John", false, 0.2f, false, 0.7f); + + given(settings.getProperty(LimboSettings.RESTORE_ALLOW_FLIGHT)).willReturn(AllowFlightRestoreType.ENABLE); + given(settings.getProperty(LimboSettings.RESTORE_WALK_SPEED)).willReturn(WalkFlySpeedRestoreType.RESTORE); + given(settings.getProperty(LimboSettings.RESTORE_FLY_SPEED)).willReturn(WalkFlySpeedRestoreType.RESTORE_NO_ZERO); + + // when + limboService.restoreData(player); + + // then + verify(player).setOp(true); + verify(player).setWalkSpeed(0.4f); + verify(player).setAllowFlight(true); + verify(player).setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED); + verify(limbo).clearTasks(); + assertThat(limboService.hasLimboPlayer("John"), equalTo(false)); + } + + @Test + public void shouldHandleMissingLimboPlayerWhileRestoring() { + // given + Player player = newPlayer("Test"); + + // when + limboService.restoreData(player); + + // then + verify(player, only()).getName(); + } + + @Test + public void shouldReplaceTasks() { + // given + LimboPlayer limbo = mock(LimboPlayer.class); + getLimboMap().put("jeff", limbo); + Player player = newPlayer("JEFF"); + + + // when + limboService.replaceTasksAfterRegistration(player); + + // then + verify(taskManager).registerTimeoutTask(player, limbo); + verify(taskManager).registerMessageTask(player, limbo, true); + } + + @Test + public void shouldHandleMissingLimboForReplaceTasks() { + // given + Player player = newPlayer("ghost"); + + // when + limboService.replaceTasksAfterRegistration(player); + + // then + verifyZeroInteractions(taskManager); + } + + private static Player newPlayer(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } + + private static Player newPlayer(String name, boolean isOp, float walkSpeed, boolean canFly, float flySpeed) { + Player player = newPlayer(name); + given(player.isOp()).willReturn(isOp); + given(player.getWalkSpeed()).willReturn(walkSpeed); + given(player.getAllowFlight()).willReturn(canFly); + given(player.getFlySpeed()).willReturn(flySpeed); + return player; + } + + private static LimboPlayer convertToLimboPlayer(Player player, Location location, String group) { + return new LimboPlayer(location, player.isOp(), group, player.getAllowFlight(), + player.getWalkSpeed(), player.getFlySpeed()); + } + + private Map getLimboMap() { + return ReflectionTestUtils.getFieldValue(LimboService.class, limboService, "entries"); + } +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreTypeTest.java b/src/test/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreTypeTest.java new file mode 100644 index 000000000..f8aa2719c --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreTypeTest.java @@ -0,0 +1,108 @@ +package fr.xephi.authme.data.limbo; + +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static fr.xephi.authme.data.limbo.LimboPlayer.DEFAULT_FLY_SPEED; +import static fr.xephi.authme.data.limbo.LimboPlayer.DEFAULT_WALK_SPEED; +import static fr.xephi.authme.data.limbo.WalkFlySpeedRestoreType.DEFAULT; +import static fr.xephi.authme.data.limbo.WalkFlySpeedRestoreType.MAX_RESTORE; +import static fr.xephi.authme.data.limbo.WalkFlySpeedRestoreType.RESTORE; +import static fr.xephi.authme.data.limbo.WalkFlySpeedRestoreType.RESTORE_NO_ZERO; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link WalkFlySpeedRestoreType}. + */ +@RunWith(Parameterized.class) +public class WalkFlySpeedRestoreTypeTest { + + private final TestParameters parameters; + + public WalkFlySpeedRestoreTypeTest(TestParameters parameters) { + this.parameters = parameters; + } + + @Test + public void shouldRestoreToExpectedValue() { + // given + LimboPlayer limbo = mock(LimboPlayer.class); + given(limbo.getWalkSpeed()).willReturn(parameters.givenLimboWalkSpeed); + given(limbo.getFlySpeed()).willReturn(parameters.givenLimboFlySpeed); + + Player player = mock(Player.class); + given(player.getWalkSpeed()).willReturn(parameters.givenPlayerWalkSpeed); + given(player.getFlySpeed()).willReturn(parameters.givenPlayerFlySpeed); + + // when + parameters.testedType.restoreWalkSpeed(player, limbo); + parameters.testedType.restoreFlySpeed(player, limbo); + + // then + verify(player).setWalkSpeed(parameters.expectedWalkSpeed); + verify(player).setFlySpeed(parameters.expectedFlySpeed); + } + + @Parameterized.Parameters(name = "{0}") + public static List buildParams() { + List parameters = Arrays.asList( + create(RESTORE).withLimbo(0.1f, 0.4f).withPlayer(0.3f, 0.9f).expect(0.1f, 0.4f), + create(RESTORE).withLimbo(0.9f, 0.2f).withPlayer(0.3f, 0.0f).expect(0.9f, 0.2f), + create(MAX_RESTORE).withLimbo(0.3f, 0.8f).withPlayer(0.5f, 0.2f).expect(0.5f, 0.8f), + create(MAX_RESTORE).withLimbo(0.4f, 0.2f).withPlayer(0.1f, 0.4f).expect(0.4f, 0.4f), + create(RESTORE_NO_ZERO).withLimbo(0.1f, 0.2f).withPlayer(0.5f, 0.1f).expect(0.1f, 0.2f), + create(RESTORE_NO_ZERO).withLimbo(0.0f, 0.005f).withPlayer(0.4f, 0.8f).expect(DEFAULT_WALK_SPEED, DEFAULT_FLY_SPEED), + create(DEFAULT).withLimbo(0.1f, 0.7f).withPlayer(0.4f, 0.0f).expect(DEFAULT_WALK_SPEED, DEFAULT_FLY_SPEED) + ); + + // Convert List to List + return parameters.stream().map(p -> new Object[]{p}).collect(Collectors.toList()); + } + + private static TestParameters create(WalkFlySpeedRestoreType testedType) { + TestParameters params = new TestParameters(); + params.testedType = testedType; + return params; + } + + private static final class TestParameters { + private WalkFlySpeedRestoreType testedType; + private float givenLimboWalkSpeed; + private float givenLimboFlySpeed; + private float givenPlayerWalkSpeed; + private float givenPlayerFlySpeed; + private float expectedWalkSpeed; + private float expectedFlySpeed; + + TestParameters withLimbo(float walkSpeed, float flySpeed) { + this.givenLimboWalkSpeed = walkSpeed; + this.givenLimboFlySpeed = flySpeed; + return this; + } + + TestParameters withPlayer(float walkSpeed, float flySpeed) { + this.givenPlayerWalkSpeed = walkSpeed; + this.givenPlayerFlySpeed = flySpeed; + return this; + } + + TestParameters expect(float walkSpeed, float flySpeed) { + this.expectedWalkSpeed = walkSpeed; + this.expectedFlySpeed = flySpeed; + return this; + } + + @Override + public String toString() { + return testedType + " {" + expectedWalkSpeed + ", " + expectedFlySpeed + "}"; + } + } +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceTest.java b/src/test/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceTest.java new file mode 100644 index 000000000..d253869f7 --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceTest.java @@ -0,0 +1,158 @@ +package fr.xephi.authme.data.limbo.persistence; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.limbo.LimboPlayer; +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 org.hamcrest.Matcher; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.logging.Logger; + +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; + +/** + * Test for {@link LimboPersistence}. + */ +@RunWith(DelayedInjectionRunner.class) +public class LimboPersistenceTest { + + @InjectDelayed + private LimboPersistence limboPersistence; + + @Mock + private Factory handlerFactory; + + @Mock + private Settings settings; + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @BeforeInjecting + @SuppressWarnings("unchecked") + public void setUpMocks() { + given(settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE)).willReturn(LimboPersistenceType.DISABLED); + given(handlerFactory.newInstance(any(Class.class))) + .willAnswer(invocation -> mock(invocation.getArgument(0))); + } + + @Test + public void shouldInitializeProperly() { + // given / when / then + assertThat(getHandler(), instanceOf(NoOpPersistenceHandler.class)); + } + + @Test + public void shouldDelegateToHandler() { + // given + Player player = mock(Player.class); + LimboPersistenceHandler handler = getHandler(); + LimboPlayer limbo = mock(LimboPlayer.class); + given(handler.getLimboPlayer(player)).willReturn(limbo); + + // when + LimboPlayer result = limboPersistence.getLimboPlayer(player); + limboPersistence.saveLimboPlayer(player, mock(LimboPlayer.class)); + limboPersistence.removeLimboPlayer(mock(Player.class)); + + // then + assertThat(result, equalTo(limbo)); + verify(handler).getLimboPlayer(player); + verify(handler).saveLimboPlayer(eq(player), argThat(notNullAndDifferentFrom(limbo))); + verify(handler).removeLimboPlayer(argThat(notNullAndDifferentFrom(player))); + } + + @Test + public void shouldReloadProperly() { + // given + given(settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE)) + .willReturn(LimboPersistenceType.INDIVIDUAL_FILES); + + // when + limboPersistence.reload(settings); + + // then + assertThat(getHandler(), instanceOf(LimboPersistenceType.INDIVIDUAL_FILES.getImplementationClass())); + } + + @Test + public void shouldHandleExceptionWhenGettingLimbo() { + // given + Player player = mock(Player.class); + Logger logger = TestHelper.setupLogger(); + LimboPersistenceHandler handler = getHandler(); + doThrow(IllegalAccessException.class).when(handler).getLimboPlayer(player); + + // when + LimboPlayer result = limboPersistence.getLimboPlayer(player); + + // then + assertThat(result, nullValue()); + verify(logger).warning(argThat(containsString("[IllegalAccessException]"))); + } + + @Test + public void shouldHandleExceptionWhenSavingLimbo() { + // given + Player player = mock(Player.class); + LimboPlayer limbo = mock(LimboPlayer.class); + Logger logger = TestHelper.setupLogger(); + LimboPersistenceHandler handler = getHandler(); + doThrow(IllegalStateException.class).when(handler).saveLimboPlayer(player, limbo); + + // when + limboPersistence.saveLimboPlayer(player, limbo); + + // then + verify(logger).warning(argThat(containsString("[IllegalStateException]"))); + } + + @Test + public void shouldHandleExceptionWhenRemovingLimbo() { + // given + Player player = mock(Player.class); + Logger logger = TestHelper.setupLogger(); + LimboPersistenceHandler handler = getHandler(); + doThrow(UnsupportedOperationException.class).when(handler).removeLimboPlayer(player); + + // when + limboPersistence.removeLimboPlayer(player); + + // then + verify(logger).warning(argThat(containsString("[UnsupportedOperationException]"))); + } + + private LimboPersistenceHandler getHandler() { + return ReflectionTestUtils.getFieldValue(LimboPersistence.class, limboPersistence, "handler"); + } + + private static Matcher notNullAndDifferentFrom(T o) { + return both(not(sameInstance(o))).and(not(nullValue())); + } +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceTypeTest.java b/src/test/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceTypeTest.java new file mode 100644 index 000000000..248ab9c14 --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceTypeTest.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.data.limbo.persistence; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link LimboPersistenceType}. + */ +public class LimboPersistenceTypeTest { + + @Test + public void shouldHaveUniqueImplementationClasses() { + // given + Set> classes = new HashSet<>(); + + // when / then + for (LimboPersistenceType persistenceType : LimboPersistenceType.values()) { + if (!classes.add(persistenceType.getImplementationClass())) { + fail("Implementation class '" + persistenceType.getImplementationClass() + "' from '" + + persistenceType + "' already encountered previously"); + } + } + } + + @Test + public void shouldHaveTypeReturnedFromImplementationClass() { + for (LimboPersistenceType persistenceType : LimboPersistenceType.values()) { + // given + LimboPersistenceHandler implementationMock = mock(persistenceType.getImplementationClass()); + given(implementationMock.getType()).willCallRealMethod(); + + // when + LimboPersistenceType returnedType = implementationMock.getType(); + + // then + assertThat(returnedType, equalTo(persistenceType)); + } + } + +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentConfigurationTest.java b/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentConfigurationTest.java new file mode 100644 index 000000000..34d23723d --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentConfigurationTest.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.fail; + +/** + * Test for {@link SegmentConfiguration}. + */ +public class SegmentConfigurationTest { + + @Test + public void shouldHaveDistributionThatIsPowerOf2() { + // given + Set allowedDistributions = ImmutableSet.of(1, 2, 4, 8, 16); + + // when / then + for (SegmentConfiguration entry : SegmentConfiguration.values()) { + if (!allowedDistributions.contains(entry.getDistribution())) { + fail("Distribution must be a power of 2 and within [1, 16]. Offending item: " + entry); + } + } + } + + @Test + public void shouldHaveDifferentSegmentSizes() { + // given + Set segmentTotals = new HashSet<>(); + + // when / then + for (SegmentConfiguration entry : SegmentConfiguration.values()) { + int totalSegments = entry.getTotalSegments(); + assertThat(entry + " must have a positive segment size", + totalSegments, greaterThan(0)); + + assertThat(entry + " has a segment total that was already encountered (" + totalSegments + ")", + segmentTotals.add(totalSegments), equalTo(true)); + } + } +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolderTest.java b/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolderTest.java new file mode 100644 index 000000000..064fc9c3d --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolderTest.java @@ -0,0 +1,205 @@ +package fr.xephi.authme.data.limbo.persistence; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import com.google.common.io.Files; +import fr.xephi.authme.TestHelper; +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 org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.hamcrest.Matcher; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +import static fr.xephi.authme.data.limbo.LimboPlayerMatchers.hasLocation; +import static fr.xephi.authme.data.limbo.LimboPlayerMatchers.isLimbo; +import static java.util.UUID.fromString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link SegmentFilesPersistenceHolder}. + */ +@RunWith(DelayedInjectionRunner.class) +public class SegmentFilesPersistenceHolderTest { + + /** Player is in seg32-10110 and should be migrated into seg16-f. */ + private static final UUID MIGRATED_UUID = fromString("f6a97c88-7c8f-c12e-4931-6206d4ca067d"); + private static final Matcher MIGRATED_LIMBO_MATCHER = + isLimbo(false, "noob", true, 0.2f, 0.1f); + + /** Existing player in seg16-f. */ + private static final UUID UUID_FAB69 = fromString("fab69c88-2cd0-1fed-f00d-dead14ca067d"); + private static final Matcher FAB69_MATCHER = + isLimbo(false, "", false, 0.2f, 0.1f); + + /** Player in seg16-8. */ + private static final UUID UUID_STAFF = fromString("88897c88-7c8f-c12e-4931-6206d4ca067d"); + private static final Matcher STAFF_MATCHER = + isLimbo(true, "staff", false, 0.3f, 0.1f); + + /** Player in seg16-8. */ + private static final UUID UUID_8C679 = fromString("8c679491-1234-abcd-9102-1fa6e0cc3f81"); + private static final Matcher SC679_MATCHER = + isLimbo(false, "primary", true, 0.1f, 0.0f); + + /** UUID for which no data is stored (belongs to a segment file that does not exist, seg16-4). */ + private static final UUID UNKNOWN_UUID = fromString("42d1cc0b-8f12-d04a-e7ba-a067d05cdc39"); + + /** UUID for which no data is stored (belongs to an existing segment file: seg16-8). */ + private static final UUID UNKNOWN_UUID2 = fromString("84d1cc0b-8f12-d04a-e7ba-a067d05cdc39"); + + + @InjectDelayed + private SegmentFilesPersistenceHolder persistenceHandler; + + @Mock + private Settings settings; + @Mock + private BukkitService bukkitService; + @DataFolder + private File dataFolder; + private File playerDataFolder; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void initLogger() { + TestHelper.setupLogger(); + } + + @BeforeInjecting + public void setUpClasses() throws IOException { + given(settings.getProperty(LimboSettings.SEGMENT_DISTRIBUTION)).willReturn(SegmentConfiguration.SIXTEEN); + dataFolder = temporaryFolder.newFolder(); + playerDataFolder = new File(dataFolder, "playerdata"); + playerDataFolder.mkdir(); + + File limboFilesFolder = new File("src/test/resources/fr/xephi/authme/data/limbo"); + for (File file : limboFilesFolder.listFiles()) { + File from = new File(playerDataFolder, file.getName()); + Files.copy(file, from); + } + + given(bukkitService.getWorld(anyString())) + .willAnswer(invocation -> { + World world = mock(World.class); + given(world.getName()).willReturn(invocation.getArgument(0)); + return world; + }); + } + + // Note ljacqu 20170314: These tests are a little slow to set up; therefore we sometimes + // test things in one test that would traditionally belong into two separate tests + + @Test + public void shouldMigrateOldSegmentFilesOnStartup() { + // Ensure that only the files of the current segmenting scheme remain + assertThat(playerDataFolder.list(), arrayContainingInAnyOrder("seg16-8-limbo.json", "seg16-f-limbo.json")); + + // Check that the expected limbo players can be read + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(MIGRATED_UUID)), MIGRATED_LIMBO_MATCHER); + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UUID_FAB69)), FAB69_MATCHER); + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UUID_STAFF)), STAFF_MATCHER); + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UUID_8C679)), SC679_MATCHER); + + // Check that unknown players are null (whose segment file exists and does not exist) + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UNKNOWN_UUID)), nullValue()); + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UNKNOWN_UUID2)), nullValue()); + } + + @Test + public void shouldRemovePlayer() { + // given + Player playerToRemove = mockPlayerWithUuid(UUID_STAFF); + Player unknownPlayerToRemove = mockPlayerWithUuid(UNKNOWN_UUID); + + // when + persistenceHandler.removeLimboPlayer(playerToRemove); + persistenceHandler.removeLimboPlayer(unknownPlayerToRemove); + + // then + assertThat(persistenceHandler.getLimboPlayer(playerToRemove), nullValue()); + assertThat(persistenceHandler.getLimboPlayer(unknownPlayerToRemove), nullValue()); + // Player in same segment should still exist... + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UUID_8C679)), SC679_MATCHER); + + // Check that we didn't create seg16-4 by deleting UNKNOWN_UUID. + assertThat(playerDataFolder.list(), arrayContainingInAnyOrder("seg16-8-limbo.json", "seg16-f-limbo.json")); + } + + @Test + public void shouldAddPlayer() { + // given + Player uuidToAdd1 = mockPlayerWithUuid(UNKNOWN_UUID); + Location location1 = new Location(mockWorldWithName("1world"), 120, 60, -80, 0.42345f, 120.32f); + LimboPlayer limbo1 = new LimboPlayer(location1, false, "group-1", true, 0.1f, 0.2f); + Player uuidToAdd2 = mockPlayerWithUuid(UNKNOWN_UUID2); + Location location2 = new Location(mockWorldWithName("2world"), -40, 20, 33, 4.235f, 8.32299f); + LimboPlayer limbo2 = new LimboPlayer(location2, true, "", false, 0.0f, 0.25f); + + // when + persistenceHandler.saveLimboPlayer(uuidToAdd1, limbo1); + persistenceHandler.saveLimboPlayer(uuidToAdd2, limbo2); + + // then + LimboPlayer addedPlayer1 = persistenceHandler.getLimboPlayer(uuidToAdd1); + assertThat(addedPlayer1, isLimbo(limbo1)); + assertThat(addedPlayer1, hasLocation(location1)); + LimboPlayer addedPlayer2 = persistenceHandler.getLimboPlayer(uuidToAdd2); + assertThat(addedPlayer2, isLimbo(limbo2)); + assertThat(addedPlayer2, hasLocation(location2)); + + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(MIGRATED_UUID)), MIGRATED_LIMBO_MATCHER); + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UUID_FAB69)), FAB69_MATCHER); + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UUID_STAFF)), STAFF_MATCHER); + assertThat(persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UUID_8C679)), SC679_MATCHER); + } + + @Test + public void shouldHandleReadErrorGracefully() throws IOException { + // given + // assumption + File invalidFile = new File(playerDataFolder, "seg16-4-limbo.json"); + assertThat(invalidFile.exists(), equalTo(false)); + Files.write("not valid json".getBytes(), invalidFile); + + // when + LimboPlayer result = persistenceHandler.getLimboPlayer(mockPlayerWithUuid(UNKNOWN_UUID)); + + // then + assertThat(result, nullValue()); + } + + private static Player mockPlayerWithUuid(UUID uuid) { + Player player = mock(Player.class); + given(player.getUniqueId()).willReturn(uuid); + return player; + } + + private static World mockWorldWithName(String name) { + World world = mock(World.class); + given(world.getName()).willReturn(name); + return world; + } +} diff --git a/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilderTest.java b/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilderTest.java new file mode 100644 index 000000000..64843db85 --- /dev/null +++ b/src/test/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilderTest.java @@ -0,0 +1,141 @@ +package fr.xephi.authme.data.limbo.persistence; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static fr.xephi.authme.data.limbo.persistence.SegmentConfiguration.EIGHT; +import static fr.xephi.authme.data.limbo.persistence.SegmentConfiguration.FOUR; +import static fr.xephi.authme.data.limbo.persistence.SegmentConfiguration.ONE; +import static fr.xephi.authme.data.limbo.persistence.SegmentConfiguration.SIXTEEN; +import static fr.xephi.authme.data.limbo.persistence.SegmentConfiguration.SIXTY_FOUR; +import static fr.xephi.authme.data.limbo.persistence.SegmentConfiguration.THIRTY_TWO; +import static fr.xephi.authme.data.limbo.persistence.SegmentConfiguration.TWO_FIFTY; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link SegmentNameBuilder}. + */ +public class SegmentNameBuilderTest { + + /** + * Checks that using a given segment size really produces as many segments as defined. + * E.g. if we partition with {@link SegmentConfiguration#EIGHT} we expect eight different buckets. + */ + @Test + public void shouldCreatePromisedSizeOfSegments() { + for (SegmentConfiguration part : SegmentConfiguration.values()) { + // Perform this check only for `length` <= 5 because the test creates all hex numbers with `length` digits. + if (part.getLength() <= 5) { + checkTotalSegmentsProduced(part); + } + } + } + + private void checkTotalSegmentsProduced(SegmentConfiguration part) { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(part); + Set encounteredSegments = new HashSet<>(); + int shift = part.getLength() * 4; + // e.g. (1 << 16) - 1 = 0xFFFF. (Number of digits = shift/4, since 16 = 2^4) + int max = (1 << shift) - 1; + + // when + for (int i = 0; i <= max; ++i) { + String uuid = toPaddedHex(i, part.getLength()); + encounteredSegments.add(nameBuilder.createSegmentName(uuid)); + } + + // then + assertThat(encounteredSegments, hasSize(part.getTotalSegments())); + } + + private static String toPaddedHex(int dec, int padLength) { + String hexResult = Integer.toString(dec, 16); + while (hexResult.length() < padLength) { + hexResult = "0" + hexResult; + } + return hexResult; + } + + @Test + public void shouldCreateOneSegment() { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(ONE); + + // when / then + assertThat(nameBuilder.createSegmentName("abc"), equalTo("seg1-0")); + assertThat(nameBuilder.createSegmentName("f0e"), equalTo("seg1-0")); + assertThat(nameBuilder.createSegmentName("329"), equalTo("seg1-0")); + } + + @Test + public void shouldCreateFourSegments() { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(FOUR); + + // when / then + assertThat(nameBuilder.createSegmentName("f9cc"), equalTo("seg4-3")); + assertThat(nameBuilder.createSegmentName("84c9"), equalTo("seg4-2")); + assertThat(nameBuilder.createSegmentName("3799"), equalTo("seg4-0")); + } + + @Test + public void shouldCreateEightSegments() { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(EIGHT); + + // when / then + assertThat(nameBuilder.createSegmentName("fc9c"), equalTo("seg8-7")); + assertThat(nameBuilder.createSegmentName("90ad"), equalTo("seg8-4")); + assertThat(nameBuilder.createSegmentName("35e4"), equalTo("seg8-1")); + assertThat(nameBuilder.createSegmentName("a39f"), equalTo("seg8-5")); + } + + @Test + public void shouldCreateSixteenSegments() { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(SIXTEEN); + + // when / then + assertThat(nameBuilder.createSegmentName("fc9a054"), equalTo("seg16-f")); + assertThat(nameBuilder.createSegmentName("b0a945e"), equalTo("seg16-b")); + assertThat(nameBuilder.createSegmentName("7afebab"), equalTo("seg16-7")); + } + + @Test + public void shouldCreateThirtyTwoSegments() { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(THIRTY_TWO); + + // when / then + assertThat(nameBuilder.createSegmentName("f890c9"), equalTo("seg32-11101")); + assertThat(nameBuilder.createSegmentName("49c39a"), equalTo("seg32-01101")); + assertThat(nameBuilder.createSegmentName("b75d09"), equalTo("seg32-10010")); + } + + @Test + public void shouldCreateSixtyFourSegments() { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(SIXTY_FOUR); + + // when / then + assertThat(nameBuilder.createSegmentName("82f"), equalTo("seg64-203")); + assertThat(nameBuilder.createSegmentName("9b4"), equalTo("seg64-221")); + assertThat(nameBuilder.createSegmentName("068"), equalTo("seg64-012")); + } + + @Test + public void shouldCreate256Segments() { + // given + SegmentNameBuilder nameBuilder = new SegmentNameBuilder(TWO_FIFTY); + + // when / then + assertThat(nameBuilder.createSegmentName("a813c"), equalTo("seg256-a8")); + assertThat(nameBuilder.createSegmentName("b4d01"), equalTo("seg256-b4")); + assertThat(nameBuilder.createSegmentName("7122f"), equalTo("seg256-71")); + } +} diff --git a/src/test/java/fr/xephi/authme/data/backup/LimboPlayerStorageTest.java b/src/test/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandlerTest.java similarity index 72% rename from src/test/java/fr/xephi/authme/data/backup/LimboPlayerStorageTest.java rename to src/test/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandlerTest.java index f9c5880ca..a4e7dc93f 100644 --- a/src/test/java/fr/xephi/authme/data/backup/LimboPlayerStorageTest.java +++ b/src/test/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandlerTest.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.data.backup; +package fr.xephi.authme.data.limbo.persistence; import ch.jalu.injector.testing.BeforeInjecting; import ch.jalu.injector.testing.DelayedInjectionRunner; @@ -6,8 +6,6 @@ import ch.jalu.injector.testing.InjectDelayed; import fr.xephi.authme.TestHelper; 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 org.bukkit.Location; @@ -32,26 +30,20 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Test for {@link LimboPlayerStorage}. + * Test for {@link SeparateFilePersistenceHandler}. */ @RunWith(DelayedInjectionRunner.class) -public class LimboPlayerStorageTest { +public class SeparateFilePersistenceHandlerTest { - private static final UUID SAMPLE_UUID = UUID.nameUUIDFromBytes("PlayerDataStorageTest".getBytes()); + private static final UUID SAMPLE_UUID = UUID.nameUUIDFromBytes("PersistenceTest".getBytes()); private static final String SOURCE_FOLDER = TestHelper.PROJECT_ROOT + "data/backup/"; @InjectDelayed - private LimboPlayerStorage limboPlayerStorage; - - @Mock - private SpawnLoader spawnLoader; + private SeparateFilePersistenceHandler handler; @Mock private BukkitService bukkitService; - @Mock - private PermissionsManager permissionsManager; - @DataFolder private File dataFolder; @@ -78,7 +70,7 @@ public class LimboPlayerStorageTest { given(bukkitService.getWorld("nether")).willReturn(world); // when - LimboPlayer data = limboPlayerStorage.readData(player); + LimboPlayer data = handler.getLimboPlayer(player); // then assertThat(data, not(nullValue())); @@ -103,44 +95,28 @@ public class LimboPlayerStorageTest { given(player.getUniqueId()).willReturn(UUID.nameUUIDFromBytes("other-player".getBytes())); // when - LimboPlayer data = limboPlayerStorage.readData(player); + LimboPlayer data = handler.getLimboPlayer(player); // then assertThat(data, nullValue()); } - @Test - public void shouldReturnIfHasData() { - // given - Player player1 = mock(Player.class); - given(player1.getUniqueId()).willReturn(SAMPLE_UUID); - Player player2 = mock(Player.class); - given(player2.getUniqueId()).willReturn(UUID.nameUUIDFromBytes("not-stored".getBytes())); - - // when / then - assertThat(limboPlayerStorage.hasData(player1), equalTo(true)); - assertThat(limboPlayerStorage.hasData(player2), equalTo(false)); - } - @Test public void shouldSavePlayerData() { // given Player player = mock(Player.class); UUID uuid = UUID.nameUUIDFromBytes("New player".getBytes()); given(player.getUniqueId()).willReturn(uuid); - given(permissionsManager.getPrimaryGroup(player)).willReturn("primary-grp"); - given(player.isOp()).willReturn(true); - given(player.getWalkSpeed()).willReturn(1.2f); - given(player.getFlySpeed()).willReturn(0.8f); - given(player.getAllowFlight()).willReturn(true); + World world = mock(World.class); given(world.getName()).willReturn("player-world"); Location location = new Location(world, 0.2, 102.25, -89.28, 3.02f, 90.13f); - given(spawnLoader.getPlayerLocationOrSpawn(player)).willReturn(location); + String group = "primary-grp"; + LimboPlayer limbo = new LimboPlayer(location, true, group, true, 1.2f, 0.8f); // when - limboPlayerStorage.saveData(player); + handler.saveLimboPlayer(player, limbo); // then File playerFile = new File(dataFolder, FileUtils.makePath("playerdata", uuid.toString(), "data.json")); diff --git a/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java index 7e0bb1a51..102584edf 100644 --- a/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java @@ -61,13 +61,13 @@ public class FlatFileIntegrationTest { // then assertThat(authList, hasSize(7)); - assertThat(getName("bobby", authList), hasAuthBasicData("bobby", "Bobby", "your@email.com", "123.45.67.89")); + assertThat(getName("bobby", authList), hasAuthBasicData("bobby", "bobby", "your@email.com", "123.45.67.89")); assertThat(getName("bobby", authList), hasAuthLocation(1.05, 2.1, 4.2, "world")); assertThat(getName("bobby", authList).getPassword(), equalToHash("$SHA$11aa0706173d7272$dbba966")); - assertThat(getName("twofields", authList), hasAuthBasicData("twofields", "twoFields", "your@email.com", "127.0.0.1")); + assertThat(getName("twofields", authList), hasAuthBasicData("twofields", "twofields", "your@email.com", "127.0.0.1")); assertThat(getName("twofields", authList).getPassword(), equalToHash("hash1234")); - assertThat(getName("threefields", authList), hasAuthBasicData("threefields", "threeFields", "your@email.com", "33.33.33.33")); - assertThat(getName("fourfields", authList), hasAuthBasicData("fourfields", "fourFields", "your@email.com", "4.4.4.4")); + assertThat(getName("threefields", authList), hasAuthBasicData("threefields", "threefields", "your@email.com", "33.33.33.33")); + assertThat(getName("fourfields", authList), hasAuthBasicData("fourfields", "fourfields", "your@email.com", "4.4.4.4")); assertThat(getName("fourfields", authList).getLastLogin(), equalTo(404040404L)); assertThat(getName("sevenfields", authList), hasAuthLocation(7.7, 14.14, 21.21, "world")); assertThat(getName("eightfields", authList), hasAuthLocation(8.8, 17.6, 26.4, "eightworld")); diff --git a/src/test/java/fr/xephi/authme/datasource/converter/ForceFlatToSqliteTest.java b/src/test/java/fr/xephi/authme/datasource/converter/ForceFlatToSqliteTest.java index 3ec28a7b6..556f58ef9 100644 --- a/src/test/java/fr/xephi/authme/datasource/converter/ForceFlatToSqliteTest.java +++ b/src/test/java/fr/xephi/authme/datasource/converter/ForceFlatToSqliteTest.java @@ -63,11 +63,11 @@ public class ForceFlatToSqliteTest { ArgumentCaptor authCaptor = ArgumentCaptor.forClass(PlayerAuth.class); verify(dataSource, times(7)).saveAuth(authCaptor.capture()); List auths = authCaptor.getAllValues(); - assertThat(auths, hasItem(hasAuthBasicData("bobby", "Bobby", "your@email.com", "123.45.67.89"))); + assertThat(auths, hasItem(hasAuthBasicData("bobby", "Player", "your@email.com", "123.45.67.89"))); assertThat(auths, hasItem(hasAuthLocation(1.05, 2.1, 4.2, "world"))); - assertThat(auths, hasItem(hasAuthBasicData("user", "user", "user@example.org", "34.56.78.90"))); + assertThat(auths, hasItem(hasAuthBasicData("user", "Player", "user@example.org", "34.56.78.90"))); assertThat(auths, hasItem(hasAuthLocation(124.1, 76.3, -127.8, "nether"))); - assertThat(auths, hasItem(hasAuthBasicData("eightfields", "eightFields", "your@email.com", "6.6.6.66"))); + assertThat(auths, hasItem(hasAuthBasicData("eightfields", "Player", "your@email.com", "6.6.6.66"))); assertThat(auths, hasItem(hasAuthLocation(8.8, 17.6, 26.4, "eightworld"))); } diff --git a/src/test/java/fr/xephi/authme/initialization/OnStartupTasksTest.java b/src/test/java/fr/xephi/authme/initialization/OnStartupTasksTest.java new file mode 100644 index 000000000..4a95a1029 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/OnStartupTasksTest.java @@ -0,0 +1,76 @@ +package fr.xephi.authme.initialization; + +import ch.jalu.injector.exceptions.InjectorReflectionException; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.security.HashAlgorithm; +import org.junit.Test; + +import java.util.logging.Logger; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link OnStartupTasks}. + */ +public class OnStartupTasksTest { + + @Test + public void shouldDisplayLegacyJarHint() { + // given + Logger logger = TestHelper.setupLogger(); + NoClassDefFoundError noClassDefError = new NoClassDefFoundError("Lcom/google/gson/Gson;"); + ReflectiveOperationException ex2 = new ReflectiveOperationException("", noClassDefError); + InjectorReflectionException ex = new InjectorReflectionException("", ex2); + + // when + OnStartupTasks.displayLegacyJarHint(ex); + + // then + verify(logger).warning("YOU MUST DOWNLOAD THE LEGACY JAR TO USE AUTHME ON YOUR SERVER"); + } + + @Test + public void shouldNotDisplayLegacyHintForDifferentException() { + // given + Logger logger = TestHelper.setupLogger(); + NullPointerException npe = new NullPointerException(); + + // when + OnStartupTasks.displayLegacyJarHint(npe); + + // then + verifyZeroInteractions(logger); + } + + @Test + public void shouldNotDisplayLegacyHintForWrongCause() { + // given + Logger logger = TestHelper.setupLogger(); + IllegalAccessException illegalAccessException = new IllegalAccessException("Lcom/google/gson/Gson;"); + ReflectiveOperationException ex2 = new ReflectiveOperationException("", illegalAccessException); + InjectorReflectionException ex = new InjectorReflectionException("", ex2); + + // when + OnStartupTasks.displayLegacyJarHint(ex); + + // then + verifyZeroInteractions(logger); + } + + @Test + public void shouldCheckIfHashIsDeprecatedIn54() { + // given / when / then + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.CUSTOM), equalTo(false)); + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.IPB3), equalTo(false)); + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.PLAINTEXT), equalTo(false)); + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.SHA256), equalTo(false)); + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.WORDPRESS), equalTo(false)); + + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.MD5), equalTo(true)); + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.SHA512), equalTo(true)); + assertThat(OnStartupTasks.isHashDeprecatedIn54(HashAlgorithm.WHIRLPOOL), equalTo(true)); + } +} diff --git a/src/test/java/fr/xephi/authme/mail/EmailServiceTest.java b/src/test/java/fr/xephi/authme/mail/EmailServiceTest.java new file mode 100644 index 000000000..43cbaa61a --- /dev/null +++ b/src/test/java/fr/xephi/authme/mail/EmailServiceTest.java @@ -0,0 +1,190 @@ +package fr.xephi.authme.mail; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.TestHelper; +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 org.apache.commons.mail.EmailException; +import org.apache.commons.mail.HtmlEmail; +import org.bukkit.Server; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link EmailService}. + */ +@RunWith(DelayedInjectionRunner.class) +public class EmailServiceTest { + + @InjectDelayed + private EmailService emailService; + + @Mock + private Settings settings; + @Mock + private Server server; + @Mock + private SendMailSsl sendMailSsl; + @DataFolder + private File dataFolder; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void initLogger() { + TestHelper.setupLogger(); + } + + @BeforeInjecting + public void initFields() throws IOException { + dataFolder = temporaryFolder.newFolder(); + given(server.getServerName()).willReturn("serverName"); + given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("mail@example.org"); + given(settings.getProperty(EmailSettings.MAIL_PASSWORD)).willReturn("pass1234"); + given(sendMailSsl.hasAllInformation()).willReturn(true); + } + + @Test + public void shouldHaveAllInformation() { + // given / when / then + assertThat(emailService.hasAllInformation(), equalTo(true)); + } + + @Test + public void shouldSendPasswordMail() throws EmailException { + // given + given(settings.getPasswordEmailMessage()) + .willReturn("Hi , your new password for is "); + given(settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)).willReturn(false); + HtmlEmail email = mock(HtmlEmail.class); + given(sendMailSsl.initializeMail(anyString())).willReturn(email); + given(sendMailSsl.sendEmail(anyString(), eq(email))).willReturn(true); + + // when + boolean result = emailService.sendPasswordMail("Player", "user@example.com", "new_password"); + + // then + assertThat(result, equalTo(true)); + verify(sendMailSsl).initializeMail("user@example.com"); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(sendMailSsl).sendEmail(messageCaptor.capture(), eq(email)); + assertThat(messageCaptor.getValue(), + equalTo("Hi Player, your new password for serverName is new_password")); + } + + @Test + public void shouldHandleMailCreationError() throws EmailException { + // given + doThrow(EmailException.class).when(sendMailSsl).initializeMail(anyString()); + + // when + boolean result = emailService.sendPasswordMail("Player", "user@example.com", "new_password"); + + // then + assertThat(result, equalTo(false)); + verify(sendMailSsl).initializeMail("user@example.com"); + verify(sendMailSsl, never()).sendEmail(anyString(), any(HtmlEmail.class)); + } + + @Test + public void shouldHandleMailSendingFailure() throws EmailException { + // given + given(settings.getPasswordEmailMessage()).willReturn("Hi , your new pass is "); + given(settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)).willReturn(false); + HtmlEmail email = mock(HtmlEmail.class); + given(sendMailSsl.initializeMail(anyString())).willReturn(email); + given(sendMailSsl.sendEmail(anyString(), any(HtmlEmail.class))).willReturn(false); + + // when + boolean result = emailService.sendPasswordMail("bobby", "user@example.com", "myPassw0rd"); + + // then + assertThat(result, equalTo(false)); + verify(sendMailSsl).initializeMail("user@example.com"); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(sendMailSsl).sendEmail(messageCaptor.capture(), eq(email)); + assertThat(messageCaptor.getValue(), equalTo("Hi bobby, your new pass is myPassw0rd")); + } + + @Test + public void shouldSendRecoveryCode() throws EmailException { + // given + given(settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID)).willReturn(7); + given(settings.getRecoveryCodeEmailMessage()) + .willReturn("Hi , your code on is (valid hours)"); + HtmlEmail email = mock(HtmlEmail.class); + given(sendMailSsl.initializeMail(anyString())).willReturn(email); + given(sendMailSsl.sendEmail(anyString(), any(HtmlEmail.class))).willReturn(true); + + // when + boolean result = emailService.sendRecoveryCode("Timmy", "tim@example.com", "12C56A"); + + // then + assertThat(result, equalTo(true)); + verify(sendMailSsl).initializeMail("tim@example.com"); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(sendMailSsl).sendEmail(messageCaptor.capture(), eq(email)); + assertThat(messageCaptor.getValue(), equalTo("Hi Timmy, your code on serverName is 12C56A (valid 7 hours)")); + } + + @Test + public void shouldHandleMailCreationErrorForRecoveryCode() throws EmailException { + // given + given(sendMailSsl.initializeMail(anyString())).willThrow(EmailException.class); + + // when + boolean result = emailService.sendRecoveryCode("Player", "player@example.org", "ABC1234"); + + // then + assertThat(result, equalTo(false)); + verify(sendMailSsl).initializeMail("player@example.org"); + verify(sendMailSsl, never()).sendEmail(anyString(), any(HtmlEmail.class)); + } + + @Test + public void shouldHandleFailureToSendRecoveryCode() throws EmailException { + // given + given(settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID)).willReturn(7); + given(settings.getRecoveryCodeEmailMessage()).willReturn("Hi , your code is "); + EmailService sendMailSpy = spy(emailService); + HtmlEmail email = mock(HtmlEmail.class); + given(sendMailSsl.initializeMail(anyString())).willReturn(email); + given(sendMailSsl.sendEmail(anyString(), any(HtmlEmail.class))).willReturn(false); + + // when + boolean result = sendMailSpy.sendRecoveryCode("John", "user@example.com", "1DEF77"); + + // then + assertThat(result, equalTo(false)); + verify(sendMailSsl).initializeMail("user@example.com"); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(sendMailSsl).sendEmail(messageCaptor.capture(), eq(email)); + assertThat(messageCaptor.getValue(), equalTo("Hi John, your code is 1DEF77")); + } + +} diff --git a/src/test/java/fr/xephi/authme/mail/SendMailSSLTest.java b/src/test/java/fr/xephi/authme/mail/SendMailSSLTest.java deleted file mode 100644 index 027aea57d..000000000 --- a/src/test/java/fr/xephi/authme/mail/SendMailSSLTest.java +++ /dev/null @@ -1,274 +0,0 @@ -package fr.xephi.authme.mail; - -import ch.jalu.injector.testing.BeforeInjecting; -import ch.jalu.injector.testing.DelayedInjectionRunner; -import ch.jalu.injector.testing.InjectDelayed; -import fr.xephi.authme.TestHelper; -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 org.apache.commons.mail.EmailException; -import org.apache.commons.mail.HtmlEmail; -import org.bukkit.Server; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; - -import java.io.File; -import java.io.IOException; -import java.util.Properties; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -/** - * Test for {@link SendMailSSL}. - */ -@RunWith(DelayedInjectionRunner.class) -public class SendMailSSLTest { - - @InjectDelayed - private SendMailSSL sendMailSSL; - - @Mock - private Settings settings; - @Mock - private Server server; - @DataFolder - private File dataFolder; - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @BeforeClass - public static void initLogger() { - TestHelper.setupLogger(); - } - - @BeforeInjecting - public void initFields() throws IOException { - dataFolder = temporaryFolder.newFolder(); - given(server.getServerName()).willReturn("serverName"); - given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("mail@example.org"); - given(settings.getProperty(EmailSettings.MAIL_PASSWORD)).willReturn("pass1234"); - } - - @Test - public void shouldHaveAllInformation() { - // given / when / then - assertThat(sendMailSSL.hasAllInformation(), equalTo(true)); - } - - @Test - public void shouldSendPasswordMail() throws EmailException { - // given - given(settings.getPasswordEmailMessage()) - .willReturn("Hi , your new password for is "); - given(settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)).willReturn(false); - SendMailSSL sendMailSpy = spy(sendMailSSL); - HtmlEmail email = mock(HtmlEmail.class); - doReturn(email).when(sendMailSpy).initializeMail(anyString()); - doReturn(true).when(sendMailSpy).sendEmail(anyString(), any(HtmlEmail.class)); - - // when - boolean result = sendMailSpy.sendPasswordMail("Player", "user@example.com", "new_password"); - - // then - assertThat(result, equalTo(true)); - verify(sendMailSpy).initializeMail("user@example.com"); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); - verify(sendMailSpy).sendEmail(messageCaptor.capture(), eq(email)); - assertThat(messageCaptor.getValue(), - equalTo("Hi Player, your new password for serverName is new_password")); - } - - @Test - public void shouldHandleMailCreationError() throws EmailException { - // given - SendMailSSL sendMailSpy = spy(sendMailSSL); - doThrow(EmailException.class).when(sendMailSpy).initializeMail(anyString()); - - // when - boolean result = sendMailSpy.sendPasswordMail("Player", "user@example.com", "new_password"); - - // then - assertThat(result, equalTo(false)); - verify(sendMailSpy).initializeMail("user@example.com"); - verify(sendMailSpy, never()).sendEmail(anyString(), any(HtmlEmail.class)); - } - - @Test - public void shouldHandleMailSendingFailure() throws EmailException { - // given - given(settings.getPasswordEmailMessage()).willReturn("Hi , your new pass is "); - given(settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)).willReturn(false); - SendMailSSL sendMailSpy = spy(sendMailSSL); - HtmlEmail email = mock(HtmlEmail.class); - doReturn(email).when(sendMailSpy).initializeMail(anyString()); - doReturn(false).when(sendMailSpy).sendEmail(anyString(), any(HtmlEmail.class)); - - // when - boolean result = sendMailSpy.sendPasswordMail("bobby", "user@example.com", "myPassw0rd"); - - // then - assertThat(result, equalTo(false)); - verify(sendMailSpy).initializeMail("user@example.com"); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); - verify(sendMailSpy).sendEmail(messageCaptor.capture(), eq(email)); - assertThat(messageCaptor.getValue(), equalTo("Hi bobby, your new pass is myPassw0rd")); - } - - @Test - public void shouldSendRecoveryCode() throws EmailException { - // given - given(settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID)).willReturn(7); - given(settings.getRecoveryCodeEmailMessage()) - .willReturn("Hi , your code on is (valid hours)"); - SendMailSSL sendMailSpy = spy(sendMailSSL); - HtmlEmail email = mock(HtmlEmail.class); - doReturn(email).when(sendMailSpy).initializeMail(anyString()); - doReturn(true).when(sendMailSpy).sendEmail(anyString(), any(HtmlEmail.class)); - - // when - boolean result = sendMailSpy.sendRecoveryCode("Timmy", "tim@example.com", "12C56A"); - - // then - assertThat(result, equalTo(true)); - verify(sendMailSpy).initializeMail("tim@example.com"); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); - verify(sendMailSpy).sendEmail(messageCaptor.capture(), eq(email)); - assertThat(messageCaptor.getValue(), equalTo("Hi Timmy, your code on serverName is 12C56A (valid 7 hours)")); - } - - @Test - public void shouldHandleMailCreationErrorForRecoveryCode() throws EmailException { - // given - SendMailSSL sendMailSpy = spy(sendMailSSL); - doThrow(EmailException.class).when(sendMailSpy).initializeMail(anyString()); - - // when - boolean result = sendMailSpy.sendRecoveryCode("Player", "player@example.org", "ABC1234"); - - // then - assertThat(result, equalTo(false)); - verify(sendMailSpy).initializeMail("player@example.org"); - verify(sendMailSpy, never()).sendEmail(anyString(), any(HtmlEmail.class)); - } - - @Test - public void shouldHandleFailureToSendRecoveryCode() throws EmailException { - // given - given(settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID)).willReturn(7); - given(settings.getRecoveryCodeEmailMessage()).willReturn("Hi , your code is "); - SendMailSSL sendMailSpy = spy(sendMailSSL); - HtmlEmail email = mock(HtmlEmail.class); - doReturn(email).when(sendMailSpy).initializeMail(anyString()); - doReturn(false).when(sendMailSpy).sendEmail(anyString(), any(HtmlEmail.class)); - - // when - boolean result = sendMailSpy.sendRecoveryCode("John", "user@example.com", "1DEF77"); - - // then - assertThat(result, equalTo(false)); - verify(sendMailSpy).initializeMail("user@example.com"); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); - verify(sendMailSpy).sendEmail(messageCaptor.capture(), eq(email)); - assertThat(messageCaptor.getValue(), equalTo("Hi John, your code is 1DEF77")); - } - - @Test - public void shouldCreateEmailObject() throws EmailException { - // given - given(settings.getProperty(EmailSettings.SMTP_PORT)).willReturn(465); - String smtpHost = "mail.example.com"; - given(settings.getProperty(EmailSettings.SMTP_HOST)).willReturn(smtpHost); - String senderAccount = "sender@example.org"; - given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(senderAccount); - String senderName = "Server administration"; - given(settings.getProperty(EmailSettings.MAIL_SENDER_NAME)).willReturn(senderName); - - // when - HtmlEmail email = sendMailSSL.initializeMail("recipient@example.com"); - - // then - assertThat(email, not(nullValue())); - assertThat(email.getToAddresses(), hasSize(1)); - assertThat(email.getToAddresses().get(0).getAddress(), equalTo("recipient@example.com")); - assertThat(email.getFromAddress().getAddress(), equalTo(senderAccount)); - assertThat(email.getFromAddress().getPersonal(), equalTo(senderName)); - assertThat(email.getHostName(), equalTo(smtpHost)); - assertThat(email.getSmtpPort(), equalTo("465")); - } - - @Test - public void shouldCreateEmailObjectWithAddress() throws EmailException { - // given - given(settings.getProperty(EmailSettings.SMTP_PORT)).willReturn(465); - String smtpHost = "mail.example.com"; - given(settings.getProperty(EmailSettings.SMTP_HOST)).willReturn(smtpHost); - String senderAccount = "exampleAccount"; - given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(senderAccount); - String senderAddress = "mail@example.com"; - given(settings.getProperty(EmailSettings.MAIL_ADDRESS)).willReturn(senderAddress); - String senderName = "Server administration"; - given(settings.getProperty(EmailSettings.MAIL_SENDER_NAME)).willReturn(senderName); - - // when - HtmlEmail email = sendMailSSL.initializeMail("recipient@example.com"); - - // then - assertThat(email, not(nullValue())); - assertThat(email.getToAddresses(), hasSize(1)); - assertThat(email.getToAddresses().get(0).getAddress(), equalTo("recipient@example.com")); - assertThat(email.getFromAddress().getAddress(), equalTo(senderAddress)); - assertThat(email.getFromAddress().getPersonal(), equalTo(senderName)); - assertThat(email.getHostName(), equalTo(smtpHost)); - assertThat(email.getSmtpPort(), equalTo("465")); - } - - @Test - public void shouldCreateEmailObjectWithOAuth2() throws EmailException { - // given - given(settings.getProperty(EmailSettings.SMTP_PORT)).willReturn(587); - given(settings.getProperty(EmailSettings.OAUTH2_TOKEN)).willReturn("oAuth2 token"); - String smtpHost = "mail.example.com"; - given(settings.getProperty(EmailSettings.SMTP_HOST)).willReturn(smtpHost); - String senderMail = "sender@example.org"; - given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(senderMail); - - // when - HtmlEmail email = sendMailSSL.initializeMail("recipient@example.com"); - - // then - assertThat(email, not(nullValue())); - assertThat(email.getToAddresses(), hasSize(1)); - assertThat(email.getToAddresses().get(0).getAddress(), equalTo("recipient@example.com")); - assertThat(email.getFromAddress().getAddress(), equalTo(senderMail)); - assertThat(email.getHostName(), equalTo(smtpHost)); - assertThat(email.getSmtpPort(), equalTo("587")); - - Properties mailProperties = email.getMailSession().getProperties(); - assertThat(mailProperties.getProperty("mail.smtp.auth.mechanisms"), equalTo("XOAUTH2")); - assertThat(mailProperties.getProperty("mail.smtp.auth.plain.disable"), equalTo("true")); - assertThat(mailProperties.getProperty(OAuth2SaslClientFactory.OAUTH_TOKEN_PROP), equalTo("oAuth2 token")); - } - -} diff --git a/src/test/java/fr/xephi/authme/mail/SendMailSslTest.java b/src/test/java/fr/xephi/authme/mail/SendMailSslTest.java new file mode 100644 index 000000000..cd5b21523 --- /dev/null +++ b/src/test/java/fr/xephi/authme/mail/SendMailSslTest.java @@ -0,0 +1,140 @@ +package fr.xephi.authme.mail; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.TestHelper; +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.PluginSettings; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.HtmlEmail; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.io.IOException; +import java.util.Properties; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Test for {@link SendMailSsl}. + */ +@RunWith(DelayedInjectionRunner.class) +public class SendMailSslTest { + + @InjectDelayed + private SendMailSsl sendMailSsl; + + @Mock + private Settings settings; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void initLogger() { + TestHelper.setupLogger(); + } + + @BeforeInjecting + public void initFields() throws IOException { + given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("mail@example.org"); + given(settings.getProperty(EmailSettings.MAIL_PASSWORD)).willReturn("pass1234"); + given(settings.getProperty(PluginSettings.LOG_LEVEL)).willReturn(LogLevel.INFO); + } + + @Test + public void shouldHaveAllInformation() { + // given / when / then + assertThat(sendMailSsl.hasAllInformation(), equalTo(true)); + } + + @Test + public void shouldCreateEmailObject() throws EmailException { + // given + given(settings.getProperty(EmailSettings.SMTP_PORT)).willReturn(465); + String smtpHost = "mail.example.com"; + given(settings.getProperty(EmailSettings.SMTP_HOST)).willReturn(smtpHost); + String senderAccount = "sender@example.org"; + given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(senderAccount); + String senderName = "Server administration"; + given(settings.getProperty(EmailSettings.MAIL_SENDER_NAME)).willReturn(senderName); + given(settings.getProperty(PluginSettings.LOG_LEVEL)).willReturn(LogLevel.DEBUG); + + // when + HtmlEmail email = sendMailSsl.initializeMail("recipient@example.com"); + + // then + assertThat(email, not(nullValue())); + assertThat(email.getToAddresses(), hasSize(1)); + assertThat(email.getToAddresses().get(0).getAddress(), equalTo("recipient@example.com")); + assertThat(email.getFromAddress().getAddress(), equalTo(senderAccount)); + assertThat(email.getFromAddress().getPersonal(), equalTo(senderName)); + assertThat(email.getHostName(), equalTo(smtpHost)); + assertThat(email.getSmtpPort(), equalTo("465")); + } + + @Test + public void shouldCreateEmailObjectWithAddress() throws EmailException { + // given + given(settings.getProperty(EmailSettings.SMTP_PORT)).willReturn(465); + String smtpHost = "mail.example.com"; + given(settings.getProperty(EmailSettings.SMTP_HOST)).willReturn(smtpHost); + String senderAccount = "exampleAccount"; + given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(senderAccount); + String senderAddress = "mail@example.com"; + given(settings.getProperty(EmailSettings.MAIL_ADDRESS)).willReturn(senderAddress); + String senderName = "Server administration"; + given(settings.getProperty(EmailSettings.MAIL_SENDER_NAME)).willReturn(senderName); + + // when + HtmlEmail email = sendMailSsl.initializeMail("recipient@example.com"); + + // then + assertThat(email, not(nullValue())); + assertThat(email.getToAddresses(), hasSize(1)); + assertThat(email.getToAddresses().get(0).getAddress(), equalTo("recipient@example.com")); + assertThat(email.getFromAddress().getAddress(), equalTo(senderAddress)); + assertThat(email.getFromAddress().getPersonal(), equalTo(senderName)); + assertThat(email.getHostName(), equalTo(smtpHost)); + assertThat(email.getSmtpPort(), equalTo("465")); + } + + @Test + public void shouldCreateEmailObjectWithOAuth2() throws EmailException { + // given + given(settings.getProperty(EmailSettings.SMTP_PORT)).willReturn(587); + given(settings.getProperty(EmailSettings.OAUTH2_TOKEN)).willReturn("oAuth2 token"); + String smtpHost = "mail.example.com"; + given(settings.getProperty(EmailSettings.SMTP_HOST)).willReturn(smtpHost); + String senderMail = "sender@example.org"; + given(settings.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(senderMail); + + // when + HtmlEmail email = sendMailSsl.initializeMail("recipient@example.com"); + + // then + assertThat(email, not(nullValue())); + assertThat(email.getToAddresses(), hasSize(1)); + assertThat(email.getToAddresses().get(0).getAddress(), equalTo("recipient@example.com")); + assertThat(email.getFromAddress().getAddress(), equalTo(senderMail)); + assertThat(email.getHostName(), equalTo(smtpHost)); + assertThat(email.getSmtpPort(), equalTo("587")); + + Properties mailProperties = email.getMailSession().getProperties(); + assertThat(mailProperties.getProperty("mail.smtp.auth.mechanisms"), equalTo("XOAUTH2")); + assertThat(mailProperties.getProperty("mail.smtp.auth.plain.disable"), equalTo("true")); + assertThat(mailProperties.getProperty(OAuth2SaslClientFactory.OAUTH_TOKEN_PROP), equalTo("oAuth2 token")); + } +} diff --git a/src/test/java/fr/xephi/authme/message/HelpMessageConsistencyTest.java b/src/test/java/fr/xephi/authme/message/HelpMessageConsistencyTest.java new file mode 100644 index 000000000..399fd93a7 --- /dev/null +++ b/src/test/java/fr/xephi/authme/message/HelpMessageConsistencyTest.java @@ -0,0 +1,93 @@ +package fr.xephi.authme.message; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.command.help.HelpMessage; +import fr.xephi.authme.command.help.HelpSection; +import fr.xephi.authme.permission.DefaultPermission; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Tests that all help_xx.yml files contain all entries for + * {@link HelpSection}, {@link HelpMessage} and {@link DefaultPermission}. + */ +public class HelpMessageConsistencyTest { + + private static final String MESSAGES_FOLDER = "/messages"; + private static final Pattern HELP_MESSAGES_FILE = Pattern.compile("help_[a-z]+\\.yml"); + + private List helpFiles; + + @Before + public void findHelpMessagesFiles() { + File folder = TestHelper.getJarFile(MESSAGES_FOLDER); + File[] files = folder.listFiles(); + if (files == null || files.length == 0) { + throw new IllegalStateException("Could not get files from '" + MESSAGES_FOLDER + "'"); + } + helpFiles = Arrays.stream(files) + .filter(file -> HELP_MESSAGES_FILE.matcher(file.getName()).matches()) + .collect(Collectors.toList()); + } + + @Test + public void shouldHaveRequiredEntries() { + for (File file : helpFiles) { + // given + FileConfiguration configuration = YamlConfiguration.loadConfiguration(file); + + // when / then + assertHasAllHelpSectionEntries(file.getName(), configuration); + } + } + + private void assertHasAllHelpSectionEntries(String filename, FileConfiguration configuration) { + for (HelpSection section : HelpSection.values()) { + assertThat(filename + " should have entry for HelpSection '" + section + "'", + configuration.getString(section.getKey()), notEmptyString()); + } + + for (HelpMessage message : HelpMessage.values()) { + assertThat(filename + " should have entry for HelpMessage '" + message + "'", + configuration.getString(message.getKey()), notEmptyString()); + } + + for (DefaultPermission defaultPermission : DefaultPermission.values()) { + assertThat(filename + " should have entry for DefaultPermission '" + defaultPermission + "'", + configuration.getString(getPathForDefaultPermission(defaultPermission)), notEmptyString()); + } + } + + private static String getPathForDefaultPermission(DefaultPermission defaultPermission) { + String path = "common.defaultPermissions."; + switch (defaultPermission) { + case ALLOWED: + return path + "allowed"; + case NOT_ALLOWED: + return path + "notAllowed"; + case OP_ONLY: + return path + "opOnly"; + default: + throw new IllegalStateException("Unknown default permission '" + defaultPermission + "'"); + } + } + + private static Matcher notEmptyString() { + return both(not(emptyString())).and(not(nullValue())); + } +} diff --git a/src/test/java/fr/xephi/authme/message/MessagesIntegrationTest.java b/src/test/java/fr/xephi/authme/message/MessagesIntegrationTest.java index 12949c5aa..79e873176 100644 --- a/src/test/java/fr/xephi/authme/message/MessagesIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/message/MessagesIntegrationTest.java @@ -1,7 +1,9 @@ package fr.xephi.authme.message; +import com.google.common.collect.ImmutableMap; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.TestHelper; +import fr.xephi.authme.util.expiring.Duration; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; @@ -11,6 +13,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.io.File; +import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.logging.Logger; @@ -85,7 +89,7 @@ public class MessagesIntegrationTest { @Test public void shouldFormatColorCodes() { // given - MessageKey key = MessageKey.UNSAFE_QUIT_LOCATION; + MessageKey key = MessageKey.LOGIN_SUCCESS; // when String[] message = messages.retrieve(key); @@ -111,7 +115,7 @@ public class MessagesIntegrationTest { @Test public void shouldSendMessageToPlayer() { // given - MessageKey key = MessageKey.UNSAFE_QUIT_LOCATION; + MessageKey key = MessageKey.LOGIN_SUCCESS; Player player = Mockito.mock(Player.class); // when @@ -230,11 +234,31 @@ public class MessagesIntegrationTest { assertThat(result, equalTo("Use /captcha 24680 to solve the captcha")); } + @Test + public void shouldFormatDurationObjects() { + // given + Map expectedTexts = ImmutableMap.builder() + .put(new Duration(1, TimeUnit.SECONDS), "1 second") + .put(new Duration(12, TimeUnit.SECONDS), "12 seconds") + .put(new Duration(1, TimeUnit.MINUTES), "1 minute") + .put(new Duration(0, TimeUnit.MINUTES), "0 minutes") + .put(new Duration(1, TimeUnit.HOURS), "1 hour") + .put(new Duration(-4, TimeUnit.HOURS), "-4 hours") + .put(new Duration(1, TimeUnit.DAYS), "1 day") + .put(new Duration(44, TimeUnit.DAYS), "44 days") + .build(); + + // when / then + for (Map.Entry entry : expectedTexts.entrySet()) { + assertThat(messages.formatDuration(entry.getKey()), equalTo(entry.getValue())); + } + } + @SuppressWarnings("unchecked") private static MessageFileHandlerProvider providerReturning(File file, String defaultFile) { MessageFileHandlerProvider handler = mock(MessageFileHandlerProvider.class); - given(handler.initializeHandler(any(Function.class))) - .willReturn(new MessageFileHandler(file, defaultFile)); + given(handler.initializeHandler(any(Function.class), anyString())) + .willReturn(new MessageFileHandler(file, defaultFile, "/authme messages")); return handler; } } diff --git a/src/test/java/fr/xephi/authme/output/LogFilterHelperTest.java b/src/test/java/fr/xephi/authme/output/LogFilterHelperTest.java new file mode 100644 index 000000000..f211a608b --- /dev/null +++ b/src/test/java/fr/xephi/authme/output/LogFilterHelperTest.java @@ -0,0 +1,88 @@ +package fr.xephi.authme.output; + +import com.google.common.collect.Lists; +import fr.xephi.authme.command.CommandDescription; +import fr.xephi.authme.command.CommandInitializer; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link LogFilterHelper}. + */ +public class LogFilterHelperTest { + + private static final List ALL_COMMANDS = new CommandInitializer().getCommands(); + + /** + * Checks that {@link LogFilterHelper#COMMANDS_TO_SKIP} contains the entries we expect + * (commands with password argument). + */ + @Test + public void shouldBlacklistAllSensitiveCommands() { + // given + List sensitiveCommands = Arrays.asList( + getCommand("register"), getCommand("login"), getCommand("changepassword"), getCommand("unregister"), + getCommand("authme", "register"), getCommand("authme", "changepassword") + ); + // Build array with entries like "/register ", "/authme cp ", "/authme changepass " + String[] expectedEntries = sensitiveCommands.stream() + .map(cmd -> buildCommandSyntaxes(cmd)) + .flatMap(List::stream) + .map(syntax -> syntax + " ") + .toArray(String[]::new); + + // when / then + assertThat(LogFilterHelper.COMMANDS_TO_SKIP, containsInAnyOrder(expectedEntries)); + + } + + private static CommandDescription getCommand(String label) { + return findCommandWithLabel(label, ALL_COMMANDS); + } + + private static CommandDescription getCommand(String parentLabel, String childLabel) { + CommandDescription parent = getCommand(parentLabel); + return findCommandWithLabel(childLabel, parent.getChildren()); + } + + private static CommandDescription findCommandWithLabel(String label, List commands) { + return commands.stream() + .filter(cmd -> cmd.getLabels().contains(label)) + .findFirst().orElseThrow(() -> new IllegalArgumentException(label)); + } + + /** + * Returns all "command syntaxes" from which the given command can be reached. + * For example, the result might be a List containing "/authme changepassword", "/authme changepass", + * "/authme cp", "/authme:authme changepassword" etc. + * + * @param command the command to build syntaxes for + * @return command syntaxes + */ + private static List buildCommandSyntaxes(CommandDescription command) { + List prefixes = getCommandPrefixes(command); + + return command.getLabels() + .stream() + .map(label -> Lists.transform(prefixes, p -> p + label)) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + private static List getCommandPrefixes(CommandDescription command) { + if (command.getParent() == null) { + return Arrays.asList("/", "/authme:"); + } + return command.getParent().getLabels() + .stream() + .map(label -> new String[]{"/" + label + " ", "/authme:" + label + " "}) + .flatMap(Arrays::stream) + .collect(Collectors.toList()); + } +} diff --git a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java index 12d4fee7a..9f21b7ba4 100644 --- a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java +++ b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java @@ -3,18 +3,17 @@ package fr.xephi.authme.process.login; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent; import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; import org.bukkit.entity.Player; import org.junit.BeforeClass; import org.junit.Test; @@ -58,11 +57,9 @@ public class AsynchronousLoginTest { @Mock private CommonService commonService; @Mock - private LimboPlayerTaskManager limboPlayerTaskManager; + private LimboService limboService; @Mock private BukkitService bukkitService; - @Mock - private PermissionsManager permissionsManager; @BeforeClass public static void initLogger() { @@ -182,7 +179,7 @@ public class AsynchronousLoginTest { // given Player player = mockPlayer("Carl"); given(commonService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(2); - given(permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(false); + given(commonService.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(false); mockOnlinePlayersInBukkitService(); // when @@ -190,7 +187,7 @@ public class AsynchronousLoginTest { // then assertThat(result, equalTo(false)); - verify(permissionsManager).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); + verify(commonService).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); verify(bukkitService).getOnlinePlayers(); } @@ -213,14 +210,14 @@ public class AsynchronousLoginTest { // given Player player = mockPlayer("Frank"); given(commonService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(1); - given(permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(true); + given(commonService.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(true); // when boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "127.0.0.4"); // then assertThat(result, equalTo(false)); - verify(permissionsManager).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); + verify(commonService).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); verifyZeroInteractions(bukkitService); } @@ -229,7 +226,7 @@ public class AsynchronousLoginTest { // given Player player = mockPlayer("Ian"); given(commonService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(2); - given(permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(false); + given(commonService.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(false); mockOnlinePlayersInBukkitService(); // when @@ -237,7 +234,7 @@ public class AsynchronousLoginTest { // then assertThat(result, equalTo(true)); - verify(permissionsManager).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); + verify(commonService).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); verify(bukkitService).getOnlinePlayers(); } diff --git a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java index b845b8d59..dee3dc646 100644 --- a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java +++ b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java @@ -3,9 +3,13 @@ package fr.xephi.authme.process.register; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.factory.SingletonStore; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.process.register.executors.PasswordRegisterParams; import fr.xephi.authme.process.register.executors.RegistrationExecutor; +import fr.xephi.authme.process.register.executors.RegistrationMethod; +import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -16,6 +20,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.only; @@ -39,6 +44,8 @@ public class AsyncRegisterTest { private CommonService commonService; @Mock private DataSource dataSource; + @Mock + private SingletonStore registrationExecutorStore; @Test public void shouldDetectAlreadyLoggedInPlayer() { @@ -47,9 +54,10 @@ public class AsyncRegisterTest { Player player = mockPlayerWithName(name); given(playerCache.isAuthenticated(name)).willReturn(true); RegistrationExecutor executor = mock(RegistrationExecutor.class); + singletonStoreWillReturn(registrationExecutorStore, executor); // when - asyncRegister.register(player, executor); + asyncRegister.register(RegistrationMethod.PASSWORD_REGISTRATION, PasswordRegisterParams.of(player, "abc", null)); // then verify(commonService).send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); @@ -64,9 +72,10 @@ public class AsyncRegisterTest { given(playerCache.isAuthenticated(name)).willReturn(false); given(commonService.getProperty(RegistrationSettings.IS_ENABLED)).willReturn(false); RegistrationExecutor executor = mock(RegistrationExecutor.class); + singletonStoreWillReturn(registrationExecutorStore, executor); // when - asyncRegister.register(player, executor); + asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, TwoFactorRegisterParams.of(player)); // then verify(commonService).send(player, MessageKey.REGISTRATION_DISABLED); @@ -82,9 +91,10 @@ public class AsyncRegisterTest { given(commonService.getProperty(RegistrationSettings.IS_ENABLED)).willReturn(true); given(dataSource.isAuthAvailable(name)).willReturn(true); RegistrationExecutor executor = mock(RegistrationExecutor.class); + singletonStoreWillReturn(registrationExecutorStore, executor); // when - asyncRegister.register(player, executor); + asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, TwoFactorRegisterParams.of(player)); // then verify(commonService).send(player, MessageKey.NAME_ALREADY_REGISTERED); @@ -93,6 +103,7 @@ public class AsyncRegisterTest { } @Test + @SuppressWarnings("unchecked") public void shouldStopForFailedExecutorCheck() { // given String name = "edbert"; @@ -103,14 +114,16 @@ public class AsyncRegisterTest { given(commonService.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP)).willReturn(0); given(dataSource.isAuthAvailable(name)).willReturn(false); RegistrationExecutor executor = mock(RegistrationExecutor.class); - given(executor.isRegistrationAdmitted()).willReturn(false); + TwoFactorRegisterParams params = TwoFactorRegisterParams.of(player); + given(executor.isRegistrationAdmitted(params)).willReturn(false); + singletonStoreWillReturn(registrationExecutorStore, executor); // when - asyncRegister.register(player, executor); + asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, params); // then verify(dataSource, only()).isAuthAvailable(name); - verify(executor, only()).isRegistrationAdmitted(); + verify(executor, only()).isRegistrationAdmitted(params); } private static Player mockPlayerWithName(String name) { @@ -118,4 +131,10 @@ public class AsyncRegisterTest { given(player.getName()).willReturn(name); return player; } + + @SuppressWarnings("unchecked") + private static void singletonStoreWillReturn(SingletonStore store, + RegistrationExecutor mock) { + given(store.getSingleton(any(Class.class))).willReturn(mock); + } } diff --git a/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java b/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java index aecc158a3..6aa221611 100644 --- a/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java +++ b/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java @@ -1,10 +1,9 @@ package fr.xephi.authme.process.register.executors; -import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerAuth; 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.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; @@ -34,13 +33,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; /** - * Test for {@link EmailRegisterExecutorProvider}. + * Test for {@link EmailRegisterExecutor}. */ @RunWith(MockitoJUnitRunner.class) public class EmailRegisterExecutorProviderTest { @InjectMocks - private EmailRegisterExecutorProvider emailRegisterExecutorProvider; + private EmailRegisterExecutor executor; @Mock private PermissionsManager permissionsManager; @@ -49,7 +48,7 @@ public class EmailRegisterExecutorProviderTest { @Mock private CommonService commonService; @Mock - private SendMailSSL sendMailSsl; + private EmailService emailService; @Mock private SyncProcessManager syncProcessManager; @Mock @@ -62,10 +61,10 @@ public class EmailRegisterExecutorProviderTest { String email = "test@example.com"; given(dataSource.countAuthsByEmail(email)).willReturn(4); Player player = mock(Player.class); - RegistrationExecutor executor = emailRegisterExecutorProvider.new EmailRegisterExecutor(player, email); + EmailRegisterParams params = EmailRegisterParams.of(player, email); // when - boolean result = executor.isRegistrationAdmitted(); + boolean result = executor.isRegistrationAdmitted(params); // then assertThat(result, equalTo(false)); @@ -80,10 +79,10 @@ public class EmailRegisterExecutorProviderTest { given(commonService.getProperty(EmailSettings.MAX_REG_PER_EMAIL)).willReturn(3); Player player = mock(Player.class); given(permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(true); - RegistrationExecutor executor = emailRegisterExecutorProvider.new EmailRegisterExecutor(player, "test@example.com"); + EmailRegisterParams params = EmailRegisterParams.of(player, "test@example.com"); // when - boolean result = executor.isRegistrationAdmitted(); + boolean result = executor.isRegistrationAdmitted(params); // then assertThat(result, equalTo(true)); @@ -97,10 +96,10 @@ public class EmailRegisterExecutorProviderTest { String email = "test@example.com"; given(dataSource.countAuthsByEmail(email)).willReturn(0); Player player = mock(Player.class); - RegistrationExecutor executor = emailRegisterExecutorProvider.new EmailRegisterExecutor(player, "test@example.com"); + EmailRegisterParams params = EmailRegisterParams.of(player, "test@example.com"); // when - boolean result = executor.isRegistrationAdmitted(); + boolean result = executor.isRegistrationAdmitted(params); // then assertThat(result, equalTo(true)); @@ -120,10 +119,10 @@ public class EmailRegisterExecutorProviderTest { World world = mock(World.class); given(world.getName()).willReturn("someWorld"); given(player.getLocation()).willReturn(new Location(world, 48, 96, 144)); - RegistrationExecutor executor = emailRegisterExecutorProvider.new EmailRegisterExecutor(player, "test@example.com"); + EmailRegisterParams params = EmailRegisterParams.of(player, "test@example.com"); // when - PlayerAuth auth = executor.buildPlayerAuth(); + PlayerAuth auth = executor.buildPlayerAuth(params); // then assertThat(auth, hasAuthBasicData("veronica", "Veronica", "test@example.com", "123.45.67.89")); @@ -132,40 +131,38 @@ public class EmailRegisterExecutorProviderTest { } @Test - @SuppressWarnings("unchecked") public void shouldPerformActionAfterDataSourceSave() { // given - given(sendMailSsl.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); + given(emailService.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); Player player = mock(Player.class); given(player.getName()).willReturn("Laleh"); - RegistrationExecutor executor = emailRegisterExecutorProvider.new EmailRegisterExecutor(player, "test@example.com"); + EmailRegisterParams params = EmailRegisterParams.of(player, "test@example.com"); String password = "A892C#@"; - ReflectionTestUtils.setField((Class) executor.getClass(), executor, "password", password); + params.setPassword(password); // when - executor.executePostPersistAction(); + executor.executePostPersistAction(params); // then - verify(sendMailSsl).sendPasswordMail("Laleh", "test@example.com", password); + verify(emailService).sendPasswordMail("Laleh", "test@example.com", password); verify(syncProcessManager).processSyncEmailRegister(player); } @Test - @SuppressWarnings("unchecked") public void shouldHandleEmailSendingFailure() { // given - given(sendMailSsl.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(false); + given(emailService.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(false); Player player = mock(Player.class); given(player.getName()).willReturn("Laleh"); - RegistrationExecutor executor = emailRegisterExecutorProvider.new EmailRegisterExecutor(player, "test@example.com"); + EmailRegisterParams params = EmailRegisterParams.of(player, "test@example.com"); String password = "A892C#@"; - ReflectionTestUtils.setField((Class) executor.getClass(), executor, "password", password); + params.setPassword(password); // when - executor.executePostPersistAction(); + executor.executePostPersistAction(params); // then - verify(sendMailSsl).sendPasswordMail("Laleh", "test@example.com", password); + verify(emailService).sendPasswordMail("Laleh", "test@example.com", password); verify(commonService).send(player, MessageKey.EMAIL_SEND_FAILURE); verifyZeroInteractions(syncProcessManager); } diff --git a/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorProviderTest.java b/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorTest.java similarity index 81% rename from src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorProviderTest.java rename to src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorTest.java index a033975f6..6c103944b 100644 --- a/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorProviderTest.java +++ b/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorTest.java @@ -34,13 +34,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; /** - * Test for {@link PasswordRegisterExecutorProvider}. + * Test for {@link PasswordRegisterExecutor}. */ @RunWith(MockitoJUnitRunner.class) -public class PasswordRegisterExecutorProviderTest { +public class PasswordRegisterExecutorTest { @InjectMocks - private PasswordRegisterExecutorProvider passwordRegisterExecutorProvider; + private PasswordRegisterExecutor executor; @Mock private ValidationService validationService; @@ -62,10 +62,10 @@ public class PasswordRegisterExecutorProviderTest { String name = "player040"; given(validationService.validatePassword(password, name)).willReturn(new ValidationResult()); Player player = mockPlayerWithName(name); - RegistrationExecutor executor = passwordRegisterExecutorProvider.new PasswordRegisterExecutor(player, password, null); + PasswordRegisterParams params = PasswordRegisterParams.of(player, password, null); // when - boolean result = executor.isRegistrationAdmitted(); + boolean result = executor.isRegistrationAdmitted(params); // then assertThat(result, equalTo(true)); @@ -80,10 +80,10 @@ public class PasswordRegisterExecutorProviderTest { given(validationService.validatePassword(password, name)).willReturn( new ValidationResult(MessageKey.PASSWORD_CHARACTERS_ERROR, "[a-z]")); Player player = mockPlayerWithName(name); - RegistrationExecutor executor = passwordRegisterExecutorProvider.new PasswordRegisterExecutor(player, password, null); + PasswordRegisterParams params = PasswordRegisterParams.of(player, password, null); // when - boolean result = executor.isRegistrationAdmitted(); + boolean result = executor.isRegistrationAdmitted(params); // then assertThat(result, equalTo(false)); @@ -101,10 +101,10 @@ public class PasswordRegisterExecutorProviderTest { World world = mock(World.class); given(world.getName()).willReturn("someWorld"); given(player.getLocation()).willReturn(new Location(world, 48, 96, 144)); - RegistrationExecutor executor = passwordRegisterExecutorProvider.new PasswordRegisterExecutor(player, "pass", "mail@example.org"); + PasswordRegisterParams params = PasswordRegisterParams.of(player, "pass", "mail@example.org"); // when - PlayerAuth auth = executor.buildPlayerAuth(); + PlayerAuth auth = executor.buildPlayerAuth(params); // then assertThat(auth, hasAuthBasicData("s1m0n", "S1m0N", "mail@example.org", "123.45.67.89")); @@ -118,10 +118,10 @@ public class PasswordRegisterExecutorProviderTest { given(commonService.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER)).willReturn(false); given(commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(false); Player player = mock(Player.class); - RegistrationExecutor executor = passwordRegisterExecutorProvider.new PasswordRegisterExecutor(player, "pass", "mail@example.org"); + PasswordRegisterParams params = PasswordRegisterParams.of(player, "pass", "mail@example.org"); // when - executor.executePostPersistAction(); + executor.executePostPersistAction(params); // then TestHelper.runSyncDelayedTaskWithDelay(bukkitService); @@ -134,10 +134,10 @@ public class PasswordRegisterExecutorProviderTest { // given given(commonService.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER)).willReturn(true); Player player = mock(Player.class); - RegistrationExecutor executor = passwordRegisterExecutorProvider.new PasswordRegisterExecutor(player, "pass", "mail@example.org"); + PasswordRegisterParams params = PasswordRegisterParams.of(player, "pass", "mail@example.org"); // when - executor.executePostPersistAction(); + executor.executePostPersistAction(params); // then verifyZeroInteractions(bukkitService, asynchronousLogin); diff --git a/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java b/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java index d586b425a..26433a45d 100644 --- a/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java +++ b/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java @@ -3,19 +3,17 @@ package fr.xephi.authme.process.unregister; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.data.limbo.LimboCache; +import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.AuthGroupHandler; import fr.xephi.authme.permission.AuthGroupType; -import fr.xephi.authme.service.CommonService; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.TeleportationService; import fr.xephi.authme.settings.properties.RegistrationSettings; -import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.LimboPlayerTaskManager; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.BeforeClass; @@ -53,9 +51,7 @@ public class AsynchronousUnregisterTest { @Mock private BukkitService bukkitService; @Mock - private LimboCache limboCache; - @Mock - private LimboPlayerTaskManager limboPlayerTaskManager; + private LimboService limboService; @Mock private TeleportationService teleportationService; @Mock @@ -85,7 +81,7 @@ public class AsynchronousUnregisterTest { // then verify(service).send(player, MessageKey.WRONG_PASSWORD); verify(passwordSecurity).comparePassword(userPassword, password, name); - verifyZeroInteractions(dataSource, limboPlayerTaskManager, limboCache, authGroupHandler, teleportationService); + verifyZeroInteractions(dataSource, limboService, authGroupHandler, teleportationService); verify(player, only()).getName(); } @@ -104,8 +100,6 @@ public class AsynchronousUnregisterTest { given(passwordSecurity.comparePassword(userPassword, password, name)).willReturn(true); given(dataSource.removeAuth(name)).willReturn(true); given(service.getProperty(RegistrationSettings.FORCE)).willReturn(true); - given(service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)).willReturn(true); - given(service.getProperty(RestrictionSettings.TIMEOUT)).willReturn(12); // when asynchronousUnregister.unregister(player, userPassword); @@ -117,7 +111,7 @@ public class AsynchronousUnregisterTest { verify(playerCache).removePlayer(name); verify(teleportationService).teleportOnJoin(player); verify(authGroupHandler).setGroup(player, AuthGroupType.UNREGISTERED); - verify(bukkitService).runTask(any(Runnable.class)); + verify(bukkitService).scheduleSyncTaskFromOptionallyAsyncTask(any(Runnable.class)); } @Test @@ -135,8 +129,6 @@ public class AsynchronousUnregisterTest { given(passwordSecurity.comparePassword(userPassword, password, name)).willReturn(true); given(dataSource.removeAuth(name)).willReturn(true); given(service.getProperty(RegistrationSettings.FORCE)).willReturn(true); - given(service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)).willReturn(true); - given(service.getProperty(RestrictionSettings.TIMEOUT)).willReturn(0); // when asynchronousUnregister.unregister(player, userPassword); @@ -148,7 +140,7 @@ public class AsynchronousUnregisterTest { verify(playerCache).removePlayer(name); verify(teleportationService).teleportOnJoin(player); verify(authGroupHandler).setGroup(player, AuthGroupType.UNREGISTERED); - verify(bukkitService).runTask(any(Runnable.class)); + verify(bukkitService).scheduleSyncTaskFromOptionallyAsyncTask(any(Runnable.class)); } @Test @@ -175,7 +167,7 @@ public class AsynchronousUnregisterTest { verify(dataSource).removeAuth(name); verify(playerCache).removePlayer(name); verify(authGroupHandler).setGroup(player, AuthGroupType.UNREGISTERED); - verifyZeroInteractions(teleportationService, limboPlayerTaskManager); + verifyZeroInteractions(teleportationService, limboService); verify(bukkitService, never()).runTask(any(Runnable.class)); } @@ -237,8 +229,6 @@ public class AsynchronousUnregisterTest { given(player.isOnline()).willReturn(true); given(dataSource.removeAuth(name)).willReturn(true); given(service.getProperty(RegistrationSettings.FORCE)).willReturn(true); - given(service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)).willReturn(true); - given(service.getProperty(RestrictionSettings.TIMEOUT)).willReturn(12); CommandSender initiator = mock(CommandSender.class); // when @@ -251,7 +241,7 @@ public class AsynchronousUnregisterTest { verify(playerCache).removePlayer(name); verify(teleportationService).teleportOnJoin(player); verify(authGroupHandler).setGroup(player, AuthGroupType.UNREGISTERED); - verify(bukkitService).runTask(any(Runnable.class)); + verify(bukkitService).scheduleSyncTaskFromOptionallyAsyncTask(any(Runnable.class)); } @Test diff --git a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java index e651ebea8..6bd0b45ad 100644 --- a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java +++ b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java @@ -2,26 +2,28 @@ package fr.xephi.authme.security; import ch.jalu.injector.Injector; import ch.jalu.injector.InjectorBuilder; +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.PasswordEncryptionEvent; +import fr.xephi.authme.initialization.factory.Factory; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.security.crypts.JOOMLA; +import fr.xephi.authme.security.crypts.Joomla; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.event.Event; import org.bukkit.plugin.PluginManager; -import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import java.util.Collections; @@ -43,10 +45,11 @@ import static org.mockito.hamcrest.MockitoHamcrest.argThat; /** * Test for {@link PasswordSecurity}. */ -@RunWith(MockitoJUnitRunner.class) +@RunWith(DelayedInjectionRunner.class) public class PasswordSecurityTest { - private Injector injector; + @InjectDelayed + private PasswordSecurity passwordSecurity; @Mock private Settings settings; @@ -57,6 +60,9 @@ public class PasswordSecurityTest { @Mock private DataSource dataSource; + @Mock + private Factory hashAlgorithmFactory; + @Mock private EncryptionMethod method; @@ -67,7 +73,7 @@ public class PasswordSecurityTest { TestHelper.setupLogger(); } - @Before + @BeforeInjecting public void setUpMocks() { caughtClassInEvent = null; @@ -84,10 +90,24 @@ public class PasswordSecurityTest { return null; } }).when(pluginManager).callEvent(any(Event.class)); - injector = new InjectorBuilder().addDefaultHandlers("fr.xephi.authme").create(); + + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.BCRYPT); + given(settings.getProperty(SecuritySettings.LEGACY_HASHES)).willReturn(Collections.emptySet()); + given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8); + + Injector injector = new InjectorBuilder() + .addDefaultHandlers("fr.xephi.authme.security.crypts") + .create(); injector.register(Settings.class, settings); - injector.register(DataSource.class, dataSource); - injector.register(PluginManager.class, pluginManager); + + given(hashAlgorithmFactory.newInstance(any(Class.class))).willAnswer(invocation -> { + Object o = injector.createIfHasDependencies(invocation.getArgument(0)); + if (o == null) { + throw new IllegalArgumentException("Cannot create object of class '" + invocation.getArgument(0) + + "': missing class that needs to be provided?"); + } + return o; + }); } @Test @@ -101,11 +121,9 @@ public class PasswordSecurityTest { given(dataSource.getPassword(playerName)).willReturn(password); given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(true); - initSettings(HashAlgorithm.BCRYPT); - PasswordSecurity security = newPasswordSecurity(); // when - boolean result = security.comparePassword(clearTextPass, playerName); + boolean result = passwordSecurity.comparePassword(clearTextPass, playerName); // then assertThat(result, equalTo(true)); @@ -124,11 +142,9 @@ public class PasswordSecurityTest { given(dataSource.getPassword(playerName)).willReturn(password); given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(false); - initSettings(HashAlgorithm.CUSTOM); - PasswordSecurity security = newPasswordSecurity(); // when - boolean result = security.comparePassword(clearTextPass, playerName); + boolean result = passwordSecurity.comparePassword(clearTextPass, playerName); // then assertThat(result, equalTo(false)); @@ -142,13 +158,10 @@ public class PasswordSecurityTest { // given String playerName = "bobby"; String clearTextPass = "tables"; - given(dataSource.getPassword(playerName)).willReturn(null); - initSettings(HashAlgorithm.MD5); - PasswordSecurity security = newPasswordSecurity(); // when - boolean result = security.comparePassword(clearTextPass, playerName); + boolean result = passwordSecurity.comparePassword(clearTextPass, playerName); // then assertThat(result, equalTo(false)); @@ -172,12 +185,12 @@ public class PasswordSecurityTest { given(dataSource.getPassword(argThat(equalToIgnoringCase(playerName)))).willReturn(password); given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(false); given(method.computeHash(clearTextPass, playerLowerCase)).willReturn(newPassword); - initSettings(HashAlgorithm.MD5); + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.MD5); given(settings.getProperty(SecuritySettings.LEGACY_HASHES)).willReturn(newHashSet(HashAlgorithm.BCRYPT)); - PasswordSecurity security = newPasswordSecurity(); + passwordSecurity.reload(); // when - boolean result = security.comparePassword(clearTextPass, playerName); + boolean result = passwordSecurity.comparePassword(clearTextPass, playerName); // then assertThat(result, equalTo(true)); @@ -198,11 +211,13 @@ public class PasswordSecurityTest { String clearTextPass = "someInvalidPassword"; given(dataSource.getPassword(playerName)).willReturn(password); given(method.comparePassword(clearTextPass, password, playerName)).willReturn(false); - initSettings(HashAlgorithm.MD5); - PasswordSecurity security = newPasswordSecurity(); + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.MD5); + given(settings.getProperty(SecuritySettings.LEGACY_HASHES)).willReturn( + newHashSet(HashAlgorithm.DOUBLEMD5, HashAlgorithm.JOOMLA, HashAlgorithm.SMF, HashAlgorithm.SHA256)); + passwordSecurity.reload(); // when - boolean result = security.comparePassword(clearTextPass, playerName); + boolean result = passwordSecurity.comparePassword(clearTextPass, playerName); // then assertThat(result, equalTo(false)); @@ -217,18 +232,18 @@ public class PasswordSecurityTest { String usernameLowerCase = username.toLowerCase(); HashedPassword hashedPassword = new HashedPassword("$T$est#Hash", "__someSalt__"); given(method.computeHash(password, usernameLowerCase)).willReturn(hashedPassword); - initSettings(HashAlgorithm.JOOMLA); - PasswordSecurity security = newPasswordSecurity(); + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.JOOMLA); + passwordSecurity.reload(); // when - HashedPassword result = security.computeHash(password, username); + HashedPassword result = passwordSecurity.computeHash(password, username); // then assertThat(result, equalTo(hashedPassword)); ArgumentCaptor captor = ArgumentCaptor.forClass(PasswordEncryptionEvent.class); verify(pluginManager).callEvent(captor.capture()); PasswordEncryptionEvent event = captor.getValue(); - assertThat(JOOMLA.class.equals(caughtClassInEvent), equalTo(true)); + assertThat(Joomla.class.equals(caughtClassInEvent), equalTo(true)); assertThat(event.getPlayerName(), equalTo(usernameLowerCase)); } @@ -239,11 +254,11 @@ public class PasswordSecurityTest { String username = "someone12"; HashedPassword hashedPassword = new HashedPassword("~T!est#Hash"); given(method.hasSeparateSalt()).willReturn(true); - initSettings(HashAlgorithm.XAUTH); - PasswordSecurity security = newPasswordSecurity(); + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.XAUTH); + passwordSecurity.reload(); // when - boolean result = security.comparePassword(password, hashedPassword, username); + boolean result = passwordSecurity.comparePassword(password, hashedPassword, username); // then assertThat(result, equalTo(false)); @@ -255,8 +270,6 @@ public class PasswordSecurityTest { @Test public void shouldReloadSettings() { // given - initSettings(HashAlgorithm.BCRYPT); - PasswordSecurity passwordSecurity = newPasswordSecurity(); given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.MD5); given(settings.getProperty(SecuritySettings.LEGACY_HASHES)) .willReturn(newHashSet(HashAlgorithm.CUSTOM, HashAlgorithm.BCRYPT)); @@ -271,21 +284,4 @@ public class PasswordSecurityTest { assertThat(ReflectionTestUtils.getFieldValue(PasswordSecurity.class, passwordSecurity, "legacyAlgorithms"), equalTo(legacyHashesSet)); } - - private PasswordSecurity newPasswordSecurity() { - // Use this method to make sure we have all dependents of PasswordSecurity already registered as mocks - PasswordSecurity passwordSecurity = injector.createIfHasDependencies(PasswordSecurity.class); - if (passwordSecurity == null) { - throw new IllegalStateException("Cannot create PasswordSecurity directly! " - + "Did you forget to provide a dependency as mock?"); - } - return passwordSecurity; - } - - private void initSettings(HashAlgorithm algorithm) { - given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(algorithm); - given(settings.getProperty(SecuritySettings.LEGACY_HASHES)).willReturn(Collections.emptySet()); - given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8); - } - } diff --git a/src/test/java/fr/xephi/authme/security/crypts/BCRYPT2YTest.java b/src/test/java/fr/xephi/authme/security/crypts/BCrypt2yTest.java similarity index 73% rename from src/test/java/fr/xephi/authme/security/crypts/BCRYPT2YTest.java rename to src/test/java/fr/xephi/authme/security/crypts/BCrypt2yTest.java index ac34dea02..83308c9fc 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/BCRYPT2YTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/BCrypt2yTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link BCRYPT2Y}. + * Test for {@link BCrypt2y}. */ -public class BCRYPT2YTest extends AbstractEncryptionMethodTest { +public class BCrypt2yTest extends AbstractEncryptionMethodTest { - public BCRYPT2YTest() { - super(new BCRYPT2Y(), + public BCrypt2yTest() { + super(new BCrypt2y(), "$2y$10$da641e404b982edf1c7c0uTU9BcKzfA2vWKV05q6r.dCvm/93wqVK", // password "$2y$10$e52c48a76f5b86f5da899uiK/HYocyPsfQXESNbP278rIz08LKEP2", // PassWord1 "$2y$10$be6f11548dc5fb4088410ONdC0dXnJ04y1RHcJh5fVF3XK5d.qgqK", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/BcryptTest.java b/src/test/java/fr/xephi/authme/security/crypts/BCryptTest.java similarity index 83% rename from src/test/java/fr/xephi/authme/security/crypts/BcryptTest.java rename to src/test/java/fr/xephi/authme/security/crypts/BCryptTest.java index 1fe9ed086..c2d9d6f5a 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/BcryptTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/BCryptTest.java @@ -7,12 +7,12 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Test for {@link BCRYPT}. + * Test for {@link BCrypt}. */ -public class BcryptTest extends AbstractEncryptionMethodTest { +public class BCryptTest extends AbstractEncryptionMethodTest { - public BcryptTest() { - super(new BCRYPT(mockSettings()), + public BCryptTest() { + super(new BCrypt(mockSettings()), "$2a$10$6iATmYgwJVc3YONhVcZFve3Cfb5GnwvKhJ20r.hMjmcNkIT9.Uh9K", // password "$2a$10$LOhUxhEcS0vgDPv/jkXvCurNb7LjP9xUlEolJGk.Uhgikqc6FtIOi", // PassWord1 "$2a$10$j9da7SGiaakWhzIms9BtwemLUeIhSEphGUQ3XSlvYgpYsGnGCKRBa", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1Test.java b/src/test/java/fr/xephi/authme/security/crypts/CrazyCrypt1Test.java similarity index 81% rename from src/test/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1Test.java rename to src/test/java/fr/xephi/authme/security/crypts/CrazyCrypt1Test.java index fa27de3b1..f98ece5ab 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/CrazyCrypt1Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link CRAZYCRYPT1}. + * Test for {@link CrazyCrypt1}. */ -public class CRAZYCRYPT1Test extends AbstractEncryptionMethodTest { +public class CrazyCrypt1Test extends AbstractEncryptionMethodTest { - public CRAZYCRYPT1Test() { - super(new CRAZYCRYPT1(), + public CrazyCrypt1Test() { + super(new CrazyCrypt1(), "d5c76eb36417d4e97ec62609619e40a9e549a2598d0dab5a7194fd997a9305af78de2b93f958e150d19dd1e7f821043379ddf5f9c7f352bf27df91ae4913f3e8", // password "49c63f827c88196871e344e589bd46cc4fa6db3c27801bbad5374c0d216381977627c1d76f2114667d5dd117e046f7493eb06e4f461f4f848aa08f6f40a3e934", // PassWord1 "6fefb0233bab6e6efb9c16f82cb0d8f569488905e2dae0e7c9dde700e7363da67213d37c44bc15f4a05854c9c21e5688389d416413c7309398aa96cb1f341d08", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/DOUBLEMD5Test.java b/src/test/java/fr/xephi/authme/security/crypts/DoubleMd5Test.java similarity index 67% rename from src/test/java/fr/xephi/authme/security/crypts/DOUBLEMD5Test.java rename to src/test/java/fr/xephi/authme/security/crypts/DoubleMd5Test.java index 530399516..cc12df9e5 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/DOUBLEMD5Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/DoubleMd5Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link DOUBLEMD5}. + * Test for {@link DoubleMd5}. */ -public class DOUBLEMD5Test extends AbstractEncryptionMethodTest { +public class DoubleMd5Test extends AbstractEncryptionMethodTest { - public DOUBLEMD5Test() { - super(new DOUBLEMD5(), + public DoubleMd5Test() { + super(new DoubleMd5(), "696d29e0940a4957748fe3fc9efd22a3", // password "c77aa2024d9fb7233a2872452d601aba", // PassWord1 "fbd5790af706ec19f8a7ef161878758b", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/IPB3Test.java b/src/test/java/fr/xephi/authme/security/crypts/Ipb3Test.java similarity index 75% rename from src/test/java/fr/xephi/authme/security/crypts/IPB3Test.java rename to src/test/java/fr/xephi/authme/security/crypts/Ipb3Test.java index cf42567eb..984f3d217 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/IPB3Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Ipb3Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link IPB3}. + * Test for {@link Ipb3}. */ -public class IPB3Test extends AbstractEncryptionMethodTest { +public class Ipb3Test extends AbstractEncryptionMethodTest { - public IPB3Test() { - super(new IPB3(), + public Ipb3Test() { + super(new Ipb3(), new HashedPassword("f8ecea1ce42b5babef369ff7692dbe3f", "1715b"), //password new HashedPassword("40a93731a931352e0619cdf09b975040", "ba91c"), //PassWord1 new HashedPassword("a77ca982373946d5800430bd2947ba11", "a7725"), //&^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/IPB4Test.java b/src/test/java/fr/xephi/authme/security/crypts/Ipb4Test.java similarity index 81% rename from src/test/java/fr/xephi/authme/security/crypts/IPB4Test.java rename to src/test/java/fr/xephi/authme/security/crypts/Ipb4Test.java index 28c76a09f..5f71c23d7 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/IPB4Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Ipb4Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link IPB4}. + * Test for {@link Ipb4}. */ -public class IPB4Test extends AbstractEncryptionMethodTest { +public class Ipb4Test extends AbstractEncryptionMethodTest { - public IPB4Test() { - super(new IPB4(), + public Ipb4Test() { + super(new Ipb4(), new HashedPassword("$2a$13$leEvXu77OIwPwNvtZIJvaeAx8EItGHuR3nIlq8416g0gXeJaQdrr2", "leEvXu77OIwPwNvtZIJval"), //password new HashedPassword("$2a$13$xyTTP9zhQQtRRKIJPv5AuuOGJ6Ni9FLbDhcuIAcPjt3XzCxIWe3Uu", "xyTTP9zhQQtRRKIJPv5Au3"), //PassWord1 new HashedPassword("$2a$13$rGBrqErm9DZyzbxIGHlgf.xfA15/4d5Ay/TK.3y9lG3AljcoG9Lsi", "rGBrqErm9DZyzbxIGHlgfN"), //&^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/JoomlaTest.java b/src/test/java/fr/xephi/authme/security/crypts/JoomlaTest.java index f46d51d83..d2673b34c 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/JoomlaTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/JoomlaTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link JOOMLA}. + * Test for {@link Joomla}. */ public class JoomlaTest extends AbstractEncryptionMethodTest { public JoomlaTest() { - super(new JOOMLA(), + super(new Joomla(), "b18c99813cd96df3a706652f47177490:377c4aaf92c5ed57711306909e6065ca", // password "c5af71da91a8841d95937ba24a5b7fdb:07068e5850930b794526a614438cafc7", // PassWord1 "f5fccd5166af7080833d7c7a6a531295:7cb6eeabcfac67ffe1341ec43375a9e6", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/Md5Test.java b/src/test/java/fr/xephi/authme/security/crypts/Md5Test.java index 0c9d67cf1..c07918371 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/Md5Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Md5Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link MD5}. + * Test for {@link Md5}. */ public class Md5Test extends AbstractEncryptionMethodTest { public Md5Test() { - super(new MD5(), + super(new Md5(), "5f4dcc3b5aa765d61d8327deb882cf99", // password "f2126d405f46ed603ff5b2950f062c96", // PassWord1 "0833dcd2bc741f90c46bbac5498fd08f", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/MD5VBTest.java b/src/test/java/fr/xephi/authme/security/crypts/Md5vBTest.java similarity index 74% rename from src/test/java/fr/xephi/authme/security/crypts/MD5VBTest.java rename to src/test/java/fr/xephi/authme/security/crypts/Md5vBTest.java index acb823e66..eac20fcd1 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/MD5VBTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Md5vBTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link MD5VB}. + * Test for {@link Md5vB}. */ -public class MD5VBTest extends AbstractEncryptionMethodTest { +public class Md5vBTest extends AbstractEncryptionMethodTest { - public MD5VBTest() { - super(new MD5VB(), + public Md5vBTest() { + super(new Md5vB(), "$MD5vb$bd9832fffa287321$5006d371fcb813f2347987f902a024ad", // password "$MD5vb$5e492c1166b5a828$c954fa5ee561700a097826971653b57f", // PassWord1 "$MD5vb$3ec43cd46a61d70b$59687c0976f2e327b1245c8063f7008c", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/MYBBTest.java b/src/test/java/fr/xephi/authme/security/crypts/MyBBTest.java similarity index 76% rename from src/test/java/fr/xephi/authme/security/crypts/MYBBTest.java rename to src/test/java/fr/xephi/authme/security/crypts/MyBBTest.java index 101f475ff..fabd9f35d 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/MYBBTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/MyBBTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link MYBB}. + * Test for {@link MyBB}. */ -public class MYBBTest extends AbstractEncryptionMethodTest { +public class MyBBTest extends AbstractEncryptionMethodTest { - public MYBBTest() { - super(new MYBB(), + public MyBBTest() { + super(new MyBB(), new HashedPassword("57c7a16d860833db5030738f5a465d2b", "acdc14e6"), //password new HashedPassword("08fbdf721f2c42d9780b7d66df0ba830", "792fd7fb"), //PassWord1 new HashedPassword("d602f38fb59ad9e185d5604f5d4ddb36", "4b5534a4"), //&^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/PHPBBTest.java b/src/test/java/fr/xephi/authme/security/crypts/PhpBBTest.java similarity index 69% rename from src/test/java/fr/xephi/authme/security/crypts/PHPBBTest.java rename to src/test/java/fr/xephi/authme/security/crypts/PhpBBTest.java index 61a423609..f495659ec 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/PHPBBTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/PhpBBTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link PHPBB}. + * Test for {@link PhpBB}. */ -public class PHPBBTest extends AbstractEncryptionMethodTest { +public class PhpBBTest extends AbstractEncryptionMethodTest { - public PHPBBTest() { - super(new PHPBB(), + public PhpBBTest() { + super(new PhpBB(), "$H$7MaSGQb0xe3Fp/a.Q.Ewpw.UKfCv.t0", // password "$H$7ESfAVjzqajC7fJFcZKZIhyds41MuW.", // PassWord1 "$H$7G65SXRPbR69jLg.qZTjtqsw36Ciw7.", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/PHPFUSIONTest.java b/src/test/java/fr/xephi/authme/security/crypts/PhpFusionTest.java similarity index 79% rename from src/test/java/fr/xephi/authme/security/crypts/PHPFUSIONTest.java rename to src/test/java/fr/xephi/authme/security/crypts/PhpFusionTest.java index 0b10c1c72..a08a6ad8c 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/PHPFUSIONTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/PhpFusionTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link PHPFUSION}. + * Test for {@link PhpFusion}. */ -public class PHPFUSIONTest extends AbstractEncryptionMethodTest { +public class PhpFusionTest extends AbstractEncryptionMethodTest { - public PHPFUSIONTest() { - super(new PHPFUSION(), + public PhpFusionTest() { + super(new PhpFusion(), new HashedPassword("f7a606c4eb3fcfbc382906476e05b06f21234a77d1a4eacc0f93f503deb69e70", "6cd1c97c55cb"), // password new HashedPassword("8a9b7bb706a3347e5f684a7cb905bfb26b9a0d099358064139ab3ed1a66aeb2b", "d6012370b73f"), // PassWord1 new HashedPassword("43f2f23f44c8f89e2dbf06050bc8c77dbcdf71a7b5d28c87ec657d474e63d62d", "f75400a209a4"), // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/ROYALAUTHTest.java b/src/test/java/fr/xephi/authme/security/crypts/RoyalAuthTest.java similarity index 81% rename from src/test/java/fr/xephi/authme/security/crypts/ROYALAUTHTest.java rename to src/test/java/fr/xephi/authme/security/crypts/RoyalAuthTest.java index 0de9c2f39..7734dc03a 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/ROYALAUTHTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/RoyalAuthTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link ROYALAUTH}. + * Test for {@link RoyalAuth}. */ -public class ROYALAUTHTest extends AbstractEncryptionMethodTest { +public class RoyalAuthTest extends AbstractEncryptionMethodTest { - public ROYALAUTHTest() { - super(new ROYALAUTH(), + public RoyalAuthTest() { + super(new RoyalAuth(), "5d21ef9236896bc4ac508e524e2da8a0def555dac1cdfc7259d62900d1d3f553826210c369870673ae2cf1c41abcf4f92670d76af1db044d33559324f5c2a339", // password "ecc685f4328bc54093c086ced66c5c11855e117ea22940632d5c0f55fff84d94bfdcc74e05f5d95bbdd052823a7057910748bc1c7a07af96b3e86731a4f11794", // PassWord1 "2c0b4674f7c2c266db13ae4382cbeee3083167a774f6e73793a6268a0b8b2c3c6b324a99596f4a7958e58c5311c77e25975a3b517ce17adfc4eaece821e3dd19", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/SALTED2MD5Test.java b/src/test/java/fr/xephi/authme/security/crypts/Salted2Md5Test.java similarity index 83% rename from src/test/java/fr/xephi/authme/security/crypts/SALTED2MD5Test.java rename to src/test/java/fr/xephi/authme/security/crypts/Salted2Md5Test.java index 20ec99fe8..fc34a13fc 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/SALTED2MD5Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Salted2Md5Test.java @@ -7,12 +7,12 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Test for {@link SALTED2MD5}. + * Test for {@link Salted2Md5}. */ -public class SALTED2MD5Test extends AbstractEncryptionMethodTest { +public class Salted2Md5Test extends AbstractEncryptionMethodTest { - public SALTED2MD5Test() { - super(new SALTED2MD5(mockSettings()), + public Salted2Md5Test() { + super(new Salted2Md5(mockSettings()), new HashedPassword("9f3d13dc01a6fe61fd669954174399f3", "9b5f5749"), // password new HashedPassword("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32"), // PassWord1 new HashedPassword("38dcb83cc68424afe3cda012700c2bb1", "eb2c3394"), // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/SALTEDSHA512Test.java b/src/test/java/fr/xephi/authme/security/crypts/SaltedSha512Test.java similarity index 84% rename from src/test/java/fr/xephi/authme/security/crypts/SALTEDSHA512Test.java rename to src/test/java/fr/xephi/authme/security/crypts/SaltedSha512Test.java index 94cc78e82..98ab06153 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/SALTEDSHA512Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/SaltedSha512Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link SALTEDSHA512}. + * Test for {@link SaltedSha512}. */ -public class SALTEDSHA512Test extends AbstractEncryptionMethodTest { +public class SaltedSha512Test extends AbstractEncryptionMethodTest { - public SALTEDSHA512Test() { - super(new SALTEDSHA512(), + public SaltedSha512Test() { + super(new SaltedSha512(), new HashedPassword("dea7a37cecf5384ae8e347fd1411efb51364b6ba1b328695de3b354612c1d7010807e8b7051c40f740e498490e1f133e2c2408327d13fbdd68e1b1f6d548e624", "29f8a3c52147f987fee7ba3e0fb311bd"), // password new HashedPassword("7c06225aac574d2dc7c81a2ed306637adf025715f52083e05bdab014faaa234e24a97d0e69ea0108dfa77cc9228e58be319ee677e679b5d1ad168d40e50a42f6", "8ea37b85d020b98f60c0fe9b8ec9296c"), // PassWord1 new HashedPassword("55711adbe03c9616f3505f0d57077fdd528c32243eb6f9840c1a6ff9e553940d6b89790750ebd52ebda63ca793fbe9980d54057af40836820c648750fe22d49c", "9f58079631ef21d32b4710694f1f461b"), // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/Sha1Test.java b/src/test/java/fr/xephi/authme/security/crypts/Sha1Test.java index 620cef270..2bd5543f3 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/Sha1Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Sha1Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link SHA1}. + * Test for {@link Sha1}. */ public class Sha1Test extends AbstractEncryptionMethodTest { public Sha1Test() { - super(new SHA1(), + super(new Sha1(), "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", // password "285d0c707f9644b75e1a87a62f25d0efb56800f0", // PassWord1 "a42ef8e61e890af80461ca5dcded25cbfcf407a4", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/Sha256Test.java b/src/test/java/fr/xephi/authme/security/crypts/Sha256Test.java index 3257fe1f5..b9f751654 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/Sha256Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Sha256Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link SHA256}. + * Test for {@link Sha256}. */ public class Sha256Test extends AbstractEncryptionMethodTest { public Sha256Test() { - super(new SHA256(), + super(new Sha256(), "$SHA$11aa0706173d7272$dbba96681c2ae4e0bfdf226d70fbbc5e4ee3d8071faa613bc533fe8a64817d10", // password "$SHA$3c72a18a29b08d40$8e50a7a4f69a80f4893dc921eac84bd74b3f9ebfa22908302c9965eac3aa45e5", // PassWord1 "$SHA$584cea1cfab90030$adc006330e73d81e463fe02a4fe9b17bdbbcc05955bff72fb27cf2089f0b3859", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/Sha512Test.java b/src/test/java/fr/xephi/authme/security/crypts/Sha512Test.java index 17ba989c6..2871cd3a2 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/Sha512Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Sha512Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link SHA512}. + * Test for {@link Sha512}. */ public class Sha512Test extends AbstractEncryptionMethodTest { public Sha512Test() { - super(new SHA512(), + super(new Sha512(), "b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86", // password "ae9942149995a8171391625b36da134d5e288c721650d7c8d2d464fb49a49f3f551e4916ab1e097d9dd1201b01d69b1dccdefa3d2524a66092fb61b3df6e7e71", // PassWord1 "8c4f3df78db191142d819a72c16058b9e1ea41ae9b1649e1184eb89e30344c51c9c71039c483cf2f1b76b51480d8459d7eb3cfbaa24b07f2041d1551af4ead75", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/SMFTest.java b/src/test/java/fr/xephi/authme/security/crypts/SmfTest.java similarity index 72% rename from src/test/java/fr/xephi/authme/security/crypts/SMFTest.java rename to src/test/java/fr/xephi/authme/security/crypts/SmfTest.java index 8ba3b1973..53803334a 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/SMFTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/SmfTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link SMF}. + * Test for {@link Smf}. */ -public class SMFTest extends AbstractEncryptionMethodTest { +public class SmfTest extends AbstractEncryptionMethodTest { - public SMFTest() { - super(new SMF(), + public SmfTest() { + super(new Smf(), "9b361c66977bb059d460a20d3c21fb3394772df5", // password "31a560bdd095a837945d46add1605108ba87b268", // PassWord1 "8d4b84544e0891be8c183fe9b1003cfac18c51a1", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/WBB3Test.java b/src/test/java/fr/xephi/authme/security/crypts/Wbb3Test.java similarity index 81% rename from src/test/java/fr/xephi/authme/security/crypts/WBB3Test.java rename to src/test/java/fr/xephi/authme/security/crypts/Wbb3Test.java index d838c14fc..7443253fe 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/WBB3Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Wbb3Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link WBB3}. + * Test for {@link Wbb3}. */ -public class WBB3Test extends AbstractEncryptionMethodTest { +public class Wbb3Test extends AbstractEncryptionMethodTest { - public WBB3Test() { - super(new WBB3(), + public Wbb3Test() { + super(new Wbb3(), new HashedPassword("8df818ef7d56075ab2744f74b98ad68a375ccac4", "b7415b355492ea60314f259a35733a3092c03e3f"), // password new HashedPassword("106da5cf5df92cb845e12cf62cbdb5235b6dc693", "6110f19b2b52910dccf592a19c59126873f42e69"), // PassWord1 new HashedPassword("940a9fb7acec0178c6691e8b3c14bd7d789078b1", "f9dd501ff3d1bf74904f9e89649e378429af56e7"), // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/WBB4Test.java b/src/test/java/fr/xephi/authme/security/crypts/Wbb4Test.java similarity index 75% rename from src/test/java/fr/xephi/authme/security/crypts/WBB4Test.java rename to src/test/java/fr/xephi/authme/security/crypts/Wbb4Test.java index 5b714cab3..6c5459f60 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/WBB4Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Wbb4Test.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link WBB4}. + * Test for {@link Wbb4}. */ -public class WBB4Test extends AbstractEncryptionMethodTest { +public class Wbb4Test extends AbstractEncryptionMethodTest { - public WBB4Test() { - super(new WBB4(), + public Wbb4Test() { + super(new Wbb4(), "$2a$08$7DGr.wROqEPe0Z3XJS7n5.k.QWehovLHbpI.UkdfRb4ns268WsR6C", // password "$2a$08$yWWVUA4PB4mqW.0wyIvV3OdoH492HuLk5L3iaqUrpRK2.2zn08d/K", // PassWord1 "$2a$08$EHXUFt7bTT9Fnsu22KWvF.QDssiosV8YzH8CyWqulB/ckOA7qioJG", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/WHIRLPOOLTest.java b/src/test/java/fr/xephi/authme/security/crypts/WhirlpoolTest.java similarity index 81% rename from src/test/java/fr/xephi/authme/security/crypts/WHIRLPOOLTest.java rename to src/test/java/fr/xephi/authme/security/crypts/WhirlpoolTest.java index 1fbc94fd8..76cef4b05 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/WHIRLPOOLTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/WhirlpoolTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link WHIRLPOOL}. + * Test for {@link Whirlpool}. */ -public class WHIRLPOOLTest extends AbstractEncryptionMethodTest { +public class WhirlpoolTest extends AbstractEncryptionMethodTest { - public WHIRLPOOLTest() { - super(new WHIRLPOOL(), + public WhirlpoolTest() { + super(new Whirlpool(), "74DFC2B27ACFA364DA55F93A5CAEE29CCAD3557247EDA238831B3E9BD931B01D77FE994E4F12B9D4CFA92A124461D2065197D8CF7F33FC88566DA2DB2A4D6EAE", // password "819B4CBD26508E39EA76BFE102DCF2ACC87A446747CAB0BD88522B0822A724583E81B6A4BD2CE255DB694E530B659F47D434EEB50344A02F50B64414C9671583", // PassWord1 "71ECB0E5AEAB006F5336348076AA6A8E46075AEC9E010C7055BA1334B57746F2A9D8A8799BDD9B7EB4AB7544A59D25F469C8BCA2067508ACBA62A929260A1E17", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/WORDPRESSTest.java b/src/test/java/fr/xephi/authme/security/crypts/WordpressTest.java similarity index 77% rename from src/test/java/fr/xephi/authme/security/crypts/WORDPRESSTest.java rename to src/test/java/fr/xephi/authme/security/crypts/WordpressTest.java index 49a16d65b..0c444c2f8 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/WORDPRESSTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/WordpressTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link WORDPRESS}. + * Test for {@link Wordpress}. */ -public class WORDPRESSTest extends AbstractEncryptionMethodTest { +public class WordpressTest extends AbstractEncryptionMethodTest { - public WORDPRESSTest() { - super(new WORDPRESS(), + public WordpressTest() { + super(new Wordpress(), "$P$B9wyjxuU4yrfjnnHNGSzH9ti9CC0Os1", // password "$P$BjzPjjzPjjkRzvGGRTyYu0sNqcz6Ci0", // PassWord1 "$P$BjzPjjzPjrAOyB1V0WFdpisgCTFx.N/", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/XAUTHTest.java b/src/test/java/fr/xephi/authme/security/crypts/XAuthTest.java similarity index 84% rename from src/test/java/fr/xephi/authme/security/crypts/XAUTHTest.java rename to src/test/java/fr/xephi/authme/security/crypts/XAuthTest.java index 4fcd8039b..877a81ee1 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/XAUTHTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/XAuthTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link XAUTH}. + * Test for {@link XAuth}. */ -public class XAUTHTest extends AbstractEncryptionMethodTest { +public class XAuthTest extends AbstractEncryptionMethodTest { - public XAUTHTest() { - super(new XAUTH(), + public XAuthTest() { + super(new XAuth(), "e54d4916577410d26d2f6e9362445463dab9ffdff9a67ed3b74d3f2312bc8fab84f653fcb88ad8338793ef8a6d0a1162105e46ec24f0dcb52355c634e3e6439f45444b09c715", // password "d54489a4fd4732ee03d56810ab92944096e3d49335266adeecfbc12567abb3ff744761b33a1fcc4d04739e377775c788e4baace3caf35c7b9176b82b1fe3472e4cbdc5a43214", // PassWord1 "ce6404c1092fb5abf0a72f9c4327bfe8f4cdc4b8dc90ee6ca35c42b8ae9481b89c2559bb60b99ff2b57a102cfced40b8e2f5ef481400c9e6f79445017fc763b1cc27f4c2df36", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/security/crypts/XFBCRYPTTest.java b/src/test/java/fr/xephi/authme/security/crypts/XfBCryptTest.java similarity index 73% rename from src/test/java/fr/xephi/authme/security/crypts/XFBCRYPTTest.java rename to src/test/java/fr/xephi/authme/security/crypts/XfBCryptTest.java index 4edafbdd0..af1f4589b 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/XFBCRYPTTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/XfBCryptTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.security.crypts; /** - * Test for {@link XFBCRYPT}. + * Test for {@link XfBCrypt}. */ -public class XFBCRYPTTest extends AbstractEncryptionMethodTest { +public class XfBCryptTest extends AbstractEncryptionMethodTest { - public XFBCRYPTTest() { - super(new XFBCRYPT(), + public XfBCryptTest() { + super(new XfBCrypt(), "$2a$10$UtuON/ZG.x8EWG/zQbryB.BHfQVrfxk3H7qykzP.UJQ8YiLjZyfqq", // password "$2a$10$Q.ocUo.YtHTdI4nu3pcpKun6BILcmWHm541ANULucmuU/ps1QKY4K", // PassWord1 "$2a$10$yHjm02.K4HP5iFU1F..yLeTeo7PWZVbKAr/QGex5jU4.J3mdq/uuO", // &^%te$t?Pw@_ diff --git a/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java b/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java index 597f89a76..6f8695644 100644 --- a/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java @@ -9,10 +9,14 @@ import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import java.util.Collection; @@ -20,9 +24,13 @@ import java.util.Collection; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link BukkitService}. @@ -38,10 +46,13 @@ public class BukkitServiceTest { private Settings settings; @Mock private Server server; + @Mock + private BukkitScheduler scheduler; @Before public void constructBukkitService() { ReflectionTestUtils.setField(Bukkit.class, null, "server", server); + given(server.getScheduler()).willReturn(scheduler); given(settings.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); bukkitService = new BukkitService(authMe, settings); } @@ -101,6 +112,191 @@ public class BukkitServiceTest { verify(server).dispatchCommand(consoleSender, command); } + @Test + public void shouldScheduleSyncDelayedTask() { + // given + Runnable task = () -> {/* noop */}; + given(scheduler.scheduleSyncDelayedTask(authMe, task)).willReturn(123); + + // when + int taskId = bukkitService.scheduleSyncDelayedTask(task); + + // then + verify(scheduler, only()).scheduleSyncDelayedTask(authMe, task); + assertThat(taskId, equalTo(123)); + } + + @Test + public void shouldScheduleSyncDelayedTaskWithDelay() { + // given + Runnable task = () -> {/* noop */}; + int delay = 3; + given(scheduler.scheduleSyncDelayedTask(authMe, task, delay)).willReturn(44); + + // when + int taskId = bukkitService.scheduleSyncDelayedTask(task, delay); + + // then + verify(scheduler, only()).scheduleSyncDelayedTask(authMe, task, delay); + assertThat(taskId, equalTo(44)); + } + + @Test + public void shouldScheduleSyncTask() { + // given + BukkitService spy = Mockito.spy(bukkitService); + doReturn(1).when(spy).scheduleSyncDelayedTask(any(Runnable.class)); + Runnable task = mock(Runnable.class); + + // when + spy.scheduleSyncTaskFromOptionallyAsyncTask(task); + + // then + verify(spy).scheduleSyncDelayedTask(task); + verifyZeroInteractions(task); + } + + @Test + public void shouldRunTaskDirectly() { + // given + given(settings.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(false); + bukkitService.reload(settings); + BukkitService spy = Mockito.spy(bukkitService); + Runnable task = mock(Runnable.class); + + // when + spy.scheduleSyncTaskFromOptionallyAsyncTask(task); + + // then + verify(task).run(); + verify(spy, only()).scheduleSyncTaskFromOptionallyAsyncTask(task); + } + + @Test + public void shouldRunTask() { + // given + Runnable task = () -> {/* noop */}; + BukkitTask bukkitTask = mock(BukkitTask.class); + given(scheduler.runTask(authMe, task)).willReturn(bukkitTask); + + // when + BukkitTask resultingTask = bukkitService.runTask(task); + + // then + assertThat(resultingTask, equalTo(bukkitTask)); + verify(scheduler, only()).runTask(authMe, task); + } + + @Test + public void shouldRunTaskLater() { + // given + Runnable task = () -> {/* noop */}; + BukkitTask bukkitTask = mock(BukkitTask.class); + long delay = 400; + given(scheduler.runTaskLater(authMe, task, delay)).willReturn(bukkitTask); + + // when + BukkitTask resultingTask = bukkitService.runTaskLater(task, delay); + + // then + assertThat(resultingTask, equalTo(bukkitTask)); + verify(scheduler, only()).runTaskLater(authMe, task, delay); + } + + @Test + public void shouldRunTaskInAsync() { + // given + Runnable task = mock(Runnable.class); + BukkitService spy = Mockito.spy(bukkitService); + doReturn(null).when(spy).runTaskAsynchronously(task); + + // when + spy.runTaskOptionallyAsync(task); + + // then + verifyZeroInteractions(task); + verify(spy).runTaskAsynchronously(task); + } + + @Test + public void shouldRunTaskDirectlyIfConfigured() { + // given + given(settings.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(false); + bukkitService.reload(settings); + BukkitService spy = Mockito.spy(bukkitService); + Runnable task = mock(Runnable.class); + + // when + spy.runTaskOptionallyAsync(task); + + // then + verify(task).run(); + verify(spy, only()).runTaskOptionallyAsync(task); + } + + @Test + public void shouldRunTaskAsynchronously() { + // given + Runnable task = () -> {/* noop */}; + BukkitTask bukkitTask = mock(BukkitTask.class); + given(scheduler.runTaskAsynchronously(authMe, task)).willReturn(bukkitTask); + + // when + BukkitTask resultingTask = bukkitService.runTaskAsynchronously(task); + + // then + assertThat(resultingTask, equalTo(bukkitTask)); + verify(scheduler, only()).runTaskAsynchronously(authMe, task); + } + + @Test + public void shouldRunTaskTimerAsynchronously() { + // given + Runnable task = () -> {/* */}; + long delay = 20L; + long period = 4000L; + BukkitTask bukkitTask = mock(BukkitTask.class); + given(scheduler.runTaskTimerAsynchronously(authMe, task, delay, period)).willReturn(bukkitTask); + + // when + BukkitTask resultingTask = bukkitService.runTaskTimerAsynchronously(task, delay, period); + + // then + assertThat(resultingTask, equalTo(bukkitTask)); + verify(scheduler).runTaskTimerAsynchronously(authMe, task, delay, period); + } + + @Test + public void shouldRunTaskTimer() { + // given + BukkitRunnable bukkitRunnable = mock(BukkitRunnable.class); + long delay = 20; + long period = 80; + BukkitTask bukkitTask = mock(BukkitTask.class); + given(bukkitRunnable.runTaskTimer(authMe, delay, period)).willReturn(bukkitTask); + + // when + BukkitTask result = bukkitService.runTaskTimer(bukkitRunnable, delay, period); + + // then + assertThat(result, equalTo(bukkitTask)); + verify(bukkitRunnable).runTaskTimer(authMe, delay, period); + } + + @Test + public void shouldBroadcastMessage() { + // given + String message = "Important message to all"; + given(server.broadcastMessage(message)).willReturn(24); + + // when + int result = bukkitService.broadcastMessage(message); + + // then + assertThat(result, equalTo(24)); + verify(server).broadcastMessage(message); + } + // Note: This method is used through reflections public static Player[] onlinePlayersImpl() { return new Player[]{ diff --git a/src/test/java/fr/xephi/authme/service/CommonServiceTest.java b/src/test/java/fr/xephi/authme/service/CommonServiceTest.java index ff992bfeb..ad0b7e73f 100644 --- a/src/test/java/fr/xephi/authme/service/CommonServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/CommonServiceTest.java @@ -84,21 +84,6 @@ public class CommonServiceTest { verify(messages).send(sender, key, replacements); } - @Test - public void shouldRetrieveMessage() { - // given - MessageKey key = MessageKey.ACCOUNT_NOT_ACTIVATED; - String[] lines = new String[]{"First message line", "second line"}; - given(messages.retrieve(key)).willReturn(lines); - - // when - String[] result = commonService.retrieveMessage(key); - - // then - assertThat(result, equalTo(lines)); - verify(messages).retrieve(key); - } - @Test public void shouldRetrieveSingleMessage() { // given @@ -134,13 +119,11 @@ public class CommonServiceTest { // given Player player = mock(Player.class); AuthGroupType type = AuthGroupType.LOGGED_IN; - given(authGroupHandler.setGroup(player, type)).willReturn(true); // when - boolean result = commonService.setGroup(player, type); + commonService.setGroup(player, type); // then verify(authGroupHandler).setGroup(player, type); - assertThat(result, equalTo(true)); } } diff --git a/src/test/java/fr/xephi/authme/service/MigrationServiceTest.java b/src/test/java/fr/xephi/authme/service/MigrationServiceTest.java index fa82b97ae..11a0f253f 100644 --- a/src/test/java/fr/xephi/authme/service/MigrationServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/MigrationServiceTest.java @@ -5,7 +5,7 @@ import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.security.crypts.SHA256; +import fr.xephi.authme.security.crypts.Sha256; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.SecuritySettings; import org.junit.BeforeClass; @@ -28,6 +28,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.hamcrest.MockitoHamcrest.argThat; +import static fr.xephi.authme.AuthMeMatchers.equalToHash; /** * Test for {@link MigrationService}. @@ -42,7 +43,7 @@ public class MigrationServiceTest { private DataSource dataSource; @Mock - private SHA256 sha256; + private Sha256 sha256; @BeforeClass public static void setUpLogger() { @@ -122,7 +123,7 @@ public class MigrationServiceTest { .build(); } - private static void setSha256MockToUppercase(SHA256 sha256) { + private static void setSha256MockToUppercase(Sha256 sha256) { given(sha256.computeHash(anyString(), anyString())).willAnswer(new Answer() { @Override public HashedPassword answer(InvocationOnMock invocation) { diff --git a/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java new file mode 100644 index 000000000..48973bbfa --- /dev/null +++ b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java @@ -0,0 +1,73 @@ +package fr.xephi.authme.service; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link PasswordRecoveryService}. + */ +@RunWith(DelayedInjectionRunner.class) +public class PasswordRecoveryServiceTest { + + @InjectDelayed + private PasswordRecoveryService recoveryService; + + @Mock + private CommonService commonService; + + @Mock + private RecoveryCodeService codeService; + + @Mock + private DataSource dataSource; + + @Mock + private EmailService emailService; + + @Mock + private PasswordSecurity passwordSecurity; + + @Mock + private Messages messages; + + @BeforeInjecting + public void initSettings() { + given(commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS)).willReturn(40); + given(commonService.getProperty(SecuritySettings.PASSWORD_CHANGE_TIMEOUT)).willReturn(2); + } + + @Test + public void shouldSendRecoveryCode() { + // given + Player player = mock(Player.class); + String name = "Carl"; + given(player.getName()).willReturn(name); + String email = "test@example.com"; + String code = "qwerty"; + given(codeService.generateCode(name)).willReturn(code); + given(emailService.sendRecoveryCode(player.getName(), email, code)).willReturn(true); + + // when + recoveryService.createAndSendRecoveryCode(player, email); + + // then + verify(codeService).generateCode(name); + verify(emailService).sendRecoveryCode(name, email, code); + verify(commonService).send(player, MessageKey.RECOVERY_CODE_SENT); + } +} diff --git a/src/test/java/fr/xephi/authme/service/PluginHookServiceTest.java b/src/test/java/fr/xephi/authme/service/PluginHookServiceTest.java index d4e3f8cda..e07ea1c2e 100644 --- a/src/test/java/fr/xephi/authme/service/PluginHookServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/PluginHookServiceTest.java @@ -58,7 +58,7 @@ public class PluginHookServiceTest { assertThat(pluginHookService.isEssentialsAvailable(), equalTo(true)); } - // Note ljacqu 20160312: Cannot test with Multiverse or CombatTagPlus because their classes are declared final + // Note ljacqu 20160312: Cannot test with CombatTagPlus because its class is declared final @Test public void shouldHookIntoEssentialsAtInitialization() { diff --git a/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java b/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java index b06a01f82..acd26d933 100644 --- a/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/RecoveryCodeServiceTest.java @@ -4,15 +4,13 @@ import ch.jalu.injector.testing.BeforeInjecting; import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; import fr.xephi.authme.ReflectionTestUtils; -import fr.xephi.authme.service.RecoveryCodeService.ExpiringEntry; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.expiring.ExpiringMap; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import java.util.Map; - import static fr.xephi.authme.AuthMeMatchers.stringWithLength; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -35,6 +33,7 @@ public class RecoveryCodeServiceTest { public void initSettings() { given(settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID)).willReturn(4); given(settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH)).willReturn(5); + given(settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES)).willReturn(3); } @Test @@ -60,19 +59,34 @@ public class RecoveryCodeServiceTest { recoveryCodeService.generateCode(name); // then - ExpiringEntry entry = getCodeMap().get(name); - assertThat(entry.getCode(), stringWithLength(5)); + String code = getCodeMap().get(name); + assertThat(code, stringWithLength(5)); } @Test - public void shouldNotConsiderExpiredCode() { + public void playerHasTriesLeft() { // given - String player = "Cat"; - String code = "11F235"; - setCodeInMap(player, code, System.currentTimeMillis() - 500); + String player = "Dusty"; + recoveryCodeService.generateCode(player); // when - boolean result = recoveryCodeService.isCodeValid(player, code); + boolean result = recoveryCodeService.hasTriesLeft(player); + + // then + assertThat(result, equalTo(true)); + } + + @Test + public void playerHasNoTriesLeft() { + // given + String player = "Dusty"; + recoveryCodeService.generateCode(player); + recoveryCodeService.isCodeValid(player, "1st try"); + recoveryCodeService.isCodeValid(player, "2nd try"); + recoveryCodeService.isCodeValid(player, "3rd try"); + + // when + boolean result = recoveryCodeService.hasTriesLeft(player); // then assertThat(result, equalTo(false)); @@ -103,15 +117,15 @@ public class RecoveryCodeServiceTest { // then assertThat(recoveryCodeService.isCodeValid(player, code), equalTo(false)); assertThat(getCodeMap().get(player), nullValue()); + assertThat(getTriesCounter().get(player), equalTo(0)); } - private Map getCodeMap() { + private ExpiringMap getCodeMap() { return ReflectionTestUtils.getFieldValue(RecoveryCodeService.class, recoveryCodeService, "recoveryCodes"); } - private void setCodeInMap(String player, String code, long expiration) { - Map map = getCodeMap(); - map.put(player, new ExpiringEntry(code, expiration)); + private ExpiringMap getTriesCounter() { + return ReflectionTestUtils.getFieldValue(RecoveryCodeService.class, recoveryCodeService, "playerTries"); } } diff --git a/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java b/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java index 21ffa9525..3953d4efd 100644 --- a/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java @@ -4,24 +4,30 @@ import ch.jalu.injector.testing.BeforeInjecting; import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; import com.google.common.base.Strings; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.service.ValidationService.ValidationResult; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.service.ValidationService.ValidationResult; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import java.util.Arrays; import java.util.Collections; +import java.util.logging.Logger; import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; @@ -55,6 +61,7 @@ public class ValidationServiceTest { .willReturn(asList("unsafe", "other-unsafe")); given(settings.getProperty(EmailSettings.MAX_REG_PER_EMAIL)).willReturn(3); given(settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES)).willReturn(asList("name01", "npc")); + given(settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS)).willReturn(false); } @Test @@ -115,8 +122,8 @@ public class ValidationServiceTest { @Test public void shouldAcceptEmailWithEmptyLists() { // given - given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)).willReturn(Collections.emptyList()); - given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)).willReturn(Collections.emptyList()); // when boolean result = validationService.validateEmail("test@example.org"); @@ -130,7 +137,7 @@ public class ValidationServiceTest { // given given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)) .willReturn(asList("domain.tld", "example.com")); - given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)).willReturn(Collections.emptyList()); // when boolean result = validationService.validateEmail("TesT@Example.com"); @@ -144,7 +151,7 @@ public class ValidationServiceTest { // given given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)) .willReturn(asList("domain.tld", "example.com")); - given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)).willReturn(Collections.emptyList()); // when boolean result = validationService.validateEmail("email@other-domain.abc"); @@ -156,7 +163,7 @@ public class ValidationServiceTest { @Test public void shouldAcceptEmailNotInBlacklist() { // given - given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)).willReturn(Collections.emptyList()); given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)) .willReturn(asList("Example.org", "a-test-name.tld")); @@ -170,7 +177,7 @@ public class ValidationServiceTest { @Test public void shouldRejectEmailInBlacklist() { // given - given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(EmailSettings.DOMAIN_WHITELIST)).willReturn(Collections.emptyList()); given(settings.getProperty(EmailSettings.DOMAIN_BLACKLIST)) .willReturn(asList("Example.org", "a-test-name.tld")); @@ -263,8 +270,8 @@ public class ValidationServiceTest { @Test public void shouldNotInvokeGeoLiteApiIfCountryListsAreEmpty() { // given - given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(Collections.emptyList()); - given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(Collections.emptyList()); // when boolean result = validationService.isCountryAdmitted("addr"); @@ -278,7 +285,7 @@ public class ValidationServiceTest { public void shouldAcceptCountryInWhitelist() { // given given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(asList("ch", "it")); - given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(Collections.emptyList()); String ip = "127.0.0.1"; given(geoIpService.getCountryCode(ip)).willReturn("CH"); @@ -294,7 +301,7 @@ public class ValidationServiceTest { public void shouldRejectCountryMissingFromWhitelist() { // given given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(asList("ch", "it")); - given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(Collections.emptyList()); String ip = "123.45.67.89"; given(geoIpService.getCountryCode(ip)).willReturn("BR"); @@ -309,7 +316,7 @@ public class ValidationServiceTest { @Test public void shouldAcceptCountryAbsentFromBlacklist() { // given - given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(Collections.emptyList()); given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(asList("ch", "it")); String ip = "127.0.0.1"; given(geoIpService.getCountryCode(ip)).willReturn("BR"); @@ -325,7 +332,7 @@ public class ValidationServiceTest { @Test public void shouldRejectCountryInBlacklist() { // given - given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(Collections.emptyList()); + given(settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST)).willReturn(Collections.emptyList()); given(settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST)).willReturn(asList("ch", "it")); String ip = "123.45.67.89"; given(geoIpService.getCountryCode(ip)).willReturn("IT"); @@ -338,6 +345,54 @@ public class ValidationServiceTest { verify(geoIpService).getCountryCode(ip); } + @Test + public void shouldCheckNameRestrictions() { + // given + given(settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS)).willReturn(true); + given(settings.getProperty(RestrictionSettings.RESTRICTED_USERS)) + .willReturn(Arrays.asList("Bobby;127.0.0.4", "Tamara;32.24.16.8")); + validationService.reload(); + + Player bobby = mockPlayer("bobby", "127.0.0.4"); + Player tamara = mockPlayer("taMARA", "8.8.8.8"); + Player notRestricted = mockPlayer("notRestricted", "0.0.0.0"); + + // when + boolean isBobbyAdmitted = validationService.fulfillsNameRestrictions(bobby); + boolean isTamaraAdmitted = validationService.fulfillsNameRestrictions(tamara); + boolean isNotRestrictedAdmitted = validationService.fulfillsNameRestrictions(notRestricted); + + // then + assertThat(isBobbyAdmitted, equalTo(true)); + assertThat(isTamaraAdmitted, equalTo(false)); + assertThat(isNotRestrictedAdmitted, equalTo(true)); + } + + @Test + public void shouldLogWarningForInvalidRestrictionRule() { + // given + Logger logger = TestHelper.setupLogger(); + given(settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS)).willReturn(true); + given(settings.getProperty(RestrictionSettings.RESTRICTED_USERS)) + .willReturn(Arrays.asList("Bobby;127.0.0.4", "Tamara;")); + + // when + validationService.reload(); + + // then + ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class); + verify(logger).warning(stringCaptor.capture()); + assertThat(stringCaptor.getValue(), containsString("Tamara;")); + } + + private static Player mockPlayer(String name, String ip) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + TestHelper.mockPlayerIp(player, ip); + given(player.getAddress().getHostName()).willReturn("--"); + return player; + } + private static void assertErrorEquals(ValidationResult validationResult, MessageKey messageKey, String... args) { assertThat(validationResult.hasError(), equalTo(true)); assertThat(validationResult.getMessageKey(), equalTo(messageKey)); diff --git a/src/test/java/fr/xephi/authme/settings/SettingsConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/SettingsConsistencyTest.java index 88724a847..87d9b8aac 100644 --- a/src/test/java/fr/xephi/authme/settings/SettingsConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/SettingsConsistencyTest.java @@ -1,15 +1,31 @@ package fr.xephi.authme.settings; +import ch.jalu.configme.SectionComments; +import ch.jalu.configme.SettingsHolder; import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.properties.EnumProperty; import ch.jalu.configme.properties.Property; +import com.google.common.collect.ImmutableSet; +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.settings.properties.AuthMeSettingsRetriever; +import fr.xephi.authme.settings.properties.SecuritySettings; import org.junit.BeforeClass; import org.junit.Test; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.base.Preconditions.checkArgument; +import static fr.xephi.authme.ReflectionTestUtils.getFieldValue; import static org.junit.Assert.fail; /** @@ -22,6 +38,12 @@ public class SettingsConsistencyTest { */ private static final int MAX_COMMENT_LENGTH = 90; + /** + * Properties to exclude from the enum check. + */ + private static final Set> EXCLUDED_ENUM_PROPERTIES = + ImmutableSet.of(SecuritySettings.PASSWORD_HASH, SecuritySettings.LEGACY_HASHES); + private static ConfigurationData configurationData; @BeforeClass @@ -66,4 +88,115 @@ public class SettingsConsistencyTest { } } + @Test + public void shouldNotHaveVeryLongSectionCommentLines() { + // given + List sectionCommentMethods = getSectionCommentMethods(); + Set badMethods = new HashSet<>(); + + // when + for (Method method : sectionCommentMethods) { + boolean hasTooLongLine = getSectionComments(method).stream() + .anyMatch(line -> line.length() > MAX_COMMENT_LENGTH); + if (hasTooLongLine) { + badMethods.add(method); + } + } + + // then + if (!badMethods.isEmpty()) { + String methodList = badMethods.stream() + .map(m -> m.getName() + " in " + m.getDeclaringClass().getSimpleName()) + .collect(Collectors.joining("\n- ")); + fail("Found SectionComments methods with too long comments:\n- " + methodList); + } + } + + /** + * Gets all {@link SectionComments} methods from {@link SettingsHolder} implementations. + */ + @SuppressWarnings("unchecked") + private List getSectionCommentMethods() { + // Find all SettingsHolder classes + List> settingsClasses = + new ClassCollector(TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT + "settings/properties/") + .collectClasses(SettingsHolder.class); + checkArgument(!settingsClasses.isEmpty(), "Could not find any SettingsHolder classes"); + + // Find all @SectionComments methods in these classes + return settingsClasses.stream() + .map(Class::getDeclaredMethods) + .flatMap(Arrays::stream) + .filter(method -> method.isAnnotationPresent(SectionComments.class)) + .collect(Collectors.toList()); + } + + /** + * Returns all comments returned from the given SectionComments method, flattened into one list. + * + * @param sectionCommentsMethod the method whose comments should be retrieved + * @return flattened list of all comments provided by the method + */ + private static List getSectionComments(Method sectionCommentsMethod) { + // @SectionComments methods are static + Map comments = ReflectionTestUtils.invokeMethod(sectionCommentsMethod, null); + return comments.values().stream() + .flatMap(Arrays::stream) + .collect(Collectors.toList()); + } + + /** + * Checks that enum properties have all possible enum values listed in their comment + * so the user knows which values are available. + */ + @Test + public void shouldMentionAllEnumValues() { + // given + Map, Enum> invalidEnumProperties = new HashMap<>(); + + for (Property property : configurationData.getProperties()) { + // when + Class> enumClass = getEnumClass(property); + if (enumClass != null && !EXCLUDED_ENUM_PROPERTIES.contains(property)) { + String comments = String.join("\n", configurationData.getCommentsForSection(property.getPath())); + Arrays.stream(enumClass.getEnumConstants()) + .filter(e -> !comments.contains(e.name()) && !isDeprecated(e)) + .findFirst() + .ifPresent(e -> invalidEnumProperties.put(property, e)); + } + } + + // then + if (!invalidEnumProperties.isEmpty()) { + String invalidEnums = invalidEnumProperties.entrySet().stream() + .map(e -> e.getKey() + " does not mention " + e.getValue() + " and possibly others") + .collect(Collectors.joining("\n- ")); + + fail("Found enum properties that do not list all entries in the comments:\n- " + invalidEnums); + } + } + + /** + * Returns the enum class the property holds values for, if applicable. + * + * @param property the property to get the enum class from + * @return the enum class, or null if not available + */ + private static Class> getEnumClass(Property property) { + if (property instanceof EnumProperty) { + return getFieldValue(EnumProperty.class, (EnumProperty) property, "clazz"); + } else if (property instanceof EnumSetProperty) { + return getFieldValue(EnumSetProperty.class, (EnumSetProperty) property, "enumClass"); + } + return null; + } + + private static boolean isDeprecated(Enum enumValue) { + try { + return enumValue.getDeclaringClass().getField(enumValue.name()).isAnnotationPresent(Deprecated.class); + } catch (NoSuchFieldException e) { + throw new IllegalStateException("Could not fetch field for enum '" + enumValue + + "' in " + enumValue.getDeclaringClass()); + } + } } diff --git a/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java b/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java index 67893666f..da7c270be 100644 --- a/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java +++ b/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java @@ -18,7 +18,10 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import static fr.xephi.authme.TestHelper.getJarFile; +import static fr.xephi.authme.settings.properties.PluginSettings.ENABLE_PERMISSION_CHECK; import static fr.xephi.authme.settings.properties.PluginSettings.LOG_LEVEL; +import static fr.xephi.authme.settings.properties.PluginSettings.REGISTERED_GROUP; +import static fr.xephi.authme.settings.properties.PluginSettings.UNREGISTERED_GROUP; import static fr.xephi.authme.settings.properties.RegistrationSettings.DELAY_JOIN_MESSAGE; import static fr.xephi.authme.settings.properties.RegistrationSettings.REGISTER_SECOND_ARGUMENT; import static fr.xephi.authme.settings.properties.RegistrationSettings.REGISTRATION_TYPE; @@ -65,6 +68,9 @@ public class SettingsMigrationServiceTest { assertThat(settings.getProperty(LOG_LEVEL), equalTo(LogLevel.INFO)); assertThat(settings.getProperty(REGISTRATION_TYPE), equalTo(RegistrationType.EMAIL)); assertThat(settings.getProperty(REGISTER_SECOND_ARGUMENT), equalTo(RegisterSecondaryArgument.CONFIRMATION)); + assertThat(settings.getProperty(ENABLE_PERMISSION_CHECK), equalTo(true)); + assertThat(settings.getProperty(REGISTERED_GROUP), equalTo("unLoggedinGroup")); + assertThat(settings.getProperty(UNREGISTERED_GROUP), equalTo("")); // Check migration of old setting to email.html assertThat(Files.readLines(new File(dataFolder, "email.html"), StandardCharsets.UTF_8), diff --git a/src/test/java/fr/xephi/authme/settings/SettingsTest.java b/src/test/java/fr/xephi/authme/settings/SettingsTest.java index 42326771e..f7d2604c2 100644 --- a/src/test/java/fr/xephi/authme/settings/SettingsTest.java +++ b/src/test/java/fr/xephi/authme/settings/SettingsTest.java @@ -4,7 +4,6 @@ import ch.jalu.configme.configurationdata.ConfigurationData; import ch.jalu.configme.configurationdata.ConfigurationDataBuilder; import ch.jalu.configme.resource.PropertyResource; import fr.xephi.authme.TestHelper; -import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.TestConfiguration; import org.junit.Before; import org.junit.BeforeClass; @@ -16,11 +15,8 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; -import static org.hamcrest.Matchers.arrayContaining; -import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -45,26 +41,6 @@ public class SettingsTest { testPluginFolder = temporaryFolder.newFolder(); } - @Test - public void shouldLoadWelcomeMessage() throws IOException { - // given - String welcomeMessage = "This is my welcome message for testing\nBye!"; - File welcomeFile = new File(testPluginFolder, "welcome.txt"); - createFile(welcomeFile); - Files.write(welcomeFile.toPath(), welcomeMessage.getBytes()); - - PropertyResource resource = mock(PropertyResource.class); - given(resource.getBoolean(RegistrationSettings.USE_WELCOME_MESSAGE.getPath())).willReturn(true); - Settings settings = new Settings(testPluginFolder, resource, null, CONFIG_DATA); - - // when - String[] result = settings.getWelcomeMessage(); - - // then - assertThat(result, arrayWithSize(2)); - assertThat(result, arrayContaining(welcomeMessage.split("\\n"))); - } - @Test public void shouldLoadEmailMessage() throws IOException { // given diff --git a/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java b/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java index 1ca06911a..edd561925 100644 --- a/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java +++ b/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java @@ -5,9 +5,8 @@ import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; import com.google.common.io.Files; import fr.xephi.authme.TestHelper; -import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.Location; import org.bukkit.World; @@ -38,9 +37,6 @@ public class SpawnLoaderTest { @Mock private Settings settings; - @Mock - private DataSource dataSource; - @Mock private PluginHookService pluginHookService; diff --git a/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java b/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java new file mode 100644 index 000000000..37a3386af --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java @@ -0,0 +1,141 @@ +package fr.xephi.authme.settings; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.GeoIpService; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link WelcomeMessageConfiguration}. + */ +@RunWith(DelayedInjectionRunner.class) +public class WelcomeMessageConfigurationTest { + + @InjectDelayed + private WelcomeMessageConfiguration welcomeMessageConfiguration; + @Mock + private Server server; + @Mock + private BukkitService bukkitService; + @Mock + private GeoIpService geoIpService; + @Mock + private PlayerCache playerCache; + @DataFolder + private File testPluginFolder; + + private File welcomeFile; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeInjecting + public void createPluginFolder() throws IOException { + testPluginFolder = temporaryFolder.newFolder(); + welcomeFile = new File(testPluginFolder, "welcome.txt"); + welcomeFile.createNewFile(); + } + + @Test + public void shouldLoadWelcomeMessage() throws IOException { + // given + String welcomeMessage = "This is my welcome message for testing\nBye!"; + setWelcomeMessageAndReload(welcomeMessage); + Player player = mock(Player.class); + + // when + List result = welcomeMessageConfiguration.getWelcomeMessage(player); + + // then + assertThat(result, hasSize(2)); + assertThat(result, contains(welcomeMessage.split("\\n"))); + verifyZeroInteractions(player, playerCache, geoIpService, bukkitService, server); + } + + @Test + public void shouldReplaceNameAndIpAndCountry() throws IOException { + // given + String welcomeMessage = "Hello {PLAYER}, your IP is {IP}\nYour country is {COUNTRY}.\nWelcome to {SERVER}!"; + setWelcomeMessageAndReload(welcomeMessage); + + Player player = mock(Player.class); + given(player.getName()).willReturn("Bobby"); + TestHelper.mockPlayerIp(player, "123.45.66.77"); + given(geoIpService.getCountryName("123.45.66.77")).willReturn("Syldavia"); + given(server.getServerName()).willReturn("CrazyServer"); + + // when + List result = welcomeMessageConfiguration.getWelcomeMessage(player); + + // then + assertThat(result, hasSize(3)); + assertThat(result.get(0), equalTo("Hello Bobby, your IP is 123.45.66.77")); + assertThat(result.get(1), equalTo("Your country is Syldavia.")); + assertThat(result.get(2), equalTo("Welcome to CrazyServer!")); + verify(server, only()).getServerName(); + verifyZeroInteractions(playerCache); + } + + @Test + public void shouldApplyOtherReplacements() throws IOException { + // given + String welcomeMessage = "{ONLINE}/{MAXPLAYERS} online\n{LOGINS} logged in\nYour world is {WORLD}\nServer: {VERSION}"; + setWelcomeMessageAndReload(welcomeMessage); + given(bukkitService.getOnlinePlayers()).willReturn((List) Arrays.asList(mock(Player.class), mock(Player.class))); + given(server.getMaxPlayers()).willReturn(20); + given(playerCache.getLogged()).willReturn(1); + given(server.getBukkitVersion()).willReturn("Bukkit-456.77.8"); + + World world = mock(World.class); + given(world.getName()).willReturn("Hub"); + Player player = mock(Player.class); + given(player.getWorld()).willReturn(world); + + // when + List result = welcomeMessageConfiguration.getWelcomeMessage(player); + + // then + assertThat(result, hasSize(4)); + assertThat(result.get(0), equalTo("2/20 online")); + assertThat(result.get(1), equalTo("1 logged in")); + assertThat(result.get(2), equalTo("Your world is Hub")); + assertThat(result.get(3), equalTo("Server: Bukkit-456.77.8")); + } + + private void setWelcomeMessageAndReload(String welcomeMessage) { + try { + Files.write(welcomeFile.toPath(), welcomeMessage.getBytes()); + } catch (IOException e) { + throw new IllegalStateException("Could not write to '" + welcomeFile + "'", e); + } + welcomeMessageConfiguration.reload(); + } +} diff --git a/src/test/java/fr/xephi/authme/settings/commandconfig/CommandManagerTest.java b/src/test/java/fr/xephi/authme/settings/commandconfig/CommandManagerTest.java index fb8e4e40f..3ec3b4473 100644 --- a/src/test/java/fr/xephi/authme/settings/commandconfig/CommandManagerTest.java +++ b/src/test/java/fr/xephi/authme/settings/commandconfig/CommandManagerTest.java @@ -1,9 +1,9 @@ package fr.xephi.authme.settings.commandconfig; import com.google.common.io.Files; -import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.GeoIpService; import fr.xephi.authme.settings.SettingsMigrationService; import org.bukkit.entity.Player; import org.junit.Before; @@ -17,13 +17,7 @@ import org.mockito.junit.MockitoJUnitRunner; import java.io.File; import java.io.IOException; -import java.util.function.BiConsumer; -import static fr.xephi.authme.settings.commandconfig.CommandConfigTestHelper.isCommand; -import static java.lang.String.format; -import static org.hamcrest.Matchers.anEmptyMap; -import static org.hamcrest.Matchers.contains; -import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -31,6 +25,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link CommandManager}. @@ -41,104 +36,116 @@ public class CommandManagerTest { private static final String TEST_FILES_FOLDER = "/fr/xephi/authme/settings/commandconfig/"; private CommandManager manager; + private Player player; + @InjectMocks private CommandMigrationService commandMigrationService; - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Mock private BukkitService bukkitService; @Mock + private GeoIpService geoIpService; + @Mock private SettingsMigrationService settingsMigrationService; + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private File testFolder; @Before public void setup() throws IOException { testFolder = temporaryFolder.newFolder(); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldLoadCompleteFile() { - // given - copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.complete.yml"); - - // when - initManager(); - - // then - CommandConfig commandConfig = ReflectionTestUtils.getFieldValue(CommandManager.class, manager, "commandConfig"); - assertThat(commandConfig.getOnJoin().keySet(), contains("broadcast")); - assertThat(commandConfig.getOnJoin().values(), contains(isCommand("broadcast %p has joined", Executor.CONSOLE))); - assertThat(commandConfig.getOnRegister().keySet(), contains("announce", "notify")); - assertThat(commandConfig.getOnRegister().values(), contains( - isCommand("me I just registered", Executor.PLAYER), - isCommand("log %p registered", Executor.CONSOLE))); - assertThat(commandConfig.getOnLogin().keySet(), contains("welcome", "show_motd", "display_list")); - assertThat(commandConfig.getOnLogin().values(), contains( - isCommand("msg %p Welcome back", Executor.CONSOLE), - isCommand("motd", Executor.PLAYER), - isCommand("list", Executor.PLAYER))); - } - - @Test - public void shouldLoadIncompleteFile() { - // given - copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.incomplete.yml"); - - // when - initManager(); - - // then - CommandConfig commandConfig = ReflectionTestUtils.getFieldValue(CommandManager.class, manager, "commandConfig"); - assertThat(commandConfig.getOnJoin().values(), contains(isCommand("broadcast %p has joined", Executor.CONSOLE))); - assertThat(commandConfig.getOnLogin().values(), contains( - isCommand("msg %p Welcome back", Executor.CONSOLE), - isCommand("list", Executor.PLAYER))); - assertThat(commandConfig.getOnRegister(), anEmptyMap()); - } - - @Test - public void shouldExecuteCommandsOnJoin() { - // given - String name = "Bobby1"; - - // when - testCommandExecution(name, CommandManager::runCommandsOnJoin); - - // then - verify(bukkitService, only()).dispatchConsoleCommand(format("broadcast %s has joined", name)); - } - - @Test - public void shouldExecuteCommandsOnRegister() { - // given - String name = "luis"; - - // when - testCommandExecution(name, CommandManager::runCommandsOnRegister); - - // then - verify(bukkitService).dispatchCommand(any(Player.class), eq("me I just registered")); - verify(bukkitService).dispatchConsoleCommand(format("log %s registered", name)); - verifyNoMoreInteractions(bukkitService); + player = mockPlayer(); } @Test public void shouldExecuteCommandsOnLogin() { // given - String name = "plaYer01"; + copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.complete.yml"); + initManager(); // when - testCommandExecution(name, CommandManager::runCommandsOnLogin); + manager.runCommandsOnLogin(player); // then - verify(bukkitService).dispatchConsoleCommand(format("msg %s Welcome back", name)); + verify(bukkitService).dispatchConsoleCommand("msg Bobby Welcome back"); verify(bukkitService).dispatchCommand(any(Player.class), eq("motd")); verify(bukkitService).dispatchCommand(any(Player.class), eq("list")); verifyNoMoreInteractions(bukkitService); + verifyZeroInteractions(geoIpService); + } + + @Test + public void shouldExecuteCommandsOnLoginWithIncompleteConfig() { + // given + copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.incomplete.yml"); + initManager(); + + // when + manager.runCommandsOnLogin(player); + + // then + verify(bukkitService).dispatchConsoleCommand("msg Bobby Welcome back, bob"); + verify(bukkitService).dispatchCommand(any(Player.class), eq("list")); + verifyNoMoreInteractions(bukkitService); + verifyZeroInteractions(geoIpService); + } + + @Test + public void shouldExecuteCommandsOnJoin() { + // given + copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.complete.yml"); + initManager(); + + // when + manager.runCommandsOnJoin(player); + + // then + verify(bukkitService, only()).dispatchConsoleCommand("broadcast bob has joined"); + verifyZeroInteractions(geoIpService); + } + + @Test + public void shouldExecuteCommandsOnJoinWithIncompleteConfig() { + // given + copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.incomplete.yml"); + initManager(); + + // when + manager.runCommandsOnJoin(player); + + // then + verify(bukkitService, only()).dispatchConsoleCommand("broadcast Bobby has joined"); + verifyZeroInteractions(geoIpService); + } + + @Test + public void shouldExecuteCommandsOnRegister() { + // given + copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.complete.yml"); + initManager(); + + // when + manager.runCommandsOnRegister(player); + + // then + verify(bukkitService).dispatchCommand(any(Player.class), eq("me I just registered")); + verify(bukkitService).dispatchConsoleCommand("log Bobby (127.0.0.3, Syldavia) registered"); + verifyNoMoreInteractions(bukkitService); + } + + @Test + public void shouldExecuteCommandsOnRegisterWithIncompleteConfig() { + // given + copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.incomplete.yml"); + initManager(); + + // when + manager.runCommandsOnRegister(player); + + // then + verifyZeroInteractions(bukkitService, geoIpService); } @Test @@ -147,18 +154,8 @@ public class CommandManagerTest { TestHelper.validateHasOnlyPrivateEmptyConstructor(CommandSettingsHolder.class); } - - private void testCommandExecution(String playerName, BiConsumer testMethod) { - copyJarFileAsCommandsYml(TEST_FILES_FOLDER + "commands.complete.yml"); - initManager(); - Player player = mock(Player.class); - given(player.getName()).willReturn(playerName); - - testMethod.accept(manager, player); - } - private void initManager() { - manager = new CommandManager(testFolder, bukkitService, commandMigrationService); + manager = new CommandManager(testFolder, bukkitService, geoIpService, commandMigrationService); } private void copyJarFileAsCommandsYml(String path) { @@ -171,4 +168,13 @@ public class CommandManagerTest { } } + private Player mockPlayer() { + Player player = mock(Player.class); + given(player.getName()).willReturn("Bobby"); + given(player.getDisplayName()).willReturn("bob"); + String ip = "127.0.0.3"; + TestHelper.mockPlayerIp(player, ip); + given(geoIpService.getCountryName(ip)).willReturn("Syldavia"); + return player; + } } diff --git a/src/test/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetrieverTest.java b/src/test/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetrieverTest.java index 2a1c588e6..fcb1228c9 100644 --- a/src/test/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetrieverTest.java +++ b/src/test/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetrieverTest.java @@ -22,7 +22,7 @@ public class AuthMeSettingsRetrieverTest { // an error margin of 10: this prevents us from having to adjust the test every time the config is changed. // If this test fails, replace the first argument in closeTo() with the new number of properties assertThat((double) configurationData.getProperties().size(), - closeTo(150, 10)); + closeTo(160, 10)); } @Test diff --git a/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java index a88dbfb9a..d0d741359 100644 --- a/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java @@ -1,6 +1,7 @@ package fr.xephi.authme.settings.properties; import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.configurationdata.ConfigurationData; import ch.jalu.configme.properties.Property; import fr.xephi.authme.ClassCollector; import fr.xephi.authme.ReflectionTestUtils; @@ -10,11 +11,13 @@ import org.junit.Test; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -74,6 +77,27 @@ public class SettingsClassConsistencyTest { } } + /** + * Checks that {@link AuthMeSettingsRetriever} returns a ConfigurationData with all + * available SettingsHolder classes. + */ + @Test + public void shouldHaveAllClassesInConfigurationData() { + // given + long totalProperties = classes.stream() + .map(Class::getDeclaredFields) + .flatMap(Arrays::stream) + .filter(field -> Property.class.isAssignableFrom(field.getType())) + .count(); + + // when + ConfigurationData configData = AuthMeSettingsRetriever.buildConfigurationData(); + + // then + assertThat("ConfigurationData should have " + totalProperties + " properties (as found manually)", + configData.getProperties(), hasSize((int) totalProperties)); + } + @Test public void shouldHaveHiddenEmptyConstructorOnly() { for (Class clazz : classes) { diff --git a/src/test/java/fr/xephi/authme/task/CleanupTaskTest.java b/src/test/java/fr/xephi/authme/task/CleanupTaskTest.java index c58e8276c..33cd360be 100644 --- a/src/test/java/fr/xephi/authme/task/CleanupTaskTest.java +++ b/src/test/java/fr/xephi/authme/task/CleanupTaskTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.task; -import ch.jalu.injector.Injector; import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.factory.SingletonStore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -13,7 +13,6 @@ import java.util.List; import static java.util.Arrays.asList; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; /** @@ -26,13 +25,13 @@ public class CleanupTaskTest { private CleanupTask cleanupTask; @Mock - private Injector injector; + private SingletonStore hasCleanupStore; @Test public void shouldPerformCleanup() { // given List services = asList(mock(HasCleanup.class), mock(HasCleanup.class), mock(HasCleanup.class)); - given(injector.retrieveAllOfType(HasCleanup.class)).willReturn(services); + given(hasCleanupStore.retrieveAllOfType()).willReturn(services); // when cleanupTask.run(); @@ -41,6 +40,5 @@ public class CleanupTaskTest { verify(services.get(0)).performCleanup(); verify(services.get(1)).performCleanup(); verify(services.get(2)).performCleanup(); - verify(injector, only()).retrieveAllOfType(HasCleanup.class); } } diff --git a/src/test/java/fr/xephi/authme/util/CollectionUtilsTest.java b/src/test/java/fr/xephi/authme/util/CollectionUtilsTest.java deleted file mode 100644 index f359f0883..000000000 --- a/src/test/java/fr/xephi/authme/util/CollectionUtilsTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package fr.xephi.authme.util; - -import org.junit.Test; - -import java.util.Arrays; -import java.util.List; - -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; -import static org.hamcrest.Matchers.empty; - -/** - * Test for {@link CollectionUtils}. - */ -public class CollectionUtilsTest { - - @Test - public void shouldGetFullList() { - // given - List list = Arrays.asList("test", "1", "2", "3", "4"); - - // when - List result = CollectionUtils.getRange(list, 0, 24); - - // then - assertThat(result, equalTo(list)); - } - - @Test - public void shouldReturnEmptyListForZeroCount() { - // given - List list = Arrays.asList("test", "1", "2", "3", "4"); - - // when - List result = CollectionUtils.getRange(list, 2, 0); - - // then - assertThat(result, empty()); - } - - - @Test - public void shouldReturnEmptyListForTooHighStart() { - // given - List list = Arrays.asList("test", "1", "2", "3", "4"); - - // when - List result = CollectionUtils.getRange(list, 12, 2); - - // then - assertThat(result, empty()); - } - - @Test - public void shouldReturnSubList() { - // given - List list = Arrays.asList("test", "1", "2", "3", "4"); - - // when - List result = CollectionUtils.getRange(list, 1, 3); - - // then - assertThat(result, contains("1", "2", "3")); - } - - @Test - public void shouldReturnTillEnd() { - // given - List list = Arrays.asList("test", "1", "2", "3", "4"); - - // when - List result = CollectionUtils.getRange(list, 2, 3); - - // then - assertThat(result, contains("2", "3", "4")); - } - - @Test - public void shouldRemoveFirstTwo() { - // given - List list = Arrays.asList("test", "1", "2", "3", "4"); - - // when - List result = CollectionUtils.getRange(list, 2); - - // then - assertThat(result, contains("2", "3", "4")); - } - - @Test - public void shouldHandleNegativeStart() { - // given - List list = Arrays.asList("test", "1", "2", "3", "4"); - - // when - List result = CollectionUtils.getRange(list, -4); - - // then - assertThat(result, equalTo(list)); - } -} diff --git a/src/test/java/fr/xephi/authme/util/FileUtilsTest.java b/src/test/java/fr/xephi/authme/util/FileUtilsTest.java index fa8ee9c6d..9d65047c5 100644 --- a/src/test/java/fr/xephi/authme/util/FileUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/FileUtilsTest.java @@ -137,6 +137,11 @@ public class FileUtilsTest { assertThat(result, equalTo("path" + File.separator + "to" + File.separator + "test-file.txt")); } + @Test + public void shouldHaveHiddenConstructor() { + TestHelper.validateHasOnlyPrivateEmptyConstructor(FileUtils.class); + } + private static void createFiles(File... files) throws IOException { for (File file : files) { boolean result = file.getParentFile().mkdirs() & file.createNewFile(); diff --git a/src/test/java/fr/xephi/authme/util/PlayerUtilsTest.java b/src/test/java/fr/xephi/authme/util/PlayerUtilsTest.java index be5425c23..d16a85478 100644 --- a/src/test/java/fr/xephi/authme/util/PlayerUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/PlayerUtilsTest.java @@ -64,4 +64,10 @@ public class PlayerUtilsTest { // then assertThat(result, equalTo(name)); } + + @Test + public void shouldHaveHiddenConstructor() { + // given / when / then + TestHelper.validateHasOnlyPrivateEmptyConstructor(PlayerUtils.class); + } } diff --git a/src/test/java/fr/xephi/authme/util/RandomStringUtilsTest.java b/src/test/java/fr/xephi/authme/util/RandomStringUtilsTest.java index c0f9cd0b5..b5200b01d 100644 --- a/src/test/java/fr/xephi/authme/util/RandomStringUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/RandomStringUtilsTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.util; +import fr.xephi.authme.TestHelper; import org.junit.Test; import java.util.regex.Pattern; @@ -68,4 +69,10 @@ public class RandomStringUtilsTest { // then - throw exception } + @Test + public void shouldHaveHiddenConstructor() { + // given / when / then + TestHelper.validateHasOnlyPrivateEmptyConstructor(RandomStringUtils.class); + } + } diff --git a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java index 1be95d0d8..7111f81b8 100644 --- a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java @@ -5,6 +5,7 @@ import org.junit.Test; import java.net.MalformedURLException; +import static java.util.Arrays.asList; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertFalse; @@ -23,7 +24,7 @@ public class StringUtilsTest { String piece = "test"; // when - boolean result = StringUtils.containsAny(text, "some", "words", "that", "do not", "exist", piece); + boolean result = StringUtils.containsAny(text, asList("some", "words", "that", "do not", "exist", piece)); // then assertThat(result, equalTo(true)); @@ -35,7 +36,7 @@ public class StringUtilsTest { String text = "This is a test string"; // when - boolean result = StringUtils.containsAny(text, "some", "other", "words", null); + boolean result = StringUtils.containsAny(text, asList("some", "other", "words", null)); // then assertThat(result, equalTo(false)); @@ -44,7 +45,7 @@ public class StringUtilsTest { @Test public void shouldReturnFalseForNullString() { // given/when - boolean result = StringUtils.containsAny(null, "some", "words", "to", "check"); + boolean result = StringUtils.containsAny(null, asList("some", "words", "to", "check")); // then assertThat(result, equalTo(false)); @@ -94,4 +95,14 @@ public class StringUtilsTest { public void shouldHaveHiddenConstructor() { TestHelper.validateHasOnlyPrivateEmptyConstructor(StringUtils.class); } + + @Test + public void shouldCheckIfHasNeedleInWord() { + // given/when/then + assertThat(StringUtils.isInsideString('@', "@hello"), equalTo(false)); + assertThat(StringUtils.isInsideString('?', "absent"), equalTo(false)); + assertThat(StringUtils.isInsideString('-', "abcd-"), equalTo(false)); + assertThat(StringUtils.isInsideString('@', "hello@example"), equalTo(true)); + assertThat(StringUtils.isInsideString('@', "D@Z"), equalTo(true)); + } } diff --git a/src/test/java/fr/xephi/authme/util/UtilsTest.java b/src/test/java/fr/xephi/authme/util/UtilsTest.java index 458023d6d..cbb64f370 100644 --- a/src/test/java/fr/xephi/authme/util/UtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/UtilsTest.java @@ -1,13 +1,20 @@ package fr.xephi.authme.util; import fr.xephi.authme.TestHelper; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; import org.junit.BeforeClass; import org.junit.Test; +import java.util.logging.Logger; import java.util.regex.Pattern; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link Utils}. @@ -49,6 +56,49 @@ public class UtilsTest { TestHelper.validateHasOnlyPrivateEmptyConstructor(Utils.class); } + @Test + public void shouldLogAndSendMessage() { + // given + Logger logger = TestHelper.setupLogger(); + Player player = mock(Player.class); + String message = "Finished adding foo to the bar"; + + // when + Utils.logAndSendMessage(player, message); + + // then + verify(logger).info(message); + verify(player).sendMessage(message); + } + + @Test + public void shouldHandleNullAsCommandSender() { + // given + Logger logger = TestHelper.setupLogger(); + String message = "Test test, test."; + + // when + Utils.logAndSendMessage(null, message); + + // then + verify(logger).info(message); + } + + @Test + public void shouldNotSendToCommandSenderTwice() { + // given + Logger logger = TestHelper.setupLogger(); + CommandSender sender = mock(ConsoleCommandSender.class); + String message = "Test test, test."; + + // when + Utils.logAndSendMessage(sender, message); + + // then + verify(logger).info(message); + verifyZeroInteractions(sender); + } + @Test public void shouldCheckIfClassIsLoaded() { // given / when / then diff --git a/src/test/java/fr/xephi/authme/util/expiring/DurationTest.java b/src/test/java/fr/xephi/authme/util/expiring/DurationTest.java new file mode 100644 index 000000000..03a0f9d47 --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/expiring/DurationTest.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.util.expiring; + +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link Duration}. + */ +public class DurationTest { + + @Test + public void shouldConvertToAppropriateTimeUnit() { + check(Duration.createWithSuitableUnit(0, TimeUnit.HOURS), + 0, TimeUnit.SECONDS); + + check(Duration.createWithSuitableUnit(124, TimeUnit.MINUTES), + 2, TimeUnit.HOURS); + + check(Duration.createWithSuitableUnit(300, TimeUnit.HOURS), + 12, TimeUnit.DAYS); + + check(Duration.createWithSuitableUnit(60 * 24 * 50 + 8, TimeUnit.MINUTES), + 50, TimeUnit.DAYS); + + check(Duration.createWithSuitableUnit(1000L * 60 * 60 * 24 * 7 + 3000, TimeUnit.MILLISECONDS), + 7, TimeUnit.DAYS); + + check(Duration.createWithSuitableUnit(1000L * 60 * 60 * 3 + 1400, TimeUnit.MILLISECONDS), + 3, TimeUnit.HOURS); + + check(Duration.createWithSuitableUnit(248, TimeUnit.SECONDS), + 4, TimeUnit.MINUTES); + } + + private static void check(Duration duration, long expectedDuration, TimeUnit expectedUnit) { + assertThat(duration.getTimeUnit(), equalTo(expectedUnit)); + assertThat(duration.getDuration(), equalTo(expectedDuration)); + } +} diff --git a/src/test/java/fr/xephi/authme/util/expiring/ExpiringMapTest.java b/src/test/java/fr/xephi/authme/util/expiring/ExpiringMapTest.java new file mode 100644 index 000000000..ac9a5a719 --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/expiring/ExpiringMapTest.java @@ -0,0 +1,95 @@ +package fr.xephi.authme.util.expiring; + +import org.junit.Test; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link ExpiringMap}. + */ +public class ExpiringMapTest { + + @Test + public void shouldAddAndRetrieveEntries() { + // given + ExpiringMap map = new ExpiringMap<>(3, TimeUnit.MINUTES); + + // when / then + map.put("three", 3.0); + map.put("treefiddy", 3.50); + + assertThat(map.get("three"), equalTo(3.0)); + assertThat(map.get("treefiddy"), equalTo(3.50)); + } + + @Test + public void shouldRemoveEntry() { + // given + ExpiringMap map = new ExpiringMap<>(1, TimeUnit.HOURS); + map.put("hi", true); + map.put("ha", false); + + // when + map.remove("ha"); + + // then + assertThat(map.get("ha"), nullValue()); + assertThat(map.get("hi"), equalTo(true)); + } + + @Test + public void shouldUpdateExpirationAndSupportNegativeValues() { + // given + ExpiringMap map = new ExpiringMap<>(2, TimeUnit.DAYS); + map.put(2, 4); + map.put(3, 9); + + // when + map.setExpiration(-100, TimeUnit.MILLISECONDS); + + // then + map.put(5, 25); + assertThat(map.get(2), equalTo(4)); + assertThat(map.get(3), equalTo(9)); + assertThat(map.get(5), nullValue()); + } + + @Test + public void shouldCleanUpExpiredEntries() throws InterruptedException { + // given + ExpiringMap map = new ExpiringMap<>(200, TimeUnit.MILLISECONDS); + map.put(144, 12); + map.put(121, 11); + map.put(81, 9); + map.setExpiration(900, TimeUnit.MILLISECONDS); + map.put(64, 8); + map.put(25, 5); + + // when + Thread.sleep(300); + map.removeExpiredEntries(); + + // then + Map internalMap = map.getEntries(); + assertThat(internalMap.keySet(), containsInAnyOrder(64, 25)); + } + + @Test + public void shouldReturnIfIsEmpty() { + // given + ExpiringMap map = new ExpiringMap<>(-8, TimeUnit.SECONDS); + + // when / then + assertThat(map.isEmpty(), equalTo(true)); + map.put("hoi", "Welt"); + assertThat(map.isEmpty(), equalTo(false)); + map.removeExpiredEntries(); + assertThat(map.isEmpty(), equalTo(true)); + } +} diff --git a/src/test/java/fr/xephi/authme/util/expiring/ExpiringSetTest.java b/src/test/java/fr/xephi/authme/util/expiring/ExpiringSetTest.java new file mode 100644 index 000000000..42180ab26 --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/expiring/ExpiringSetTest.java @@ -0,0 +1,143 @@ +package fr.xephi.authme.util.expiring; + +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link ExpiringSet}. + */ +public class ExpiringSetTest { + + @Test + public void shouldAddEntry() { + // given + ExpiringSet set = new ExpiringSet<>(10, TimeUnit.MINUTES); + + // when + set.add("authme"); + + // then + assertThat(set.contains("authme"), equalTo(true)); + assertThat(set.contains("other"), equalTo(false)); + } + + @Test + public void shouldRemoveEntries() { + // given + ExpiringSet set = new ExpiringSet<>(20, TimeUnit.SECONDS); + set.add(20); + set.add(40); + + // when + set.remove(40); + set.remove(60); + + // then + assertThat(set.contains(20), equalTo(true)); + assertThat(set.contains(40), equalTo(false)); + assertThat(set.contains(60), equalTo(false)); + } + + @Test + public void shouldHandleNewExpirationAndSupportNegativeValues() { + // given + ExpiringSet set = new ExpiringSet<>(800, TimeUnit.MILLISECONDS); + set.add('A'); + + // when + set.setExpiration(-10, TimeUnit.SECONDS); + set.add('Y'); + + // then + assertThat(set.contains('A'), equalTo(true)); + assertThat(set.contains('Y'), equalTo(false)); + } + + @Test + public void shouldClearAllValues() { + // given + ExpiringSet set = new ExpiringSet<>(1, TimeUnit.MINUTES); + set.add("test"); + + // when / then + assertThat(set.isEmpty(), equalTo(false)); + set.clear(); + assertThat(set.isEmpty(), equalTo(true)); + assertThat(set.contains("test"), equalTo(false)); + } + + @Test + public void shouldClearExpiredValues() { + // given + ExpiringSet set = new ExpiringSet<>(2, TimeUnit.HOURS); + set.add(2); + set.setExpiration(-100, TimeUnit.SECONDS); + set.add(3); + set.setExpiration(20, TimeUnit.MINUTES); + set.add(6); + + // when + set.removeExpiredEntries(); + + // then + assertThat(set.contains(2), equalTo(true)); + assertThat(set.contains(3), equalTo(false)); + assertThat(set.contains(6), equalTo(true)); + } + + @Test + public void shouldReturnExpiration() { + // given + ExpiringSet set = new ExpiringSet<>(123, TimeUnit.MINUTES); + set.add("my entry"); + + // when + Duration expiration = set.getExpiration("my entry"); + Duration unknownExpiration = set.getExpiration("bogus"); + + // then + assertIsDuration(expiration, 2, TimeUnit.HOURS); + assertIsDuration(unknownExpiration, -1, TimeUnit.SECONDS); + } + + @Test + public void shouldReturnExpirationInSuitableUnits() { + // given + ExpiringSet set = new ExpiringSet<>(601, TimeUnit.SECONDS); + set.add(12); + set.setExpiration(49, TimeUnit.HOURS); + set.add(23); + + // when + Duration expiration12 = set.getExpiration(12); + Duration expiration23 = set.getExpiration(23); + Duration expectedUnknown = set.getExpiration(-100); + + // then + assertIsDuration(expiration12, 10, TimeUnit.MINUTES); + assertIsDuration(expiration23, 2, TimeUnit.DAYS); + assertIsDuration(expectedUnknown, -1, TimeUnit.SECONDS); + } + + @Test + public void shouldReturnMinusOneForExpiredEntry() { + // given + ExpiringSet set = new ExpiringSet<>(-100, TimeUnit.SECONDS); + set.add(23); + + // when + Duration expiration = set.getExpiration(23); + + // then + assertIsDuration(expiration, -1, TimeUnit.SECONDS); + } + + private static void assertIsDuration(Duration duration, long expectedDuration, TimeUnit expectedUnit) { + assertThat(duration.getTimeUnit(), equalTo(expectedUnit)); + assertThat(duration.getDuration(), equalTo(expectedDuration)); + } +} diff --git a/src/test/java/fr/xephi/authme/util/expiring/TimedCounterTest.java b/src/test/java/fr/xephi/authme/util/expiring/TimedCounterTest.java new file mode 100644 index 000000000..c2b65eed3 --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/expiring/TimedCounterTest.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.util.expiring; + +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link TimedCounter}. + */ +public class TimedCounterTest { + + @Test + public void shouldReturnZeroForAnyKey() { + // given + TimedCounter counter = new TimedCounter<>(1, TimeUnit.DAYS); + + // when / then + assertThat(counter.get(2.0), equalTo(0)); + assertThat(counter.get(-3.14159), equalTo(0)); + } + + @Test + public void shouldIncrementCount() { + // given + TimedCounter counter = new TimedCounter<>(10, TimeUnit.MINUTES); + counter.put("moto", 12); + + // when + counter.increment("hello"); + counter.increment("moto"); + + // then + assertThat(counter.get("hello"), equalTo(1)); + assertThat(counter.get("moto"), equalTo(13)); + } + + @Test + public void shouldDecrementCount() { + // given + TimedCounter counter = new TimedCounter<>(10, TimeUnit.MINUTES); + counter.put("moto", 12); + + // when + counter.decrement("hello"); + counter.decrement("moto"); + + // then + assertThat(counter.get("hello"), equalTo(0)); + assertThat(counter.get("moto"), equalTo(11)); + } + + @Test + public void shouldSumUpEntries() { + // given + TimedCounter counter = new TimedCounter<>(90, TimeUnit.SECONDS); + counter.getEntries().put("expired", new ExpiringMap.ExpiringEntry<>(800, 0)); + counter.getEntries().put("expired2", new ExpiringMap.ExpiringEntry<>(24, System.currentTimeMillis() - 100)); + counter.put("other", 10); + counter.put("Another", 4); + + // when + int totals = counter.total(); + + // then + assertThat(totals, equalTo(14)); + } +} diff --git a/src/test/java/fr/xephi/authme/util/lazytags/TagBuilderTest.java b/src/test/java/fr/xephi/authme/util/lazytags/TagBuilderTest.java new file mode 100644 index 000000000..60b5b68ec --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/lazytags/TagBuilderTest.java @@ -0,0 +1,50 @@ +package fr.xephi.authme.util.lazytags; + +import fr.xephi.authme.TestHelper; +import org.junit.Test; + +import java.util.function.Function; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link TagBuilder}. + */ +public class TagBuilderTest { + + @Test + public void shouldCreateNoArgsTag() { + // given + Supplier supplier = () -> "hello"; + + // when + Tag tag = TagBuilder.createTag("hey", supplier); + + // then + assertThat(tag, instanceOf(SimpleTag.class)); + assertThat(tag.getValue(null), equalTo("hello")); + } + + @Test + public void shouldCreateDependentTag() { + // given + Function function = d -> Double.toString(d + d/10); + + // when + Tag tag = TagBuilder.createTag("%test", function); + + // then + assertThat(tag, instanceOf(DependentTag.class)); + assertThat(tag.getValue(24d), equalTo("26.4")); + } + + @Test + public void shouldHaveHiddenConstructor() { + // given / when / then + TestHelper.validateHasOnlyPrivateEmptyConstructor(TagBuilder.class); + } + +} diff --git a/src/test/java/fr/xephi/authme/util/lazytags/TagReplacerTest.java b/src/test/java/fr/xephi/authme/util/lazytags/TagReplacerTest.java new file mode 100644 index 000000000..8c9a8187f --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/lazytags/TagReplacerTest.java @@ -0,0 +1,90 @@ +package fr.xephi.authme.util.lazytags; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static fr.xephi.authme.util.lazytags.TagBuilder.createTag; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link TagReplacer}. + */ +public class TagReplacerTest { + + @Test + public void shouldReplaceTags() { + // given + TestTagService tagService = new TestTagService(); + List> tags = tagService.getAvailableTags(); + List messages = Arrays.asList("pi = %PI", "for i = %self, i^2 = %square", "%self %self %PI"); + + // when + TagReplacer tagReplacer = TagReplacer.newReplacer(tags, messages); + List result = tagReplacer.getAdaptedMessages(3); + + // then + assertThat(tagService.piCount, equalTo(1)); + assertThat(tagService.selfCount, equalTo(1)); + assertThat(tagService.doubleCount, equalTo(0)); + assertThat(tagService.squareCount, equalTo(1)); + assertThat(result, contains("pi = 3.14159", "for i = 3, i^2 = 9", "3 3 3.14159")); + } + + @Test + public void shouldNotCallUnusedTags() { + // given + TestTagService tagService = new TestTagService(); + List> tags = tagService.getAvailableTags(); + List messages = Arrays.asList("pi = %PI", "double i = %double"); + + // when + TagReplacer tagReplacer = TagReplacer.newReplacer(tags, messages); + List result1 = tagReplacer.getAdaptedMessages(-4); + List result2 = tagReplacer.getAdaptedMessages(0); + + // then + assertThat(tagService.piCount, equalTo(2)); + assertThat(tagService.selfCount, equalTo(0)); + assertThat(tagService.doubleCount, equalTo(2)); + assertThat(tagService.squareCount, equalTo(0)); + assertThat(result1, contains("pi = 3.14159", "double i = -8")); + assertThat(result2, contains("pi = 3.14159", "double i = 0")); + } + + static final class TestTagService { + int piCount, selfCount, doubleCount, squareCount; + + String pi() { + ++piCount; + return "3.14159"; + } + + String self(int i) { + ++selfCount; + return Integer.toString(i); + } + + String calcDouble(int i) { + ++doubleCount; + return Integer.toString(2 * i); + } + + String calcSquare(int i) { + ++squareCount; + return Integer.toString(i * i); + } + + List> getAvailableTags() { + return Arrays.asList( + createTag("%PI", this::pi), + createTag("%self", this::self), + createTag("%double", this::calcDouble), + createTag("%square", this::calcSquare)); + } + } + +} diff --git a/src/test/java/fr/xephi/authme/util/lazytags/WrappedTagReplacerTest.java b/src/test/java/fr/xephi/authme/util/lazytags/WrappedTagReplacerTest.java new file mode 100644 index 000000000..199150545 --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/lazytags/WrappedTagReplacerTest.java @@ -0,0 +1,76 @@ +package fr.xephi.authme.util.lazytags; + +import fr.xephi.authme.util.lazytags.TagReplacerTest.TestTagService; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link WrappedTagReplacer}. + */ +public class WrappedTagReplacerTest { + + @Test + public void shouldApplyTags() { + // given + TestTagService tagService = new TestTagService(); + List> tags = tagService.getAvailableTags(); + List objects = Arrays.asList( + new SampleClass(3, "pi is %PI"), + new SampleClass(5, "no tags here"), + new SampleClass(7, "i+i = %double")); + + // when + WrappedTagReplacer replacer = new WrappedTagReplacer<>( + tags, objects, SampleClass::getDescription, (o, s) -> new SampleClass(o.number, s)); + List result1 = replacer.getAdaptedItems(8); + List result2 = replacer.getAdaptedItems(1); + + // then + assertThat(tagService.piCount, equalTo(2)); + assertThat(tagService.selfCount, equalTo(0)); + assertThat(tagService.doubleCount, equalTo(2)); + assertThat(tagService.squareCount, equalTo(0)); + assertThat(result1, contains( + sampleClass(3, "pi is 3.14159"), sampleClass(5, "no tags here"), sampleClass(7, "i+i = 16"))); + assertThat(result2, contains( + sampleClass(3, "pi is 3.14159"), sampleClass(5, "no tags here"), sampleClass(7, "i+i = 2"))); + } + + + private static Matcher sampleClass(int number, String description) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(SampleClass item) { + return number == item.number && description.equals(item.description); + } + + @Override + public void describeTo(Description description) { + description.appendText("SampleClass[number=" + number + ";description=" + description + "]"); + } + }; + } + + private static final class SampleClass { + private final int number; + private final String description; + + SampleClass(int number, String description) { + this.number = number; + this.description = description; + } + + String getDescription() { + return description; + } + } +} diff --git a/src/test/java/tools/dependencygraph/DrawDependency.java b/src/test/java/tools/dependencygraph/DrawDependency.java index 6b2c9c8af..98de28be4 100644 --- a/src/test/java/tools/dependencygraph/DrawDependency.java +++ b/src/test/java/tools/dependencygraph/DrawDependency.java @@ -2,31 +2,38 @@ package tools.dependencygraph; import ch.jalu.injector.handlers.instantiation.DependencyDescription; import ch.jalu.injector.handlers.instantiation.Instantiation; +import ch.jalu.injector.handlers.instantiation.StandardInjectionProvider; +import ch.jalu.injector.utils.ReflectionUtils; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import fr.xephi.authme.ClassCollector; import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.command.executable.authme.debug.DebugCommand; +import fr.xephi.authme.data.limbo.persistence.LimboPersistence; import fr.xephi.authme.datasource.converter.Converter; import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.factory.Factory; +import fr.xephi.authme.initialization.factory.SingletonStore; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.process.register.executors.RegistrationExecutor; import fr.xephi.authme.security.crypts.EncryptionMethod; import org.bukkit.event.Listener; -import tools.utils.InjectorUtils; import tools.utils.ToolTask; import tools.utils.ToolsConstants; import java.io.IOException; import java.lang.annotation.Annotation; +import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Scanner; /** @@ -36,8 +43,7 @@ public class DrawDependency implements ToolTask { private static final String DOT_FILE = ToolsConstants.TOOLS_SOURCE_ROOT + "dependencygraph/graph.dot"; - private static final List> SUPER_TYPES = ImmutableList.of(ExecutableCommand.class, - SynchronousProcess.class, AsynchronousProcess.class, EncryptionMethod.class, Converter.class, Listener.class); + private static final List> SUPER_TYPES = buildSuperTypesList(); /** Annotation types by which dependencies are identified. */ private static final List> ANNOTATION_TYPES = ImmutableList.of(DataFolder.class); @@ -113,8 +119,26 @@ public class DrawDependency implements ToolTask { return clazz; } + /** + * Returns the parameter of generic container classes, otherwise the input class. + * This is interesting so that a dependency in a class to {@code Factory} is + * rendered as a dependency to {@code Foo}, not to {@code Factory}. + * + * @param clazz class of the dependency + * @param genericType generic type of the dependency + * @return the class to use to render the dependency + */ + private Class unwrapGenericClass(Class clazz, Type genericType) { + if (clazz == Factory.class || clazz == SingletonStore.class) { + Class parameterType = ReflectionUtils.getGenericType(genericType); + Objects.requireNonNull(parameterType, "Parameter type for '" + clazz + "' should be a concrete class"); + return parameterType; + } + return clazz; + } + private List getDependencies(Class clazz) { - Instantiation instantiation = InjectorUtils.getInstantiationMethod(clazz); + Instantiation instantiation = new StandardInjectionProvider().safeGet(clazz); return instantiation == null ? null : formatInjectionDependencies(instantiation); } @@ -127,21 +151,15 @@ public class DrawDependency implements ToolTask { * @return list of dependencies in a friendly format */ private List formatInjectionDependencies(Instantiation injection) { - List descriptions = injection.getDependencies(); - final int totalDependencies = descriptions.size(); - Class[] dependencies = new Class[totalDependencies]; - Class[] annotations = new Class[totalDependencies]; - for (int i = 0; i < descriptions.size(); ++i) { - dependencies[i] = descriptions.get(i).getType(); - annotations[i] = getRelevantAnnotationClass(descriptions.get(i).getAnnotations()); - } - - List result = new ArrayList<>(dependencies.length); - for (int i = 0; i < dependencies.length; ++i) { - if (annotations[i] != null) { - result.add("@" + annotations[i].getSimpleName()); + List descriptions = injection.getDependencies(); + List result = new ArrayList<>(descriptions.size()); + for (DependencyDescription dependency : descriptions) { + Class annotation = getRelevantAnnotationClass(dependency.getAnnotations()); + if (annotation != null) { + result.add("@" + annotation.getSimpleName()); } else { - result.add(mapToSuper(dependencies[i]).getSimpleName()); + Class clazz = unwrapGenericClass(dependency.getType(), dependency.getGenericType()); + result.add(mapToSuper(clazz).getSimpleName()); } } return result; @@ -169,12 +187,26 @@ public class DrawDependency implements ToolTask { dependencies.put(entry.getValue(), Boolean.TRUE); } - Iterator> it = foundDependencies.keys().iterator(); - while (it.hasNext()) { - Class clazz = it.next(); - if (Boolean.FALSE.equals(dependencies.get(clazz.getSimpleName()))) { - it.remove(); - } + foundDependencies.keys().removeIf( + clazz -> Boolean.FALSE.equals(dependencies.get(clazz.getSimpleName()))); + } + + /** + * @return list of classes in AuthMe which have multiple extensions + */ + private static List> buildSuperTypesList() { + try { + // Get package-private classes + Class debugSectionClass = Class.forName( + DebugCommand.class.getPackage().getName() + ".DebugSection"); + Class limboPersistenceClass = Class.forName( + LimboPersistence.class.getPackage().getName() + ".LimboPersistenceHandler"); + + return ImmutableList.of(ExecutableCommand.class, SynchronousProcess.class, AsynchronousProcess.class, + EncryptionMethod.class, Converter.class, Listener.class, RegistrationExecutor.class, debugSectionClass, + limboPersistenceClass); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); } } } diff --git a/src/test/java/tools/docs/hashmethods/EncryptionMethodInfoGatherer.java b/src/test/java/tools/docs/hashmethods/EncryptionMethodInfoGatherer.java index e327e4847..0a1d3fa59 100644 --- a/src/test/java/tools/docs/hashmethods/EncryptionMethodInfoGatherer.java +++ b/src/test/java/tools/docs/hashmethods/EncryptionMethodInfoGatherer.java @@ -57,7 +57,7 @@ public class EncryptionMethodInfoGatherer { private static MethodDescription createDescription(HashAlgorithm algorithm) { Class clazz = algorithm.getClazz(); - EncryptionMethod method = injector.newInstance(clazz); + EncryptionMethod method = injector.createIfHasDependencies(clazz); if (method == null) { throw new NullPointerException("Method for '" + algorithm + "' is null"); } diff --git a/src/test/java/tools/filegeneration/GenerateCommandsYml.java b/src/test/java/tools/filegeneration/GenerateCommandsYml.java index 73113ccc4..73232d90d 100644 --- a/src/test/java/tools/filegeneration/GenerateCommandsYml.java +++ b/src/test/java/tools/filegeneration/GenerateCommandsYml.java @@ -2,11 +2,8 @@ package tools.filegeneration; import ch.jalu.configme.SettingsManager; import ch.jalu.configme.resource.YamlFileResource; -import com.google.common.collect.ImmutableMap; -import fr.xephi.authme.settings.commandconfig.Command; import fr.xephi.authme.settings.commandconfig.CommandConfig; import fr.xephi.authme.settings.commandconfig.CommandSettingsHolder; -import fr.xephi.authme.settings.commandconfig.Executor; import tools.utils.AutoToolTask; import tools.utils.ToolsConstants; @@ -23,10 +20,8 @@ public class GenerateCommandsYml implements AutoToolTask { public void executeDefault() { File file = new File(COMMANDS_YML_FILE); - // Get default and add sample entry + // Get the default CommandConfig commandConfig = CommandSettingsHolder.COMMANDS.getDefaultValue(); - commandConfig.setOnLogin( - ImmutableMap.of("welcome", newCommand("msg %p Welcome back!", Executor.PLAYER))); // Export the value to the file SettingsManager settingsManager = new SettingsManager( @@ -41,11 +36,4 @@ public class GenerateCommandsYml implements AutoToolTask { public String getTaskName() { return "generateCommandsYml"; } - - private static Command newCommand(String commandLine, Executor executor) { - Command command = new Command(); - command.setCommand(commandLine); - command.setExecutor(executor); - return command; - } } diff --git a/src/test/java/tools/messages/AddJavaDocToMessageEnumTask.java b/src/test/java/tools/messages/AddJavaDocToMessageEnumTask.java index 30a632443..44306dc81 100644 --- a/src/test/java/tools/messages/AddJavaDocToMessageEnumTask.java +++ b/src/test/java/tools/messages/AddJavaDocToMessageEnumTask.java @@ -45,6 +45,7 @@ public class AddJavaDocToMessageEnumTask implements AutoToolTask { return configuration.getString(key.getKey()) .replaceAll("&[0-9a-f]", "") .replace("&", "&") - .replace("<", "<"); + .replace("<", "<") + .replace(">", ">"); } } diff --git a/src/test/java/tools/messages/CheckMessageKeyUsages.java b/src/test/java/tools/messages/CheckMessageKeyUsages.java new file mode 100644 index 000000000..51b8eb12b --- /dev/null +++ b/src/test/java/tools/messages/CheckMessageKeyUsages.java @@ -0,0 +1,91 @@ +package tools.messages; + +import com.google.common.collect.Lists; +import fr.xephi.authme.message.MessageKey; +import tools.utils.FileIoUtils; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; + +/** + * Task which checks for {@link MessageKey} usages. + */ +public class CheckMessageKeyUsages implements ToolTask { + + private static final Predicate SHOULD_CHECK_FILE = + file -> file.getName().endsWith(".java") && !file.getName().endsWith("MessageKey.java"); + + @Override + public String getTaskName() { + return "checkMessageUses"; + } + + @Override + public void execute(Scanner scanner) { + System.out.println("Enter a message key to find the files where it is used"); + System.out.println("Enter empty line to search for all unused message keys"); + String key = scanner.nextLine(); + + if (key.trim().isEmpty()) { + List unusedKeys = findUnusedKeys(); + if (unusedKeys.isEmpty()) { + System.out.println("No unused MessageKey entries found :)"); + } else { + System.out.println("Did not find usages for keys:\n- " + + String.join("\n- ", Lists.transform(unusedKeys, MessageKey::name))); + } + } else { + MessageKey messageKey = MessageKey.valueOf(key); + List filesUsingKey = findUsagesOfKey(messageKey); + System.out.println("The following files use '" + key + "':\n- " + + filesUsingKey.stream().map(File::getName).collect(Collectors.joining("\n- "))); + } + } + + private List findUnusedKeys() { + List keys = new ArrayList<>(asList(MessageKey.values())); + File sourceFolder = new File(ToolsConstants.MAIN_SOURCE_ROOT); + + Consumer fileProcessor = file -> { + String source = FileIoUtils.readFromFile(file.toPath()); + keys.removeIf(key -> source.contains(key.name())); + }; + + walkJavaFileTree(sourceFolder, fileProcessor); + return keys; + } + + private List findUsagesOfKey(MessageKey key) { + List filesUsingKey = new ArrayList<>(); + File sourceFolder = new File(ToolsConstants.MAIN_SOURCE_ROOT); + + Consumer usagesCollector = file -> { + String source = FileIoUtils.readFromFile(file.toPath()); + if (source.contains(key.name())) { + filesUsingKey.add(file); + } + }; + + walkJavaFileTree(sourceFolder, usagesCollector); + return filesUsingKey; + } + + private static void walkJavaFileTree(File folder, Consumer javaFileConsumer) { + for (File file : FileIoUtils.listFilesOrThrow(folder)) { + if (file.isDirectory()) { + walkJavaFileTree(file, javaFileConsumer); + } else if (file.isFile() && SHOULD_CHECK_FILE.test(file)) { + javaFileConsumer.accept(file); + } + } + } +} diff --git a/src/test/java/tools/utils/InjectorUtils.java b/src/test/java/tools/utils/InjectorUtils.java index 0dbd1f4a9..6b5d510e0 100644 --- a/src/test/java/tools/utils/InjectorUtils.java +++ b/src/test/java/tools/utils/InjectorUtils.java @@ -1,12 +1,10 @@ package tools.utils; -import ch.jalu.injector.InjectorBuilder; import ch.jalu.injector.handlers.instantiation.DependencyDescription; import ch.jalu.injector.handlers.instantiation.Instantiation; -import ch.jalu.injector.handlers.instantiation.InstantiationProvider; +import ch.jalu.injector.handlers.instantiation.StandardInjectionProvider; import java.util.HashSet; -import java.util.List; import java.util.Set; /** @@ -24,7 +22,7 @@ public final class InjectorUtils { * @return the class' dependencies, or null if no instantiation method found */ public static Set> getDependencies(Class clazz) { - Instantiation instantiation = getInstantiationMethod(clazz); + Instantiation instantiation = new StandardInjectionProvider().safeGet(clazz); if (instantiation == null) { return null; } @@ -35,21 +33,4 @@ public final class InjectorUtils { return dependencies; } - /** - * Returns the instantiation method for the given class. - * - * @param clazz the class to process - * @return the instantiation method for the class, or null if none applicable - */ - public static Instantiation getInstantiationMethod(Class clazz) { - List providers = InjectorBuilder.createInstantiationProviders(); - for (InstantiationProvider provider : providers) { - Instantiation instantiation = provider.get(clazz); - if (instantiation != null) { - return instantiation; - } - } - return null; - } - } diff --git a/src/test/resources/fr/xephi/authme/data/limbo/seg16-8-limbo.json b/src/test/resources/fr/xephi/authme/data/limbo/seg16-8-limbo.json new file mode 100644 index 000000000..84da8a268 --- /dev/null +++ b/src/test/resources/fr/xephi/authme/data/limbo/seg16-8-limbo.json @@ -0,0 +1,32 @@ +{ + "88897c88-7c8f-c12e-4931-6206d4ca067d": { + "location": { + "world": "world", + "x": -196.69999998807907, + "y": 67.0, + "z": 5.699999988079071, + "yaw": 222.14977, + "pitch": 10.649977 + }, + "group": "staff", + "operator": true, + "can-fly": false, + "walk-speed": 0.3, + "fly-speed": 0.1 + }, + "8c679491-1234-abcd-9102-1fa6e0cc3f81": { + "location": { + "world": "nether", + "x": 300.12345, + "y": 42.3, + "z": -72.482749988079071, + "yaw": 100.27788, + "pitch": 4.242111 + }, + "group": "primary", + "operator": false, + "can-fly": true, + "walk-speed": 0.1, + "fly-speed": 0.0 + } +} diff --git a/src/test/resources/fr/xephi/authme/data/limbo/seg16-f-limbo.json b/src/test/resources/fr/xephi/authme/data/limbo/seg16-f-limbo.json new file mode 100644 index 000000000..9f2562622 --- /dev/null +++ b/src/test/resources/fr/xephi/authme/data/limbo/seg16-f-limbo.json @@ -0,0 +1,17 @@ +{ + "fab69c88-2cd0-1fed-f00d-dead14ca067d": { + "location": { + "world": "world", + "x": -196.69999998807907, + "y": 67.0, + "z": 5.699999988079071, + "yaw": 222.14977, + "pitch": 10.649977 + }, + "group": "", + "operator": false, + "can-fly": false, + "walk-speed": 0.2, + "fly-speed": 0.1 + } +} diff --git a/src/test/resources/fr/xephi/authme/data/limbo/seg32-01011-limbo.json b/src/test/resources/fr/xephi/authme/data/limbo/seg32-01011-limbo.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/src/test/resources/fr/xephi/authme/data/limbo/seg32-01011-limbo.json @@ -0,0 +1 @@ +{} diff --git a/src/test/resources/fr/xephi/authme/data/limbo/seg32-10110-limbo.json b/src/test/resources/fr/xephi/authme/data/limbo/seg32-10110-limbo.json new file mode 100644 index 000000000..b50fb8bda --- /dev/null +++ b/src/test/resources/fr/xephi/authme/data/limbo/seg32-10110-limbo.json @@ -0,0 +1,17 @@ +{ + "f6a97c88-7c8f-c12e-4931-6206d4ca067d": { + "location": { + "world": "lobby", + "x": -120.31415, + "y": 25.0, + "z": -80.71234, + "yaw": 22.14977, + "pitch": 40.649977 + }, + "group": "noob", + "operator": false, + "can-fly": true, + "walk-speed": 0.2, + "fly-speed": 0.1 + } +} diff --git a/src/test/resources/fr/xephi/authme/datasource/flatfile-test.txt b/src/test/resources/fr/xephi/authme/datasource/flatfile-test.txt index da5a13124..d0ac4aa57 100644 --- a/src/test/resources/fr/xephi/authme/datasource/flatfile-test.txt +++ b/src/test/resources/fr/xephi/authme/datasource/flatfile-test.txt @@ -1,7 +1,7 @@ -Bobby:$SHA$11aa0706173d7272$dbba966:123.45.67.89:1449136800:1.05:2.1:4.2:world:your@email.com +bobby:$SHA$11aa0706173d7272$dbba966:123.45.67.89:1449136800:1.05:2.1:4.2:world:your@email.com user:b28c32f624a4eb161d6adc9acb5bfc5b:34.56.78.90:1453242857:124.1:76.3:-127.8:nether:user@example.org -twoFields:hash1234 -threeFields:hash369:33.33.33.33 -fourFields:$hash$4444:4.4.4.4:404040404 -sevenFields:hash7749:5.5.5.55:1414141414:7.7:14.14:21.21 -eightFields:hash8168:6.6.6.66:1234567888:8.8:17.6:26.4:eightworld +twofields:hash1234 +threefields:hash369:33.33.33.33 +fourfields:$hash$4444:4.4.4.4:404040404 +sevenfields:hash7749:5.5.5.55:1414141414:7.7:14.14:21.21 +eightfields:hash8168:6.6.6.66:1234567888:8.8:17.6:26.4:eightworld diff --git a/src/test/resources/fr/xephi/authme/message/messages_test.yml b/src/test/resources/fr/xephi/authme/message/messages_test.yml index 3c03dd1c4..d955a6a64 100644 --- a/src/test/resources/fr/xephi/authme/message/messages_test.yml +++ b/src/test/resources/fr/xephi/authme/message/messages_test.yml @@ -1,7 +1,7 @@ # Sample messages file unknown_user: 'We''ve got%nl%new lines%nl%and '' apostrophes' -unsafe_spawn: '&cHere we have&bdefined some colors &dand some other <hings' +login: '&cHere we have&bdefined some colors &dand some other <hings' reg_voluntarily: 'You can register yourself to the server with the command "/register "' usage_log: '&cUsage: /login ' wrong_pwd: '&cWrong password!' diff --git a/src/test/resources/fr/xephi/authme/message/messages_test2.yml b/src/test/resources/fr/xephi/authme/message/messages_test2.yml index bdbedb8ae..e4a607239 100644 --- a/src/test/resources/fr/xephi/authme/message/messages_test2.yml +++ b/src/test/resources/fr/xephi/authme/message/messages_test2.yml @@ -1,6 +1,6 @@ # Sample messages file unknown_user: 'Message from test2' -unsafe_spawn: 'test2 - unsafe spawn' +login: 'test2 - login' not_logged_in: 'test2 - not logged in' wrong_pwd: 'test2 - wrong password' diff --git a/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.complete.yml b/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.complete.yml index 8c6bf79de..757f09ca3 100644 --- a/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.complete.yml +++ b/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.complete.yml @@ -2,14 +2,14 @@ onJoin: broadcast: - command: 'broadcast %p has joined' + command: 'broadcast %nick has joined' executor: CONSOLE onRegister: announce: command: 'me I just registered' executor: PLAYER notify: - command: 'log %p registered' + command: 'log %p (%ip, %country) registered' executor: CONSOLE onLogin: welcome: diff --git a/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.incomplete.yml b/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.incomplete.yml index 2eef86b03..a1e9060f7 100644 --- a/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.incomplete.yml +++ b/src/test/resources/fr/xephi/authme/settings/commandconfig/commands.incomplete.yml @@ -6,7 +6,7 @@ onJoin: executor: CONSOLE onLogin: welcome: - command: 'msg %p Welcome back' + command: 'msg %p Welcome back, %nick' executor: CONSOLE show_motd: # command: 'motd' <-- mandatory property, so entry should be ignored diff --git a/src/test/resources/fr/xephi/authme/settings/config-old.yml b/src/test/resources/fr/xephi/authme/settings/config-old.yml index 5181b3f7c..65e2614ce 100644 --- a/src/test/resources/fr/xephi/authme/settings/config-old.yml +++ b/src/test/resources/fr/xephi/authme/settings/config-old.yml @@ -290,7 +290,7 @@ permission: # to use Vault and Group Switching of # AuthMe for unloggedIn players put true # below, default is false. - EnablePermissionCheck: false + EnablePermissionCheck: true BackupSystem: # Enable or Disable Automatic Backup ActivateBackup: false