Merge branch 'master' of https://github.com/AuthMe/AuthMeReloaded into delay-commands-event

This commit is contained in:
DNx 2018-07-07 21:03:16 +07:00
commit 2e5fdb4ca0
264 changed files with 10660 additions and 5707 deletions

52
.circleci/config.yml Normal file
View File

@ -0,0 +1,52 @@
version: 2
jobs:
build_and_test_jdk8:
working_directory: ~/authmereloaded-jdk8
docker:
- image: circleci/openjdk:8-jdk
environment:
MAVEN_OPTS: -Xmx2048m
steps:
- checkout
- restore_cache:
keys:
- authmereloaded-{{ checksum "pom.xml" }}
- authmereloaded-
- run: mvn -T 2 -B dependency:go-offline
- save_cache:
paths:
- ~/.m2
key: authmereloaded-{{ checksum "pom.xml" }}
- run: mvn -T 2 -B package
- store_test_results:
path: target/surefire-reports
- store_artifacts:
path: target/*.jar
build_and_test_jdk10:
working_directory: ~/authmereloaded-jdk10
docker:
- image: circleci/openjdk:10-jdk
environment:
MAVEN_OPTS: -Xmx2048m
steps:
- checkout
- restore_cache:
keys:
- authmereloaded-{{ checksum "pom.xml" }}
- authmereloaded-
- run: mvn -T 2 -B dependency:go-offline
- save_cache:
paths:
- ~/.m2
key: authmereloaded-{{ checksum "pom.xml" }}
- run: mvn -T 2 -B package
- store_test_results:
path: target/surefire-reports
- store_artifacts:
path: target/*.jar
workflows:
version: 2
build_and_test:
jobs:
- build_and_test_jdk8
- build_and_test_jdk10

View File

@ -19,7 +19,7 @@ Standalone server/Bungeecord network, SQLite/MySql, ...
This can be found by running `/authme version`
### Error Log:
Pastebin/Hastebin/Gist link of the error logo or stacktrace (if any)
Pastebin/Hastebin/Gist link of the error log or stacktrace (if any)
### Configuration:
Pastebin/Hastebin/Gist link of your config.yml file (remember to delete any sensitive data)

View File

@ -1,25 +0,0 @@
sudo: required
addons:
apt:
packages:
- oracle-java8-installer
- git
language: java
jdk:
- oraclejdk8
- oraclejdk9
before_script:
- "sudo git clone https://www.github.com/P-H-C/phc-winner-argon2.git argon2-src"
- "cd argon2-src && sudo make && sudo make install && cd .."
script: mvn clean verify -B
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/952357dbd9d3cea70fd5
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false

115
README.md
View File

@ -1,54 +1,15 @@
<p align="center"><img src="http://i63.tinypic.com/rtp06o.png"></p>
<p align="center"><strong>The most used authentication plugin for the Spigot and derivates!</strong></p>
<hr>
# AuthMeReloaded
**"The best authentication plugin for the Bukkit modding API!"**
<img src="http://i63.tinypic.com/rtp06o.png" alt="AuthMeLogo" style="width: 250px;"/>
##### Links and Contacts:
| Type | Badges |
|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **General:** | ![](https://tokei.rs/b1/github/AuthMe/AuthMeReloaded?category=code) ![](https://tokei.rs/b1/github/AuthMe/AuthMeReloaded?category=files) |
| **Code quality:** | [![Code Climate](https://codeclimate.com/github/AuthMe/AuthMeReloaded/badges/gpa.svg)](https://codeclimate.com/github/AuthMe/AuthMeReloaded) [![Coverage status](https://coveralls.io/repos/AuthMe-Team/AuthMeReloaded/badge.svg?branch=master&service=github)](https://coveralls.io/github/AuthMe-Team/AuthMeReloaded?branch=master) |
| **Jenkins CI:** | [![Jenkins Status](https://img.shields.io/website-up-down-green-red/http/shields.io.svg?label=ci.codemc.org)](https://ci.codemc.org/) [![Build Status](https://ci.codemc.org/buildStatus/icon?job=AuthMe/AuthMeReloaded)](https://ci.codemc.org/job/AuthMe/job/AuthMeReloaded) ![Build Tests](https://img.shields.io/jenkins/t/https/ci.codemc.org/job/AuthMe/job/AuthMeReloaded.svg) |
| **Other CIs:** | [![CircleCI](https://circleci.com/gh/AuthMe/AuthMeReloaded.svg?style=svg)](https://circleci.com/gh/AuthMe/AuthMeReloaded) |
- Support:
- [GitHub issue tracker](https://github.com/AuthMe/AuthMeReloaded/issues)
- [BukkitDev page](https://dev.bukkit.org/projects/authme-reloaded)
- [Spigot page](https://www.spigotmc.org/resources/authmereloaded.6269/)
- [Discord](https://discord.gg/Vn9eCyE)
- CI Services:
- [Official Jenkins](http://ci.xephi.fr/job/AuthMeReloaded) (**DEVELOPMENT BUILDS**)
- Travis CI: [![Travis CI](https://travis-ci.org/AuthMe/AuthMeReloaded.svg?branch=master)](https://travis-ci.org/AuthMe/AuthMeReloaded)
- CircleCI: [![CircleCI](https://circleci.com/gh/AuthMe/AuthMeReloaded.svg?style=svg)](https://circleci.com/gh/AuthMe/AuthMeReloaded)
- Project status:
- Test coverage: [![Coverage status](https://coveralls.io/repos/AuthMe-Team/AuthMeReloaded/badge.svg?branch=master&service=github)](https://coveralls.io/github/AuthMe-Team/AuthMeReloaded?branch=master)
- Code climate: [![Code Climate](https://codeclimate.com/github/AuthMe/AuthMeReloaded/badges/gpa.svg)](https://codeclimate.com/github/AuthMe/AuthMeReloaded)
- Development resources:
- <a href="http://ci.xephi.fr/job/AuthMeReloaded/javadoc/">JavaDocs</a>
- <a href="http://ci.xephi.fr/plugin/repository/everything/">Maven Repository</a>
- Statistics:
- bStats: [AuthMe on bstats.org](https://bstats.org/plugin/bukkit/AuthMe)
<hr>
##### Compiling requirements:
>- JDK 1.8
>- Maven
>- Git/Github (Optional)
##### How to compile the project:
>- Clone the project with Git/Github
>- Execute command "mvn clean package"
##### Running requirements:
>- Java 1.8
>- TacoSpigot, PaperSpigot or Spigot (1.7.10, 1.8.X, 1.9.X, 1.10.X, 1.11.X, 1.12.X)<br>
(In case you use Thermos, Cauldron or similar, you have to update the SpecialSource library to support Java 8 plugins.
HowTo: https://github.com/games647/FastLogin/issues/111#issuecomment-272331347)
>- ProtocolLib (optional, required by some features)
<hr>
### Plugin Description:
##### "The best authentication plugin for the Bukkit/Spigot API!"
## Description
Prevent username stealing on your server!<br>
Use it to secure your Offline mode server or to increase your Online mode server's protection!
@ -65,7 +26,7 @@ You can also create your own translation file and, if you want, you can share it
<ul>
<li><strong>E-Mail Recovery System !!!</strong></li>
<li>Username spoofing protection.</li>
<li>Countries Whitelist/Blacklist! <a href="http://dev.maxmind.com/geoip/legacy/codes/iso3166/">(country codes)</a></li>
<li>Countries Whitelist/Blacklist! <a href="https://dev.maxmind.com/geoip/legacy/codes/iso3166/">(country codes)</a></li>
<li><strong>Built-in AntiBot System!</strong></li>
<li><strong>ForceLogin Feature: Admins can login with all account via console command!</strong></li>
<li><strong>Avoid the "Logged in from another location" message!</strong></li>
@ -117,15 +78,51 @@ You can also create your own translation file and, if you want, you can share it
- [How to convert from Rakamak](https://dev.bukkit.org/projects/authme-reloaded/pages/how-to-import-database-from-rakamak)
- Convert between database types (e.g. SQLite to MySQL): /authme converter
<hr>
##### Sponsor
[GameHosting.it](http://www.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.
## Links and Contacts
##### Credits
<p>Contributors: <a href="https://github.com/AuthMe/AuthMeReloaded/wiki/Development-team">developers</a>, <a href="https://github.com/AuthMe/AuthMeReloaded/wiki/Translators">translators</a>
<p>Credit for old version of the plugin to: d4rkwarriors, fabe1337, Whoami2 and pomo4ka</p>
<p>Thanks also to: AS1LV3RN1NJA, Hoeze and eprimex</p>
- **Support:**
- [GitHub issue tracker](https://github.com/AuthMe/AuthMeReloaded/issues)
- [BukkitDev page](https://dev.bukkit.org/projects/authme-reloaded)
- [Spigot page](https://www.spigotmc.org/resources/authmereloaded.6269/)
- [Discord](https://discord.gg/Vn9eCyE)
##### GeoIP License
This product uses data from the GeoLite API created by MaxMind, available at http://www.maxmind.com
- **Dev resources:**
- <a href="https://ci.codemc.org/job/AuthMe/job/AuthMeReloaded/javadoc/">JavaDocs</a>
- <a href="http://repo.codemc.org/repository/maven-public/">Maven Repository</a>
- **Statistics:**
![Graph](https://bstats.org/signatures/bukkit/AuthMe.svg)
## Requirements
##### Compiling requirements:
>- JDK 1.8
>- Maven
>- Git/Github (Optional)
##### How to compile the project:
>- Clone the project with Git/Github
>- Execute command "mvn clean package"
##### Running requirements:
>- Java 1.8
>- TacoSpigot, PaperSpigot or Spigot (1.7.10, 1.8.X, 1.9.X, 1.10.X, 1.11.X, 1.12.X)<br>
(In case you use Thermos, Cauldron or similar, you have to update the SpecialSource library to support Java 8 plugins.
HowTo: https://github.com/games647/FastLogin/issues/111#issuecomment-272331347)
>- ProtocolLib (optional, required by some features)
## Credits
##### Sponsor:
[FastVM.io](https://fastvm.io) is leader in VPS hosting solutions. With its own DataCenter offers Anti-DDoS solutions at affordable prices.
##### Contributors:
Team members: <a href="https://github.com/AuthMe/AuthMeReloaded/wiki/Development-team">developers</a>, <a href="https://github.com/AuthMe/AuthMeReloaded/wiki/Translators">translators</a>
Credits for the old version of the plugin: d4rkwarriors, fabe1337, Whoami2 and pomo4ka
Thanks also to: AS1LV3RN1NJA, Hoeze and eprimex
##### GeoIP License:
This product uses data from the GeoLite API created by MaxMind, available at https://www.maxmind.com

View File

@ -1,20 +0,0 @@
machine:
java:
version: oraclejdk8
dependencies:
pre:
- "sudo apt-get update; sudo apt-get install -y git; sudo git clone https://www.github.com/P-H-C/phc-winner-argon2.git argon2-src; cd argon2-src; sudo make; sudo make install"
general:
artifacts:
- "target/AuthMe-*.jar"
test:
override:
- mvn clean install -B
post:
- cp ./target/AuthMe-*.jar $CIRCLE_ARTIFACTS
- cp ./target/AuthMe-*.jar $CIRCLE_ARTIFACTS/AuthMe.jar
- mkdir -p $CIRCLE_TEST_REPORTS/junit/
- find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \;
notify:
webhooks:
- url: https://webhooks.gitter.im/e/7b92ac1a1741748b26bf

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Fri Dec 01 19:16:15 CET 2017. See docs/commands/commands.tpl.md -->
<!-- File auto-generated on Sun Apr 22 11:00:10 CEST 2018. See docs/commands/commands.tpl.md -->
## AuthMe Commands
You can use the following commands to use the features of AuthMe. Mandatory arguments are marked with `< >`
@ -49,7 +49,7 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
- **/authme version**: Show detailed information about the installed AuthMeReloaded version, the developers, contributors, and license.
- **/authme converter** [job]: Converter command for AuthMeReloaded.
<br />Requires `authme.admin.converter`
- **/authme messages** [help]: Adds missing messages to the current messages file.
- **/authme messages**: Adds missing texts to the current help messages file.
<br />Requires `authme.admin.updatemessages`
- **/authme recent**: Shows the last players that have logged in.
<br />Requires `authme.admin.seerecent`
@ -85,6 +85,15 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`).
- **/changepassword** &lt;oldPassword> &lt;newPassword>: Command to change your password using AuthMeReloaded.
<br />Requires `authme.player.changepassword`
- **/changepassword help** [query]: View detailed help for /changepassword commands.
- **/totp**: Performs actions related to two-factor authentication.
- **/totp code** &lt;code>: Processes the two-factor authentication code during login.
- **/totp add**: Enables two-factor authentication for your account.
<br />Requires `authme.player.totpadd`
- **/totp confirm** &lt;code>: Saves the generated TOTP secret after confirmation.
<br />Requires `authme.player.totpadd`
- **/totp remove** &lt;code>: Disables two-factor authentication for your account.
<br />Requires `authme.player.totpremove`
- **/totp help** [query]: View detailed help for /totp commands.
- **/captcha** &lt;captcha>: Captcha command for AuthMeReloaded.
<br />Requires `authme.player.captcha`
- **/captcha help** [query]: View detailed help for /captcha commands.
@ -95,4 +104,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 Fri Dec 01 19:16:15 CET 2017
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:00:10 CEST 2018

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Sun Jan 21 18:49:44 CET 2018. See docs/config/config.tpl.md -->
<!-- File auto-generated on Mon May 21 09:08:25 CEST 2018. See docs/config/config.tpl.md -->
## AuthMe Configuration
The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder,
@ -37,12 +37,16 @@ DataSource:
mySQLRealName: 'realname'
# Column for storing players passwords
mySQLColumnPassword: 'password'
# Column for storing players passwords salts
mySQLColumnSalt: ''
# Column for storing players emails
mySQLColumnEmail: 'email'
# Column for storing if a player is logged in or not
mySQLColumnLogged: 'isLogged'
# Column for storing if a player has a valid session or not
mySQLColumnHasSession: 'hasSession'
# Column for storing a player's TOTP key (for two-factor authentication)
mySQLtotpKey: 'totp'
# Column for storing the player's last IP
mySQLColumnIp: 'ip'
# Column for storing players lastlogins
@ -69,8 +73,6 @@ DataSource:
# You should set this at least 30 seconds less than mysql server wait_timeout
maxLifetime: 1800
ExternalBoardOptions:
# Column for storing players passwords salts
mySQLColumnSalt: ''
# Column for storing players groups
mySQLColumnGroup: ''
# -1 means disabled. If you want that only activated players
@ -138,6 +140,8 @@ settings:
- '/reg'
- '/email'
- '/captcha'
- '/2fa'
- '/totp'
# Max number of allowed registrations per IP
# The value 0 means an unlimited number of registrations!
maxRegPerIp: 1
@ -384,7 +388,7 @@ Protection:
# Apply the protection also to registered usernames
enableProtectionRegistered: true
# Countries allowed to join the server and register. For country codes, see
# http://dev.maxmind.com/geoip/legacy/codes/iso3166/
# https://dev.maxmind.com/geoip/legacy/codes/iso3166/
# PLEASE USE QUOTES!
countries:
- 'US'
@ -404,6 +408,9 @@ Protection:
antiBotDuration: 10
# Delay in seconds before the antibot activation
antiBotDelay: 60
quickCommands:
# Kicks the player that issued a command before the defined time after the join process
denyCommandsBeforeMilliseconds: 1000
Purge:
# If enabled, AuthMe automatically purges old, unused accounts
useAutoPurge: false
@ -555,4 +562,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 Sun Jan 21 18:49:44 CET 2018
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon May 21 09:08:25 CEST 2018

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Fri Dec 01 19:16:17 CET 2017. See docs/permissions/permission_nodes.tpl.md -->
<!-- File auto-generated on Mon May 21 08:43:08 CEST 2018. See docs/permissions/permission_nodes.tpl.md -->
## AuthMe Permission Nodes
The following are the permission nodes that are currently supported by the latest dev builds.
@ -30,6 +30,7 @@ The following are the permission nodes that are currently supported by the lates
- **authme.admin.switchantibot** Administrator command to toggle the AntiBot protection status.
- **authme.admin.unregister** Administrator command to unregister an existing user.
- **authme.admin.updatemessages** Permission to use the update messages command.
- **authme.allowchatbeforelogin** Permission to send chat messages before being logged in.
- **authme.allowmultipleaccounts** Permission to be able to register multiple accounts.
- **authme.bypassantibot** Permission node to bypass AntiBot protection.
- **authme.bypasscountrycheck** Permission to bypass the GeoIp country code check.
@ -57,13 +58,16 @@ The following are the permission nodes that are currently supported by the lates
- **authme.player.email.see** Command permission to see the own email address.
- **authme.player.login** Command permission to login.
- **authme.player.logout** Command permission to logout.
- **authme.player.protection.quickcommandsprotection** Permission that enables on join quick commands checks for the player.
- **authme.player.register** Command permission to register.
- **authme.player.security.verificationcode** Permission to use the email verification codes feature.
- **authme.player.seeownaccounts** Permission to use to see own other accounts.
- **authme.player.totpadd** Permission to enable two-factor authentication.
- **authme.player.totpremove** Permission to disable two-factor authentication.
- **authme.player.unregister** Command permission to unregister.
- **authme.vip** When the server is full and someone with this permission joins the server, someone will be kicked.
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Dec 01 19:16:17 CET 2017
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon May 21 08:43:08 CEST 2018

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Sun Jan 14 11:14:59 CET 2018. See docs/translations/translations.tpl.md -->
<!-- File auto-generated on Mon Jun 25 21:53:35 CEST 2018. See docs/translations/translations.tpl.md -->
# AuthMe Translations
The following translations are available in AuthMe. Set `messagesLanguage` to the language code
@ -8,37 +8,37 @@ in your config.yml to use the language, or use another language code to start a
Code | Language | Translated | &nbsp;
---- | -------- | ---------: | ------
[en](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_en.yml) | English | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 86% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=86&h=5&txtpad=1" alt="bar" />
[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 48% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=48&h=5&txtpad=1" alt="bar" />
[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 51% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=51&h=5&txtpad=1" alt="bar" />
[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 54% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=54&h=5&txtpad=1" alt="bar" />
[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 53% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb6600&w=53&h=5&txtpad=1" alt="bar" />
[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 40% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa4400&w=40&h=5&txtpad=1" alt="bar" />
[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 86% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=86&h=5&txtpad=1" alt="bar" />
[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 71% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=71&h=5&txtpad=1" alt="bar" />
[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 87% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=87&h=5&txtpad=1" alt="bar" />
[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 90% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=90&h=5&txtpad=1" alt="bar" />
[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 73% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb8800&w=73&h=5&txtpad=1" alt="bar" />
[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 98% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ee55&w=98&h=5&txtpad=1" alt="bar" />
[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 76% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=76&h=5&txtpad=1" alt="bar" />
[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 92% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=92&h=5&txtpad=1" alt="bar" />
[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 42% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=42&h=5&txtpad=1" alt="bar" />
[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 45% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=45&h=5&txtpad=1" alt="bar" />
[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 89% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=89&h=5&txtpad=1" alt="bar" />
[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 48% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=48&h=5&txtpad=1" alt="bar" />
[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 87% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=87&h=5&txtpad=1" alt="bar" />
[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 47% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa5500&w=47&h=5&txtpad=1" alt="bar" />
[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 89% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=89&h=5&txtpad=1" alt="bar" />
[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 36% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aa4400&w=36&h=5&txtpad=1" alt="bar" />
[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 100% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=66ff66&w=100&h=5&txtpad=1" alt="bar" />
[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 92% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=77dd44&w=92&h=5&txtpad=1" alt="bar" />
[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 76% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=76&h=5&txtpad=1" alt="bar" />
[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 63% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=63&h=5&txtpad=1" alt="bar" />
[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 77% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb9900&w=77&h=5&txtpad=1" alt="bar" />
[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 89% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=88cc33&w=89&h=5&txtpad=1" alt="bar" />
[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 80% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=aaaa11&w=80&h=5&txtpad=1" alt="bar" />
[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 65% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=bb7700&w=65&h=5&txtpad=1" alt="bar" />
[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 87% | <img src="https://placeholdit.imgix.net/~text?txtsize=5&bg=99bb22&w=87&h=5&txtpad=1" alt="bar" />
---
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Jan 14 11:14:59 CET 2018
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Jun 25 21:53:35 CEST 2018

287
pom.xml
View File

@ -27,7 +27,7 @@
<ciManagement>
<system>jenkins</system>
<url>http://ci.xephi.fr/job/AuthMeReloaded/</url>
<url>http://ci.codemc.org/job/AuthMe/job/AuthMeReloaded/</url>
</ciManagement>
<issueManagement>
@ -37,11 +37,11 @@
<distributionManagement>
<snapshotRepository>
<id>nexus-snapshots</id>
<id>codemc-snapshots</id>
<url>https://repo.codemc.org/repository/maven-snapshots/</url>
</snapshotRepository>
<repository>
<id>nexus-releases</id>
<id>codemc-releases</id>
<url>https://repo.codemc.org/repository/maven-releases/</url>
</repository>
</distributionManagement>
@ -57,7 +57,8 @@
<properties>
<!-- Project properties -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.jdkVersion>1.8</project.jdkVersion>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<jdk.version>1.8</jdk.version>
<!-- Output properties -->
<project.outputName>AuthMe</project.outputName>
@ -75,6 +76,10 @@
<bukkit.version>1.12.2-R0.1-SNAPSHOT</bukkit.version>
</properties>
<prerequisites>
<maven>3.3.9</maven>
</prerequisites>
<!-- Jenkins profile -->
<profiles>
<!-- Set the buildNumber using the jenkins env. variable -->
@ -104,7 +109,8 @@
</profiles>
<build>
<finalName>${project.outputName}-${project.version}</finalName>
<defaultGoal>clean install</defaultGoal>
<finalName>${project.outputName}-${project.version}-noshade</finalName>
<resources>
<resource>
@ -126,36 +132,107 @@
</resources>
<plugins>
<!-- Maven Java Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>${project.jdkVersion}</source>
<target>${project.jdkVersion}</target>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
</configuration>
</plugin>
<!-- Test Plugin -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<executions>
<execution>
<id>pre-unit-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>post-unit-test</id>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
<version>2.21.0</version>
<configuration>
<!-- Force the right file encoding during unit testing -->
<argLine>-Dfile.encoding=${project.build.sourceEncoding} @{argLine}</argLine>
<!-- Set language to English in order to get consistent results for localized time formatting -->
<argLine>-Dfile.encoding=${project.build.sourceEncoding} -Duser.language=en @{argLine}</argLine>
<systemPropertyVariables>
<project.skipExtendedHashTests>${project.skipExtendedHashTests}</project.skipExtendedHashTests>
</systemPropertyVariables>
</configuration>
</plugin>
<!-- Libs Shading and Relocation -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-javadoc</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
<execution>
<id>aggregate-javadoc</id>
<goals>
<goal>aggregate</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>${project.outputName}-${project.version}</finalName>
<show>public</show>
<failOnError>false</failOnError>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>${project.outputName}-${project.version}</finalName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
<executions>
<!-- Run shade goal on package phase -->
<execution>
<phase>package</phase>
<goals>
@ -165,7 +242,9 @@
</executions>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedArtifactAttached>true</shadedArtifactAttached>
<finalName>${project.outputName}-${project.version}</finalName>
<!--
Relocate all lib we use in order to fix class loading errors if we use different versions
than already loaded libs (i.e. by Mojang -> gson)
@ -174,31 +253,31 @@
<!-- Include all google libraries, because they are not available before 1.12 -->
<relocation>
<pattern>com.google</pattern>
<shadedPattern>fr.xephi.authme.libs.google</shadedPattern>
<shadedPattern>fr.xephi.authme.libs.com.google</shadedPattern>
</relocation>
<relocation>
<pattern>ch.jalu.injector</pattern>
<shadedPattern>fr.xephi.authme.libs.jalu.injector</shadedPattern>
</relocation>
<relocation>
<pattern>ch.jalu.configme</pattern>
<shadedPattern>fr.xephi.authme.libs.jalu.configme</shadedPattern>
<pattern>ch.jalu</pattern>
<shadedPattern>fr.xephi.authme.libs.ch.jalu</shadedPattern>
</relocation>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
<shadedPattern>fr.xephi.authme.libs.zaxxer.hikari</shadedPattern>
<shadedPattern>fr.xephi.authme.libs.com.zaxxer.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>org.slf4j</pattern>
<shadedPattern>fr.xephi.authme.libs.slf4j.slf4j</shadedPattern>
<shadedPattern>fr.xephi.authme.libs.org.slf4j</shadedPattern>
</relocation>
<relocation>
<pattern>com.maxmind.geoip</pattern>
<shadedPattern>fr.xephi.authme.libs.maxmind.geoip</shadedPattern>
<pattern>com.maxmind.db</pattern>
<shadedPattern>fr.xephi.authme.libs.com.maxmind.db</shadedPattern>
</relocation>
<relocation>
<pattern>com.ice.tar</pattern>
<shadedPattern>fr.xephi.authme.libs.com.icetar.tar</shadedPattern>
</relocation>
<relocation>
<pattern>net.ricecode.similarity</pattern>
<shadedPattern>fr.xephi.authme.libs.ricecode.similarity</shadedPattern>
<shadedPattern>fr.xephi.authme.libs.ricecode.net.ricecode.similarity</shadedPattern>
</relocation>
<relocation>
<pattern>de.rtner</pattern>
@ -208,6 +287,10 @@
<pattern>de.mkammerer</pattern>
<shadedPattern>fr.xephi.authme.libs.de.mkammerer</shadedPattern>
</relocation>
<relocation>
<pattern>com.warrenstrange</pattern>
<shadedPattern>fr.xephi.authme.libs.com.warrenstrange</shadedPattern>
</relocation>
<relocation>
<pattern>javax.inject</pattern>
<shadedPattern>fr.xephi.authme.libs.javax.inject</shadedPattern>
@ -220,58 +303,24 @@
</relocations>
</configuration>
</plugin>
<!-- Coverage report generator -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.0</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Coverage report uploader -->
<plugin>
<groupId>org.eluder.coveralls</groupId>
<artifactId>coveralls-maven-plugin</artifactId>
<version>4.3.0</version>
<configuration>
<failOnServiceError>false</failOnServiceError>
</configuration>
<!-- The secret token is provided with a command-line parameter! -->
</plugin>
<!-- JavaDocs generator -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.4</version>
<configuration>
<show>public</show>
<failOnError>false</failOnError>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<phase>deploy</phase>
<goals><goal>jar</goal></goals>
</execution>
</executions>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
<executions>
<execution>
<id>deploy</id>
<phase>deploy</phase>
<goals><goal>deploy</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.eluder.coveralls</groupId>
<artifactId>coveralls-maven-plugin</artifactId>
<version>4.3.0</version>
<configuration>
<!-- The secret token is provided with a command-line parameter! -->
<failOnServiceError>false</failOnServiceError>
</configuration>
</plugin>
</plugins>
</build>
@ -301,12 +350,6 @@
<url>http://repo.onarandombox.com/content/groups/public</url>
</repository>
<!-- LuckPerms Repo -->
<repository>
<id>luck-repo</id>
<url>https://repo.lucko.me/</url>
</repository>
<!-- Vault Repo -->
<repository>
<id>vault-repo</id>
@ -315,8 +358,8 @@
<!-- Our Repo (Many libs) -->
<repository>
<id>xephi-repo</id>
<url>http://ci.xephi.fr/plugin/repository/everything/</url>
<id>codemc-repo</id>
<url>https://repo.codemc.org/repository/maven-public/</url>
</repository>
<!-- bStats Repo -->
@ -334,7 +377,6 @@
<groupId>ch.jalu</groupId>
<artifactId>injector</artifactId>
<version>1.0</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -343,7 +385,6 @@
<groupId>net.ricecode</groupId>
<artifactId>string-similarity</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -351,8 +392,7 @@
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
<scope>compile</scope>
<version>2.8.5</version>
<optional>true</optional>
</dependency>
@ -360,17 +400,24 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.5-jre</version>
<scope>compile</scope>
<version>25.1-jre</version>
<optional>true</optional>
</dependency>
<!-- Maxmind GeoIp API -->
<!-- MaxMind GEO IP with our modifications to use GSON in replacement of the big Jackson dependency -->
<!-- GSON is already included and therefore it reduces the file size in comparison to the original version -->
<dependency>
<groupId>com.maxmind.geoip</groupId>
<artifactId>geoip-api</artifactId>
<version>1.3.1</version>
<scope>compile</scope>
<groupId>com.maxmind.db</groupId>
<artifactId>maxmind-db-gson</artifactId>
<version>2.0.2-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<!-- Library for tar archives -->
<dependency>
<groupId>javatar</groupId>
<artifactId>javatar</artifactId>
<version>2.5</version>
<optional>true</optional>
</dependency>
@ -379,7 +426,6 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>
<version>1.5</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -395,22 +441,20 @@
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.7.4</version>
<scope>compile</scope>
<version>3.2.0</version>
<optional>true</optional>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<optional>true</optional>
</dependency>
<!-- HikariCP Logger -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -419,7 +463,6 @@
<groupId>de.rtner</groupId>
<artifactId>PBKDF2</artifactId>
<version>1.1.2</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -427,7 +470,16 @@
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm-nolibs</artifactId>
<version>2.3</version>
<version>2.4</version>
<optional>true</optional>
</dependency>
<!-- TOTP client -->
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.1.5</version>
<optional>true</optional>
</dependency>
<!-- Spigot API, http://www.spigotmc.org/ -->
@ -462,12 +514,25 @@
<!-- Bukkit Libraries -->
<!-- ConfigMe -->
<dependency>
<groupId>ch.jalu</groupId>
<artifactId>configme</artifactId>
<version>0.4.1</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- bStats metrics -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>1.2</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -475,7 +540,7 @@
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib-API</artifactId>
<version>4.4.0-SNAPSHOT</version>
<version>4.3.0</version>
<scope>provided</scope>
<exclusions>
<exclusion>
@ -493,7 +558,7 @@
<dependency>
<groupId>me.lucko.luckperms</groupId>
<artifactId>luckperms-api</artifactId>
<version>4.0-SNAPSHOT</version>
<version>4.2</version>
<scope>provided</scope>
</dependency>
@ -501,7 +566,7 @@
<dependency>
<groupId>ru.tehkode</groupId>
<artifactId>PermissionsEx</artifactId>
<version>1.23.5</version>
<version>1.23.5-SNAPSHOT</version>
<scope>provided</scope>
<exclusions>
<exclusion>
@ -736,19 +801,11 @@
</exclusions>
</dependency>
<!-- ConfigMe -->
<dependency>
<groupId>ch.jalu</groupId>
<artifactId>configme</artifactId>
<version>0.4.1</version>
<scope>compile</scope>
<artifactId>datasourcecolumns</artifactId>
<version>0.1.1-SNAPSHOT</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Unit Testing Libraries -->
@ -771,7 +828,7 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
<version>2.13.0</version>
<version>2.19.0</version>
<exclusions>
<exclusion>
<artifactId>hamcrest-core</artifactId>
@ -784,13 +841,13 @@
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.21.0.1</version>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
<version>1.4.197</version>
<scope>test</scope>
</dependency>

View File

@ -26,11 +26,13 @@ import fr.xephi.authme.service.BackupService;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.MigrationService;
import fr.xephi.authme.service.bungeecord.BungeeReceiver;
import fr.xephi.authme.service.yaml.YamlParseException;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SettingsWarner;
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.ExceptionUtils;
import org.apache.commons.lang.SystemUtils;
import org.bukkit.Server;
import org.bukkit.command.Command;
@ -133,7 +135,13 @@ public class AuthMe extends JavaPlugin {
try {
initialize();
} catch (Throwable th) {
ConsoleLogger.logException("Aborting initialization of AuthMe:", th);
YamlParseException yamlParseException = ExceptionUtils.findThrowableInCause(YamlParseException.class, th);
if (yamlParseException == null) {
ConsoleLogger.logException("Aborting initialization of AuthMe:", th);
} else {
ConsoleLogger.logException("File '" + yamlParseException.getFile() + "' contains invalid YAML. "
+ "Please run its contents through http://yamllint.com", yamlParseException);
}
stopOrUnload();
return;
}
@ -148,9 +156,9 @@ public class AuthMe extends JavaPlugin {
OnStartupTasks.sendMetrics(this, settings);
// 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("Development builds are available on our jenkins, thanks to FastVM.io");
ConsoleLogger.info("Do you want a good vps for your game server? Look at our sponsor FastVM.io leader "
+ "as virtual server provider!");
// Successful message
ConsoleLogger.info("AuthMe " + getPluginVersion() + " build n." + getPluginBuildNumber()

View File

@ -5,7 +5,7 @@ import fr.xephi.authme.output.LogLevel;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.ExceptionUtils;
import java.io.File;
import java.io.FileWriter;
@ -101,7 +101,7 @@ public final class ConsoleLogger {
* @param th The Throwable to log
*/
public static void logException(String message, Throwable th) {
warning(message + " " + StringUtils.formatException(th));
warning(message + " " + ExceptionUtils.formatException(th));
writeLog(Throwables.getStackTraceAsString(th));
}

View File

@ -167,7 +167,7 @@ public class AuthMeApi {
* @param playerName The name of the player to process
*
* @return The date of the last login, or null if the player doesn't exist or has never logged in
* @Deprecated Use Java 8's Instant method {@link #getLastLoginTime(String)}
* @deprecated Use Java 8's Instant method {@link #getLastLoginTime(String)}
*/
@Deprecated
public Date getLastLogin(String playerName) {

View File

@ -12,7 +12,6 @@ import fr.xephi.authme.command.executable.authme.ForceLoginCommand;
import fr.xephi.authme.command.executable.authme.GetEmailCommand;
import fr.xephi.authme.command.executable.authme.GetIpCommand;
import fr.xephi.authme.command.executable.authme.LastLoginCommand;
import fr.xephi.authme.command.executable.authme.MessagesCommand;
import fr.xephi.authme.command.executable.authme.PurgeBannedPlayersCommand;
import fr.xephi.authme.command.executable.authme.PurgeCommand;
import fr.xephi.authme.command.executable.authme.PurgeLastPositionCommand;
@ -26,6 +25,7 @@ import fr.xephi.authme.command.executable.authme.SetSpawnCommand;
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.UpdateHelpMessagesCommand;
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;
@ -33,13 +33,18 @@ 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.EmailSetPasswordCommand;
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;
import fr.xephi.authme.command.executable.register.RegisterCommand;
import fr.xephi.authme.command.executable.totp.AddTotpCommand;
import fr.xephi.authme.command.executable.totp.ConfirmTotpCommand;
import fr.xephi.authme.command.executable.totp.RemoveTotpCommand;
import fr.xephi.authme.command.executable.totp.TotpBaseCommand;
import fr.xephi.authme.command.executable.totp.TotpCodeCommand;
import fr.xephi.authme.command.executable.unregister.UnregisterCommand;
import fr.xephi.authme.command.executable.verification.VerificationCommand;
import fr.xephi.authme.permission.AdminPermission;
@ -55,6 +60,9 @@ import java.util.List;
*/
public class CommandInitializer {
private static final boolean OPTIONAL = true;
private static final boolean MANDATORY = false;
private List<CommandDescription> commands;
public CommandInitializer() {
@ -84,7 +92,7 @@ public class CommandInitializer {
.labels("login", "l", "log")
.description("Login command")
.detailedDescription("Command to log in using AuthMeReloaded.")
.withArgument("password", "Login password", false)
.withArgument("password", "Login password", MANDATORY)
.permission(PlayerPermission.LOGIN)
.executableCommand(LoginCommand.class)
.register();
@ -105,8 +113,8 @@ public class CommandInitializer {
.labels("register", "reg")
.description("Register an account")
.detailedDescription("Command to register using AuthMeReloaded.")
.withArgument("password", "Password", true)
.withArgument("verifyPassword", "Verify password", true)
.withArgument("password", "Password", OPTIONAL)
.withArgument("verifyPassword", "Verify password", OPTIONAL)
.permission(PlayerPermission.REGISTER)
.executableCommand(RegisterCommand.class)
.register();
@ -117,7 +125,7 @@ public class CommandInitializer {
.labels("unregister", "unreg")
.description("Unregister an account")
.detailedDescription("Command to unregister using AuthMeReloaded.")
.withArgument("password", "Password", false)
.withArgument("password", "Password", MANDATORY)
.permission(PlayerPermission.UNREGISTER)
.executableCommand(UnregisterCommand.class)
.register();
@ -128,19 +136,22 @@ public class CommandInitializer {
.labels("changepassword", "changepass", "cp")
.description("Change password of an account")
.detailedDescription("Command to change your password using AuthMeReloaded.")
.withArgument("oldPassword", "Old password", false)
.withArgument("newPassword", "New password", false)
.withArgument("oldPassword", "Old password", MANDATORY)
.withArgument("newPassword", "New password", MANDATORY)
.permission(PlayerPermission.CHANGE_PASSWORD)
.executableCommand(ChangePasswordCommand.class)
.register();
// Create totp base command
CommandDescription totpBase = buildTotpBaseCommand();
// Register the base captcha command
CommandDescription captchaBase = CommandDescription.builder()
.parent(null)
.labels("captcha")
.description("Captcha command")
.detailedDescription("Captcha command for AuthMeReloaded.")
.withArgument("captcha", "The Captcha", false)
.withArgument("captcha", "The Captcha", MANDATORY)
.permission(PlayerPermission.CAPTCHA)
.executableCommand(CaptchaCommand.class)
.register();
@ -151,21 +162,13 @@ public class CommandInitializer {
.labels("verification")
.description("Verification command")
.detailedDescription("Command to complete the verification process for AuthMeReloaded.")
.withArgument("code", "The code", false)
.withArgument("code", "The code", MANDATORY)
.permission(PlayerPermission.VERIFICATION_CODE)
.executableCommand(VerificationCommand.class)
.register();
List<CommandDescription> baseCommands = ImmutableList.of(
authMeBase,
emailBase,
loginBase,
logoutBase,
registerBase,
unregisterBase,
changePasswordBase,
captchaBase,
verificationBase);
List<CommandDescription> baseCommands = ImmutableList.of(authMeBase, emailBase, loginBase, logoutBase,
registerBase, unregisterBase, changePasswordBase, totpBase, captchaBase, verificationBase);
setHelpOnAllBases(baseCommands);
commands = baseCommands;
@ -191,8 +194,8 @@ public class CommandInitializer {
.labels("register", "reg", "r")
.description("Register a player")
.detailedDescription("Register the specified player with the specified password.")
.withArgument("player", "Player name", false)
.withArgument("password", "Password", false)
.withArgument("player", "Player name", MANDATORY)
.withArgument("password", "Password", MANDATORY)
.permission(AdminPermission.REGISTER)
.executableCommand(RegisterAdminCommand.class)
.register();
@ -203,7 +206,7 @@ public class CommandInitializer {
.labels("unregister", "unreg", "unr")
.description("Unregister a player")
.detailedDescription("Unregister the specified player.")
.withArgument("player", "Player name", false)
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.UNREGISTER)
.executableCommand(UnregisterAdminCommand.class)
.register();
@ -214,7 +217,7 @@ public class CommandInitializer {
.labels("forcelogin", "login")
.description("Enforce login player")
.detailedDescription("Enforce the specified player to login.")
.withArgument("player", "Online player name", true)
.withArgument("player", "Online player name", OPTIONAL)
.permission(AdminPermission.FORCE_LOGIN)
.executableCommand(ForceLoginCommand.class)
.register();
@ -225,8 +228,8 @@ public class CommandInitializer {
.labels("password", "changepassword", "changepass", "cp")
.description("Change a player's password")
.detailedDescription("Change the password of a player.")
.withArgument("player", "Player name", false)
.withArgument("pwd", "New password", false)
.withArgument("player", "Player name", MANDATORY)
.withArgument("pwd", "New password", MANDATORY)
.permission(AdminPermission.CHANGE_PASSWORD)
.executableCommand(ChangePasswordAdminCommand.class)
.register();
@ -237,7 +240,7 @@ public class CommandInitializer {
.labels("lastlogin", "ll")
.description("Player's last login")
.detailedDescription("View the date of the specified players last login.")
.withArgument("player", "Player name", true)
.withArgument("player", "Player name", OPTIONAL)
.permission(AdminPermission.LAST_LOGIN)
.executableCommand(LastLoginCommand.class)
.register();
@ -248,7 +251,7 @@ public class CommandInitializer {
.labels("accounts", "account")
.description("Display player accounts")
.detailedDescription("Display all accounts of a player by his player name or IP.")
.withArgument("player", "Player name or IP", true)
.withArgument("player", "Player name or IP", OPTIONAL)
.permission(AdminPermission.ACCOUNTS)
.executableCommand(AccountsCommand.class)
.register();
@ -259,7 +262,7 @@ public class CommandInitializer {
.labels("email", "mail", "getemail", "getmail")
.description("Display player's email")
.detailedDescription("Display the email address of the specified player if set.")
.withArgument("player", "Player name", true)
.withArgument("player", "Player name", OPTIONAL)
.permission(AdminPermission.GET_EMAIL)
.executableCommand(GetEmailCommand.class)
.register();
@ -270,8 +273,8 @@ public class CommandInitializer {
.labels("setemail", "setmail", "chgemail", "chgmail")
.description("Change player's email")
.detailedDescription("Change the email address of the specified player.")
.withArgument("player", "Player name", false)
.withArgument("email", "Player email", false)
.withArgument("player", "Player name", MANDATORY)
.withArgument("email", "Player email", MANDATORY)
.permission(AdminPermission.CHANGE_EMAIL)
.executableCommand(SetEmailCommand.class)
.register();
@ -282,7 +285,7 @@ public class CommandInitializer {
.labels("getip", "ip")
.description("Get player's IP")
.detailedDescription("Get the IP address of the specified online player.")
.withArgument("player", "Player name", false)
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.GET_IP)
.executableCommand(GetIpCommand.class)
.register();
@ -333,7 +336,7 @@ public class CommandInitializer {
.labels("purge", "delete")
.description("Purge old data")
.detailedDescription("Purge old AuthMeReloaded data longer than the specified number of days ago.")
.withArgument("days", "Number of days", false)
.withArgument("days", "Number of days", MANDATORY)
.permission(AdminPermission.PURGE)
.executableCommand(PurgeCommand.class)
.register();
@ -344,8 +347,8 @@ public class CommandInitializer {
.labels("purgeplayer")
.description("Purges the data of one player")
.detailedDescription("Purges data of the given player.")
.withArgument("player", "The player to purge", false)
.withArgument("options", "'force' to run without checking if player is registered", true)
.withArgument("player", "The player to purge", MANDATORY)
.withArgument("options", "'force' to run without checking if player is registered", OPTIONAL)
.permission(AdminPermission.PURGE_PLAYER)
.executableCommand(PurgePlayerCommand.class)
.register();
@ -367,7 +370,7 @@ public class CommandInitializer {
"resetlastposition", "resetlastpos")
.description("Purge player's last position")
.detailedDescription("Purge the last know position of the specified player or all of them.")
.withArgument("player/*", "Player name or * for all players", false)
.withArgument("player/*", "Player name or * for all players", MANDATORY)
.permission(AdminPermission.PURGE_LAST_POSITION)
.executableCommand(PurgeLastPositionCommand.class)
.register();
@ -388,7 +391,7 @@ public class CommandInitializer {
.labels("switchantibot", "toggleantibot", "antibot")
.description("Switch AntiBot mode")
.detailedDescription("Switch or toggle the AntiBot mode to the specified state.")
.withArgument("mode", "ON / OFF", true)
.withArgument("mode", "ON / OFF", OPTIONAL)
.permission(AdminPermission.SWITCH_ANTIBOT)
.executableCommand(SwitchAntiBotCommand.class)
.register();
@ -419,7 +422,7 @@ public class CommandInitializer {
.description("Converter command")
.detailedDescription("Converter command for AuthMeReloaded.")
.withArgument("job", "Conversion job: xauth / crazylogin / rakamak / "
+ "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", true)
+ "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", OPTIONAL)
.permission(AdminPermission.CONVERTER)
.executableCommand(ConverterCommand.class)
.register();
@ -427,11 +430,10 @@ public class CommandInitializer {
CommandDescription.builder()
.parent(authmeBase)
.labels("messages", "msg")
.description("Add missing messages")
.detailedDescription("Adds missing messages to the current messages file.")
.withArgument("help", "Add 'help' to update the help messages file", true)
.description("Add missing help messages")
.detailedDescription("Adds missing texts to the current help messages file.")
.permission(AdminPermission.UPDATE_MESSAGES)
.executableCommand(MessagesCommand.class)
.executableCommand(UpdateHelpMessagesCommand.class)
.register();
CommandDescription.builder()
@ -448,9 +450,9 @@ public class CommandInitializer {
.labels("debug", "dbg")
.description("Debug features")
.detailedDescription("Allows various operations for debugging.")
.withArgument("child", "The child to execute", true)
.withArgument("arg", "argument (depends on debug section)", true)
.withArgument("arg", "argument (depends on debug section)", true)
.withArgument("child", "The child to execute", OPTIONAL)
.withArgument("arg", "argument (depends on debug section)", OPTIONAL)
.withArgument("arg", "argument (depends on debug section)", OPTIONAL)
.permission(DebugSectionPermissions.DEBUG_COMMAND)
.executableCommand(DebugCommand.class)
.register();
@ -489,8 +491,8 @@ public class CommandInitializer {
.labels("add", "addemail", "addmail")
.description("Add Email")
.detailedDescription("Add a new email address to your account.")
.withArgument("email", "Email address", false)
.withArgument("verifyEmail", "Email address verification", false)
.withArgument("email", "Email address", MANDATORY)
.withArgument("verifyEmail", "Email address verification", MANDATORY)
.permission(PlayerPermission.ADD_EMAIL)
.executableCommand(AddEmailCommand.class)
.register();
@ -501,8 +503,8 @@ public class CommandInitializer {
.labels("change", "changeemail", "changemail")
.description("Change Email")
.detailedDescription("Change an email address of your account.")
.withArgument("oldEmail", "Old email address", false)
.withArgument("newEmail", "New email address", false)
.withArgument("oldEmail", "Old email address", MANDATORY)
.withArgument("newEmail", "New email address", MANDATORY)
.permission(PlayerPermission.CHANGE_EMAIL)
.executableCommand(ChangeEmailCommand.class)
.register();
@ -514,7 +516,7 @@ public class CommandInitializer {
.description("Recover password using email")
.detailedDescription("Recover your account using an Email address by sending a mail containing "
+ "a new password.")
.withArgument("email", "Email address", false)
.withArgument("email", "Email address", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(RecoverEmailCommand.class)
.register();
@ -525,7 +527,7 @@ public class CommandInitializer {
.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)
.withArgument("code", "Recovery code", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(ProcessCodeCommand.class)
.register();
@ -536,14 +538,74 @@ public class CommandInitializer {
.labels("setpassword")
.description("Set new password after recovery")
.detailedDescription("Set a new password after successfully recovering your account.")
.withArgument("password", "New password", false)
.withArgument("password", "New password", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(SetPasswordCommand.class)
.executableCommand(EmailSetPasswordCommand.class)
.register();
return emailBase;
}
/**
* Creates a command description object for {@code /totp} including its children.
*
* @return the totp base command description
*/
private CommandDescription buildTotpBaseCommand() {
// Register the base totp command
CommandDescription totpBase = CommandDescription.builder()
.parent(null)
.labels("totp", "2fa")
.description("TOTP commands")
.detailedDescription("Performs actions related to two-factor authentication.")
.executableCommand(TotpBaseCommand.class)
.register();
// Register the base totp code
CommandDescription.builder()
.parent(totpBase)
.labels("code", "c")
.description("Command for logging in")
.detailedDescription("Processes the two-factor authentication code during login.")
.withArgument("code", "The TOTP code to use to log in", MANDATORY)
.executableCommand(TotpCodeCommand.class)
.register();
// Register totp add
CommandDescription.builder()
.parent(totpBase)
.labels("add")
.description("Enables TOTP")
.detailedDescription("Enables two-factor authentication for your account.")
.permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH)
.executableCommand(AddTotpCommand.class)
.register();
// Register totp confirm
CommandDescription.builder()
.parent(totpBase)
.labels("confirm")
.description("Enables TOTP after successful code")
.detailedDescription("Saves the generated TOTP secret after confirmation.")
.withArgument("code", "Code from the given secret from /totp add", MANDATORY)
.permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH)
.executableCommand(ConfirmTotpCommand.class)
.register();
// Register totp remove
CommandDescription.builder()
.parent(totpBase)
.labels("remove")
.description("Removes TOTP")
.detailedDescription("Disables two-factor authentication for your account.")
.withArgument("code", "Current 2FA code", MANDATORY)
.permission(PlayerPermission.DISABLE_TWO_FACTOR_AUTH)
.executableCommand(RemoveTotpCommand.class)
.register();
return totpBase;
}
/**
* Sets the help command on all base commands, e.g. to register /authme help or /register help.
*
@ -558,7 +620,7 @@ public class CommandInitializer {
.labels(helpCommandLabels)
.description("View help")
.detailedDescription("View detailed help for /" + base.getLabels().get(0) + " commands.")
.withArgument("query", "The command or query to view help for.", true)
.withArgument("query", "The command or query to view help for.", OPTIONAL)
.executableCommand(HelpCommand.class)
.register();
}

View File

@ -1,8 +1,8 @@
package fr.xephi.authme.command.executable.authme;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
@ -25,8 +25,8 @@ public class GetEmailCommand implements ExecutableCommand {
public void executeCommand(CommandSender sender, List<String> arguments) {
String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
DataSourceResult<String> email = dataSource.getEmail(playerName);
if (email.playerExists()) {
DataSourceValue<String> email = dataSource.getEmail(playerName);
if (email.rowExists()) {
sender.sendMessage("[AuthMe] " + playerName + "'s email: " + email.getValue());
} else {
commonService.send(sender, MessageKey.UNKNOWN_USER);

View File

@ -1,79 +0,0 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.help.HelpMessagesService;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.HelpTranslationGenerator;
import fr.xephi.authme.service.MessageUpdater;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Messages command, updates the user's messages file with any missing files
* from the provided file in the JAR.
*/
public class MessagesCommand implements ExecutableCommand {
private static final String DEFAULT_LANGUAGE = "en";
@Inject
private Settings settings;
@Inject
@DataFolder
private File dataFolder;
@Inject
private Messages messages;
@Inject
private HelpTranslationGenerator helpTranslationGenerator;
@Inject
private HelpMessagesService helpMessagesService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
if (!arguments.isEmpty() && "help".equalsIgnoreCase(arguments.get(0))) {
updateHelpFile(sender);
} else {
updateMessagesFile(sender);
}
}
private void updateHelpFile(CommandSender sender) {
try {
helpTranslationGenerator.updateHelpFile();
sender.sendMessage("Successfully updated the help file");
helpMessagesService.reload();
} catch (IOException e) {
sender.sendMessage("Could not update help file: " + e.getMessage());
ConsoleLogger.logException("Could not update help file:", e);
}
}
private void updateMessagesFile(CommandSender sender) {
final String language = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
try {
boolean isFileUpdated = new MessageUpdater(
new File(dataFolder, getMessagePath(language)),
getMessagePath(language),
getMessagePath(DEFAULT_LANGUAGE))
.executeCopy(sender);
if (isFileUpdated) {
messages.reload();
}
} catch (Exception e) {
sender.sendMessage("Could not update messages: " + e.getMessage());
ConsoleLogger.logException("Could not update messages:", e);
}
}
private static String getMessagePath(String code) {
return "messages/messages_" + code + ".yml";
}
}

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.command.executable.authme;
import com.google.common.primitives.Ints;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.task.purge.PurgeService;
import org.bukkit.ChatColor;
@ -26,10 +27,8 @@ public class PurgeCommand implements ExecutableCommand {
String daysStr = arguments.get(0);
// Convert the days string to an integer value, and make sure it's valid
int days;
try {
days = Integer.parseInt(daysStr);
} catch (NumberFormatException ex) {
Integer days = Ints.tryParse(daysStr);
if (days == null) {
sender.sendMessage(ChatColor.RED + "The value you've entered is invalid!");
return;
}

View File

@ -74,7 +74,7 @@ public class RegisterAdminCommand implements ExecutableCommand {
final Player player = bukkitService.getPlayerExact(playerName);
if (player != null) {
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() ->
player.kickPlayer(commonService.retrieveSingleMessage(MessageKey.KICK_FOR_ADMIN_REGISTER)));
player.kickPlayer(commonService.retrieveSingleMessage(player, MessageKey.KICK_FOR_ADMIN_REGISTER)));
}
});
}

View File

@ -0,0 +1,36 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.help.HelpMessagesService;
import fr.xephi.authme.service.HelpTranslationGenerator;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Messages command, updates the user's help messages file with any missing files
* from the provided file in the JAR.
*/
public class UpdateHelpMessagesCommand implements ExecutableCommand {
@Inject
private HelpTranslationGenerator helpTranslationGenerator;
@Inject
private HelpMessagesService helpMessagesService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
try {
File updatedFile = helpTranslationGenerator.updateHelpFile();
sender.sendMessage("Successfully updated the help file '" + updatedFile.getName() + "'");
helpMessagesService.reloadMessagesFile();
} catch (IOException e) {
sender.sendMessage("Could not update help file: " + e.getMessage());
ConsoleLogger.logException("Could not update help file:", e);
}
}
}

View File

@ -172,6 +172,11 @@ class MySqlDefaultChanger implements DebugSection {
+ sender.getName() + "'");
}
/**
* Outputs the current definitions of all {@link Columns} which can be migrated.
*
* @param sender command sender to output the data to
*/
private void showColumnDetails(CommandSender sender) {
sender.sendMessage(ChatColor.BLUE + "MySQL column details");
final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);

View File

@ -77,6 +77,7 @@ class PlayerAuthViewer implements DebugSection {
HashedPassword hashedPass = auth.getPassword();
sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6)
+ "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'");
sender.sendMessage("TOTP code (partial): '" + safeSubstring(auth.getTotpKey(), 3) + "'");
}
/**

View File

@ -1,8 +1,8 @@
package fr.xephi.authme.command.executable.authme.debug;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.mail.SendMailSsl;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
@ -82,8 +82,8 @@ class TestEmailSender implements DebugSection {
*/
private String getEmail(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
DataSourceResult<String> emailResult = dataSource.getEmail(sender.getName());
if (!emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(sender.getName());
if (!emailResult.rowExists()) {
sender.sendMessage(ChatColor.RED + "Please provide an email address, "
+ "e.g. /authme debug mail test@example.com");
return null;

View File

@ -4,6 +4,7 @@ import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.captcha.LoginCaptchaManager;
import fr.xephi.authme.data.captcha.RegistrationCaptchaManager;
import fr.xephi.authme.data.limbo.LimboMessageType;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
@ -80,6 +81,6 @@ public class CaptchaCommand extends PlayerCommand {
String newCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName());
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
}
limboService.resetMessageTask(player, false);
limboService.resetMessageTask(player, LimboMessageType.REGISTER);
}
}

View File

@ -18,7 +18,7 @@ import java.util.List;
/**
* Command for changing password following successful recovery.
*/
public class SetPasswordCommand extends PlayerCommand {
public class EmailSetPasswordCommand extends PlayerCommand {
@Inject
private DataSource dataSource;
@ -45,11 +45,14 @@ public class SetPasswordCommand extends PlayerCommand {
if (!result.hasError()) {
HashedPassword hashedPassword = passwordSecurity.computeHash(password, name);
dataSource.updatePassword(name, hashedPassword);
recoveryService.removeFromSuccessfulRecovery(player);
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());
}
} else {
commonService.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED);
}
}
}

View File

@ -1,10 +1,10 @@
package fr.xephi.authme.command.executable.email;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.BukkitService;
@ -58,8 +58,8 @@ public class RecoverEmailCommand extends PlayerCommand {
return;
}
DataSourceResult<String> emailResult = dataSource.getEmail(playerName);
if (!emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(playerName);
if (!emailResult.rowExists()) {
commonService.send(player, MessageKey.USAGE_REGISTER);
return;
}

View File

@ -18,7 +18,7 @@ public class LoginCommand extends PlayerCommand {
@Override
public void runCommand(Player player, List<String> arguments) {
final String password = arguments.get(0);
String password = arguments.get(0);
management.performLogin(player, password);
}

View File

@ -0,0 +1,43 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.totp.GenerateTotpService;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for a player to enable TOTP.
*/
public class AddTotpCommand extends PlayerCommand {
@Inject
private GenerateTotpService generateTotpService;
@Inject
private PlayerCache playerCache;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() == null) {
TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player);
messages.send(player, MessageKey.TWO_FACTOR_CREATE,
createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl());
messages.send(player, MessageKey.TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED);
} else {
messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED);
}
}
}

View File

@ -0,0 +1,71 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
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.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.totp.GenerateTotpService;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command to enable TOTP by supplying the proper code as confirmation.
*/
public class ConfirmTotpCommand extends PlayerCommand {
@Inject
private GenerateTotpService generateTotpService;
@Inject
private PlayerCache playerCache;
@Inject
private DataSource dataSource;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() != null) {
messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED);
} else {
verifyTotpCodeConfirmation(player, auth, arguments.get(0));
}
}
private void verifyTotpCodeConfirmation(Player player, PlayerAuth auth, String inputTotpCode) {
final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player);
if (totpDetails == null) {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE);
} else {
boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, inputTotpCode);
if (isCodeValid) {
generateTotpService.removeGenerateTotpKey(player);
insertTotpKeyIntoDatabase(player, auth, totpDetails);
} else {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE);
}
}
}
private void insertTotpKeyIntoDatabase(Player player, PlayerAuth auth, TotpGenerationResult totpDetails) {
if (dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey())) {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS);
auth.setTotpKey(totpDetails.getTotpKey());
playerCache.updatePlayer(auth);
ConsoleLogger.info("Player '" + player.getName() + "' has successfully added a TOTP key to their account");
} else {
messages.send(player, MessageKey.ERROR);
}
}
}

View File

@ -0,0 +1,59 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
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.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.totp.TotpAuthenticator;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for a player to remove 2FA authentication.
*/
public class RemoveTotpCommand extends PlayerCommand {
@Inject
private DataSource dataSource;
@Inject
private PlayerCache playerCache;
@Inject
private TotpAuthenticator totpAuthenticator;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() == null) {
messages.send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR);
} else {
if (totpAuthenticator.checkCode(auth, arguments.get(0))) {
removeTotpKeyFromDatabase(player, auth);
} else {
messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE);
}
}
}
private void removeTotpKeyFromDatabase(Player player, PlayerAuth auth) {
if (dataSource.removeTotpKey(auth.getNickname())) {
auth.setTotpKey(null);
playerCache.updatePlayer(auth);
messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS);
ConsoleLogger.info("Player '" + player.getName() + "' removed their TOTP key");
} else {
messages.send(player, MessageKey.ERROR);
}
}
}

View File

@ -0,0 +1,29 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.command.CommandMapper;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.FoundCommandResult;
import fr.xephi.authme.command.help.HelpProvider;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Collections;
import java.util.List;
/**
* Base command for /totp.
*/
public class TotpBaseCommand implements ExecutableCommand {
@Inject
private CommandMapper commandMapper;
@Inject
private HelpProvider helpProvider;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("totp"));
helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN);
}
}

View File

@ -0,0 +1,76 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboPlayerState;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.process.login.AsynchronousLogin;
import fr.xephi.authme.security.totp.TotpAuthenticator;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* TOTP code command for processing the 2FA code during the login process.
*/
public class TotpCodeCommand extends PlayerCommand {
@Inject
private LimboService limboService;
@Inject
private PlayerCache playerCache;
@Inject
private Messages messages;
@Inject
private TotpAuthenticator totpAuthenticator;
@Inject
private DataSource dataSource;
@Inject
private AsynchronousLogin asynchronousLogin;
@Override
protected void runCommand(Player player, List<String> arguments) {
if (playerCache.isAuthenticated(player.getName())) {
messages.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR);
return;
}
PlayerAuth auth = dataSource.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.REGISTER_MESSAGE);
return;
}
LimboPlayer limbo = limboService.getLimboPlayer(player.getName());
if (limbo != null && limbo.getState() == LimboPlayerState.TOTP_REQUIRED) {
processCode(player, auth, arguments.get(0));
} else {
ConsoleLogger.debug(() -> "Aborting TOTP check for player '" + player.getName()
+ "'. Invalid limbo state: " + (limbo == null ? "no limbo" : limbo.getState()));
messages.send(player, MessageKey.LOGIN_MESSAGE);
}
}
private void processCode(Player player, PlayerAuth auth, String inputCode) {
boolean isCodeValid = totpAuthenticator.checkCode(auth, inputCode);
if (isCodeValid) {
ConsoleLogger.debug("Successfully checked TOTP code for `{0}`", player.getName());
asynchronousLogin.performLogin(player, auth);
} else {
ConsoleLogger.debug("Input TOTP code was invalid for player `{0}`", player.getName());
messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE);
}
}
}

View File

@ -4,9 +4,7 @@ import com.google.common.base.CaseFormat;
import fr.xephi.authme.command.CommandArgumentDescription;
import fr.xephi.authme.command.CommandDescription;
import fr.xephi.authme.command.CommandUtils;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.message.MessageFileHandlerProvider;
import fr.xephi.authme.message.MessageFileHandler;
import fr.xephi.authme.message.HelpMessagesFileHandler;
import fr.xephi.authme.permission.DefaultPermission;
import javax.inject.Inject;
@ -16,20 +14,18 @@ import java.util.stream.Collectors;
/**
* Manages translatable help messages.
*/
public class HelpMessagesService implements Reloadable {
public class HelpMessagesService {
private static final String COMMAND_PREFIX = "commands.";
private static final String DESCRIPTION_SUFFIX = ".description";
private static final String DETAILED_DESCRIPTION_SUFFIX = ".detailedDescription";
private static final String DEFAULT_PERMISSIONS_PATH = "common.defaultPermissions.";
private final MessageFileHandlerProvider messageFileHandlerProvider;
private MessageFileHandler messageFileHandler;
private final HelpMessagesFileHandler helpMessagesFileHandler;
@Inject
HelpMessagesService(MessageFileHandlerProvider messageFileHandlerProvider) {
this.messageFileHandlerProvider = messageFileHandlerProvider;
reload();
HelpMessagesService(HelpMessagesFileHandler helpMessagesFileHandler) {
this.helpMessagesFileHandler = helpMessagesFileHandler;
}
/**
@ -40,7 +36,7 @@ public class HelpMessagesService implements Reloadable {
*/
public CommandDescription buildLocalizedDescription(CommandDescription command) {
final String path = COMMAND_PREFIX + getCommandSubPath(command);
if (!messageFileHandler.hasSection(path)) {
if (!helpMessagesFileHandler.hasSection(path)) {
// Messages file does not have a section for this command - return the provided command
return command;
}
@ -72,36 +68,39 @@ public class HelpMessagesService implements Reloadable {
}
public String getMessage(HelpMessage message) {
return messageFileHandler.getMessage(message.getKey());
return helpMessagesFileHandler.getMessage(message.getKey());
}
public String getMessage(HelpSection section) {
return messageFileHandler.getMessage(section.getKey());
return helpMessagesFileHandler.getMessage(section.getKey());
}
public String getMessage(DefaultPermission defaultPermission) {
// e.g. {default_permissions_path}.opOnly for DefaultPermission.OP_ONLY
String path = DEFAULT_PERMISSIONS_PATH + getDefaultPermissionsSubPath(defaultPermission);
return messageFileHandler.getMessage(path);
return helpMessagesFileHandler.getMessage(path);
}
public static String getDefaultPermissionsSubPath(DefaultPermission defaultPermission) {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, defaultPermission.name());
}
@Override
public void reload() {
messageFileHandler = messageFileHandlerProvider.initializeHandler(
lang -> "messages/help_" + lang + ".yml");
}
private String getText(String path, Supplier<String> defaultTextGetter) {
String message = messageFileHandler.getMessageIfExists(path);
String message = helpMessagesFileHandler.getMessageIfExists(path);
return message == null
? defaultTextGetter.get()
: message;
}
/**
* Triggers a reload of the help messages file. Note that this method is not needed
* to be called for /authme reload.
*/
public void reloadMessagesFile() {
helpMessagesFileHandler.reload();
}
/**
* Returns the command subpath for the given command (i.e. the path to the translations for the given
* command under "commands").

View File

@ -0,0 +1,75 @@
package fr.xephi.authme.data;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerPermission;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.ProtectionSettings;
import fr.xephi.authme.util.expiring.ExpiringSet;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.concurrent.TimeUnit;
public class QuickCommandsProtectionManager implements SettingsDependent, HasCleanup {
private final PermissionsManager permissionsManager;
private final ExpiringSet<String> latestJoin;
@Inject
public QuickCommandsProtectionManager(Settings settings, PermissionsManager permissionsManager) {
this.permissionsManager = permissionsManager;
long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS);
latestJoin = new ExpiringSet<>(countTimeout, TimeUnit.MILLISECONDS);
reload(settings);
}
/**
* Save the player in the set
* @param name the player's name
*/
private void setJoin(String name) {
latestJoin.add(name);
}
/**
* Returns whether the given player has the permission and should be saved in the set
* @param player the player to check
* @return true if the player has the permission, false otherwise
*/
private boolean shouldSavePlayer(Player player) {
return permissionsManager.hasPermission(player, PlayerPermission.QUICK_COMMANDS_PROTECTION);
}
/**
* Process the player join
* @param player the player to process
*/
public void processJoin(Player player) {
if(shouldSavePlayer(player)) {
setJoin(player.getName());
}
}
/**
* Returns whether the given player is able to perform the command
* @param name the name of the player to check
* @return true if the player is not in the set (so it's allowed to perform the command), false otherwise
*/
public boolean isAllowed(String name) {
return !latestJoin.contains(name);
}
@Override
public void reload(Settings settings) {
long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS);
latestJoin.setExpiration(countTimeout, TimeUnit.MILLISECONDS);
}
@Override
public void performCleanup() {
latestJoin.removeExpiredEntries();
}
}

View File

@ -97,7 +97,7 @@ public class TempbanManager implements SettingsDependent, HasCleanup {
if (isEnabled) {
final String name = player.getName();
final String ip = PlayerUtils.getPlayerIp(player);
final String reason = messages.retrieveSingle(MessageKey.TEMPBAN_MAX_LOGINS);
final String reason = messages.retrieveSingle(player, MessageKey.TEMPBAN_MAX_LOGINS);
final Date expires = new Date();
long newTime = expires.getTime() + (length * MILLIS_PER_MINUTE);

View File

@ -1,7 +1,7 @@
package fr.xephi.authme.data;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceResult;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.mail.EmailService;
@ -103,8 +103,8 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup {
*/
public boolean hasEmail(String name) {
boolean result = false;
DataSourceResult<String> emailResult = dataSource.getEmail(name);
if (emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(name);
if (emailResult.rowExists()) {
final String email = emailResult.getValue();
if (!Utils.isEmailEmpty(email)) {
result = true;
@ -130,8 +130,8 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup {
* @param name the name of the player to generate a code for
*/
private void generateCode(String name) {
DataSourceResult<String> emailResult = dataSource.getEmail(name);
if (emailResult.playerExists()) {
DataSourceValue<String> emailResult = dataSource.getEmail(name);
if (emailResult.rowExists()) {
final String email = emailResult.getValue();
if (!Utils.isEmailEmpty(email)) {
String code = RandomStringUtils.generateNum(6); // 6 digits code
@ -162,7 +162,7 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup {
*
* @param name the name of the player to generate a code for
*/
public void verify(String name){
public void verify(String name) {
verifiedPlayers.add(name.toLowerCase());
}

View File

@ -26,6 +26,7 @@ public class PlayerAuth {
/** The player's name in the correct casing, e.g. "Xephi". */
private String realName;
private HashedPassword password;
private String totpKey;
private String email;
private String lastIp;
private int groupId;
@ -160,6 +161,14 @@ public class PlayerAuth {
this.registrationDate = registrationDate;
}
public String getTotpKey() {
return totpKey;
}
public void setTotpKey(String totpKey) {
this.totpKey = totpKey;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PlayerAuth)) {
@ -195,6 +204,7 @@ public class PlayerAuth {
private String name;
private String realName;
private HashedPassword password;
private String totpKey;
private String lastIp;
private String email;
private int groupId = -1;
@ -219,6 +229,7 @@ public class PlayerAuth {
auth.nickname = checkNotNull(name).toLowerCase();
auth.realName = firstNonNull(realName, "Player");
auth.password = firstNonNull(password, new HashedPassword(""));
auth.totpKey = totpKey;
auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email;
auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value
auth.groupId = groupId;
@ -258,6 +269,11 @@ public class PlayerAuth {
return password(new HashedPassword(hash, salt));
}
public Builder totpKey(String totpKey) {
this.totpKey = totpKey;
return this;
}
public Builder lastIp(String lastIp) {
this.lastIp = lastIp;
return this;

View File

@ -32,7 +32,7 @@ public enum AllowFlightRestoreType {
}
},
/** Always set flight enabled to false. */
/** The user's flight handling is not modified. */
NOTHING {
@Override
public void restoreAllowFlight(Player player, LimboPlayer limbo) {

View File

@ -0,0 +1,11 @@
package fr.xephi.authme.data.limbo;
public enum LimboMessageType {
REGISTER,
LOG_IN,
TOTP_CODE
}

View File

@ -23,6 +23,7 @@ public class LimboPlayer {
private final float flySpeed;
private BukkitTask timeoutTask = null;
private MessageTask messageTask = null;
private LimboPlayerState state = LimboPlayerState.PASSWORD_REQUIRED;
public LimboPlayer(Location loc, boolean operator, Collection<String> groups, boolean fly, float walkSpeed,
float flySpeed) {
@ -124,4 +125,12 @@ public class LimboPlayer {
setMessageTask(null);
setTimeoutTask(null);
}
public LimboPlayerState getState() {
return state;
}
public void setState(LimboPlayerState state) {
this.state = state;
}
}

View File

@ -0,0 +1,9 @@
package fr.xephi.authme.data.limbo;
public enum LimboPlayerState {
PASSWORD_REQUIRED,
TOTP_REQUIRED
}

View File

@ -45,13 +45,13 @@ class LimboPlayerTaskManager {
*
* @param player the player
* @param limbo the associated limbo player of the player
* @param isRegistered whether the player is registered or not (needed to determine the message in the task)
* @param messageType message type
*/
void registerMessageTask(Player player, LimboPlayer limbo, boolean isRegistered) {
void registerMessageTask(Player player, LimboPlayer limbo, LimboMessageType messageType) {
int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL);
MessageResult messageResult = getMessageKey(player.getName(), isRegistered);
MessageResult result = getMessageKey(player.getName(), messageType);
if (interval > 0) {
String[] joinMessage = messages.retrieveSingle(messageResult.messageKey, messageResult.args).split("\n");
String[] joinMessage = messages.retrieveSingle(player, result.messageKey, result.args).split("\n");
MessageTask messageTask = new MessageTask(player, joinMessage);
bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, interval * TICKS_PER_SECOND);
limbo.setMessageTask(messageTask);
@ -67,7 +67,7 @@ class LimboPlayerTaskManager {
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);
String message = messages.retrieveSingle(player, MessageKey.LOGIN_TIMEOUT_ERROR);
BukkitTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout);
limbo.setTimeoutTask(task);
}
@ -89,12 +89,14 @@ class LimboPlayerTaskManager {
* Returns the appropriate message key according to the registration status and settings.
*
* @param name the player's name
* @param isRegistered whether or not the username is registered
* @param messageType the message to show
* @return the message key to display to the user
*/
private MessageResult getMessageKey(String name, boolean isRegistered) {
if (isRegistered) {
private MessageResult getMessageKey(String name, LimboMessageType messageType) {
if (messageType == LimboMessageType.LOG_IN) {
return new MessageResult(MessageKey.LOGIN_MESSAGE);
} else if (messageType == LimboMessageType.TOTP_CODE) {
return new MessageResult(MessageKey.TWO_FACTOR_CODE_REQUIRED);
} else if (registrationCaptchaManager.isCaptchaRequired(name)) {
final String captchaCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(name);
return new MessageResult(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, captchaCode);

View File

@ -69,7 +69,8 @@ public class LimboService {
LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered, location), limboPlayer);
taskManager.registerMessageTask(player, limboPlayer, isRegistered);
taskManager.registerMessageTask(player, limboPlayer,
isRegistered ? LimboMessageType.LOG_IN : LimboMessageType.REGISTER);
taskManager.registerTimeoutTask(player, limboPlayer);
helper.revokeLimboStates(player);
authGroupHandler.setGroup(player, limboPlayer,
@ -134,7 +135,7 @@ public class LimboService {
Optional<LimboPlayer> limboPlayer = getLimboOrLogError(player, "reset tasks");
limboPlayer.ifPresent(limbo -> {
taskManager.registerTimeoutTask(player, limbo);
taskManager.registerMessageTask(player, limbo, true);
taskManager.registerMessageTask(player, limbo, LimboMessageType.LOG_IN);
});
authGroupHandler.setGroup(player, limboPlayer.orElse(null), AuthGroupType.REGISTERED_UNAUTHENTICATED);
}
@ -143,11 +144,11 @@ public class LimboService {
* 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
* @param messageType the message to show for the limbo player
*/
public void resetMessageTask(Player player, boolean isRegistered) {
public void resetMessageTask(Player player, LimboMessageType messageType) {
getLimboOrLogError(player, "reset message task")
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, isRegistered));
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, messageType));
}
/**

View File

@ -3,7 +3,6 @@ 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.LimboSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import org.bukkit.Location;
@ -20,9 +19,6 @@ import static fr.xephi.authme.util.Utils.isCollectionEmpty;
*/
class LimboServiceHelper {
@Inject
private SpawnLoader spawnLoader;
@Inject
private PermissionsManager permissionsManager;

View File

@ -0,0 +1,169 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValueImpl;
import ch.jalu.datasourcecolumns.data.DataSourceValues;
import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumns;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.security.crypts.HashedPassword;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import static ch.jalu.datasourcecolumns.data.UpdateValues.with;
import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq;
import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* Common type for SQL-based data sources. Classes implementing this
* must ensure that {@link #columnsHandler} is initialized on creation.
*/
public abstract class AbstractSqlDataSource implements DataSource {
protected AuthMeColumnsHandler columnsHandler;
@Override
public boolean isAuthAvailable(String user) {
try {
return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists();
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public HashedPassword getPassword(String user) {
try {
DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT);
if (values.rowExists()) {
return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT));
}
} catch (SQLException e) {
logSqlException(e);
}
return null;
}
@Override
public boolean saveAuth(PlayerAuth auth) {
return columnsHandler.insert(auth,
AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT,
AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP);
}
@Override
public boolean hasSession(String user) {
try {
DataSourceValue<Integer> result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION);
return result.rowExists() && Integer.valueOf(1).equals(result.getValue());
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public boolean updateSession(PlayerAuth auth) {
return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME);
}
@Override
public boolean updatePassword(PlayerAuth auth) {
return updatePassword(auth.getNickname(), auth.getPassword());
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
return columnsHandler.update(user,
with(AuthMeColumns.PASSWORD, password.getHash())
.and(AuthMeColumns.SALT, password.getSalt()).build());
}
@Override
public boolean updateQuitLoc(PlayerAuth auth) {
return columnsHandler.update(auth,
AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z,
AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH);
}
@Override
public List<String> getAllAuthsByIp(String ip) {
try {
return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME);
} catch (SQLException e) {
logSqlException(e);
return Collections.emptyList();
}
}
@Override
public int countAuthsByEmail(String email) {
return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email));
}
@Override
public boolean updateEmail(PlayerAuth auth) {
return columnsHandler.update(auth, AuthMeColumns.EMAIL);
}
@Override
public boolean isLogged(String user) {
try {
DataSourceValue<Integer> result = columnsHandler.retrieve(user, AuthMeColumns.IS_LOGGED);
return result.rowExists() && Integer.valueOf(1).equals(result.getValue());
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public void setLogged(String user) {
columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 1);
}
@Override
public void setUnlogged(String user) {
columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 0);
}
@Override
public void grantSession(String user) {
columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1);
}
@Override
public void revokeSession(String user) {
columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0);
}
@Override
public void purgeLogged() {
columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0);
}
@Override
public int getAccountsRegistered() {
return columnsHandler.count(new AlwaysTruePredicate<>());
}
@Override
public boolean updateRealName(String user, String realName) {
return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName);
}
@Override
public DataSourceValue<String> getEmail(String user) {
try {
return columnsHandler.retrieve(user, AuthMeColumns.EMAIL);
} catch (SQLException e) {
logSqlException(e);
return DataSourceValueImpl.unknownRow();
}
}
}

View File

@ -1,5 +1,7 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValueImpl;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@ -244,10 +246,10 @@ public class CacheDataSource implements DataSource {
}
@Override
public DataSourceResult<String> getEmail(String user) {
public DataSourceValue<String> getEmail(String user) {
return cachedAuths.getUnchecked(user)
.map(auth -> DataSourceResult.of(auth.getEmail()))
.orElse(DataSourceResult.unknownPlayer());
.map(auth -> DataSourceValueImpl.of(auth.getEmail()))
.orElse(DataSourceValueImpl.unknownRow());
}
@Override
@ -268,6 +270,15 @@ public class CacheDataSource implements DataSource {
return source.getRecentlyLoggedInPlayers();
}
@Override
public boolean setTotpKey(String user, String totpKey) {
boolean result = source.setTotpKey(user, totpKey);
if (result) {
cachedAuths.refresh(user);
}
return result;
}
@Override
public void invalidateCache(String playerName) {
cachedAuths.invalidate(playerName);

View File

@ -14,6 +14,7 @@ public final class Columns {
public final String REAL_NAME;
public final String PASSWORD;
public final String SALT;
public final String TOTP_KEY;
public final String LAST_IP;
public final String LAST_LOGIN;
public final String GROUP;
@ -35,6 +36,7 @@ public final class Columns {
REAL_NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_REALNAME);
PASSWORD = settings.getProperty(DatabaseSettings.MYSQL_COL_PASSWORD);
SALT = settings.getProperty(DatabaseSettings.MYSQL_COL_SALT);
TOTP_KEY = settings.getProperty(DatabaseSettings.MYSQL_COL_TOTP_KEY);
LAST_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_LAST_IP);
LAST_LOGIN = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOGIN);
GROUP = settings.getProperty(DatabaseSettings.MYSQL_COL_GROUP);

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.security.crypts.HashedPassword;
@ -216,7 +217,7 @@ public interface DataSource extends Reloadable {
* @param user the user to retrieve an email for
* @return the email saved for the user, or null if user or email is not present
*/
DataSourceResult<String> getEmail(String user);
DataSourceValue<String> getEmail(String user);
/**
* Return all players of the database.
@ -232,6 +233,25 @@ public interface DataSource extends Reloadable {
*/
List<PlayerAuth> getRecentlyLoggedInPlayers();
/**
* Sets the given TOTP key to the player's account.
*
* @param user the name of the player to modify
* @param totpKey the totp key to set
* @return True upon success, false upon failure
*/
boolean setTotpKey(String user, String totpKey);
/**
* Removes the TOTP key if present of the given player's account.
*
* @param user the name of the player to modify
* @return True upon success, false upon failure
*/
default boolean removeTotpKey(String user) {
return setTotpKey(user, null);
}
/**
* Reload the data source.
*/

View File

@ -1,53 +0,0 @@
package fr.xephi.authme.datasource;
/**
* Wraps a value and allows to specify whether a value is missing or the player is not registered.
*/
public final class DataSourceResult<T> {
/** Instance used when a player does not exist. */
private static final DataSourceResult UNKNOWN_PLAYER = new DataSourceResult<>(null);
private final T value;
private DataSourceResult(T value) {
this.value = value;
}
/**
* Returns a {@link DataSourceResult} for the given value.
*
* @param value the value to wrap
* @param <T> the value's type
* @return DataSourceResult object for the given value
*/
public static <T> DataSourceResult<T> of(T value) {
return new DataSourceResult<>(value);
}
/**
* Returns a {@link DataSourceResult} specifying that the player does not exist.
*
* @param <T> the value type
* @return data source result for unknown player
*/
public static <T> DataSourceResult<T> unknownPlayer() {
return UNKNOWN_PLAYER;
}
/**
* @return whether the player of the associated value exists
*/
public boolean playerExists() {
return this != UNKNOWN_PLAYER;
}
/**
* Returns the value. It is {@code null} if the player is unknown. It is also {@code null}
* if the player exists but does not have the value defined.
*
* @return the value, or null
*/
public T getValue() {
return value;
}
}

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.security.crypts.HashedPassword;
@ -366,7 +367,7 @@ public class FlatFile implements DataSource {
}
@Override
public DataSourceResult<String> getEmail(String user) {
public DataSourceValue<String> getEmail(String user) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@ -398,6 +399,11 @@ public class FlatFile implements DataSource {
throw new UnsupportedOperationException("Flat file no longer supported");
}
@Override
public boolean setTotpKey(String user, String totpKey) {
throw new UnsupportedOperationException("Flat file no longer supported");
}
/**
* Creates a PlayerAuth object from the read data.
*

View File

@ -5,14 +5,12 @@ import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.Utils;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@ -29,7 +27,11 @@ import java.util.Set;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
public class MySQL implements DataSource {
/**
* MySQL data source.
*/
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class MySQL extends AbstractSqlDataSource {
private boolean useSsl;
private String host;
@ -96,6 +98,7 @@ public class MySQL implements DataSource {
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS);
this.col = new Columns(settings);
this.columnsHandler = AuthMeColumnsHandler.createForMySql(this::getConnection, settings);
this.sqlExtension = extensionsFactory.buildExtension(col);
this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE);
this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME);
@ -249,6 +252,11 @@ public class MySQL implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED);
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);");
}
}
ConsoleLogger.info("MySQL setup finished");
}
@ -259,40 +267,6 @@ public class MySQL implements DataSource {
}
}
@Override
public boolean isAuthAvailable(String user) {
String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
return rs.next();
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public HashedPassword getPassword(String user) {
boolean useSalt = !col.SALT.isEmpty();
String sql = "SELECT " + col.PASSWORD
+ (useSalt ? ", " + col.SALT : "")
+ " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return new HashedPassword(rs.getString(col.PASSWORD),
useSalt ? rs.getString(col.SALT) : null);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return null;
}
@Override
public PlayerAuth getAuth(String user) {
String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;";
@ -315,36 +289,13 @@ public class MySQL implements DataSource {
@Override
public boolean saveAuth(PlayerAuth auth) {
super.saveAuth(auth);
try (Connection con = getConnection()) {
// TODO ljacqu 20171104: Replace with generic columns util to clean this up
boolean useSalt = !col.SALT.isEmpty() || !StringUtils.isEmpty(auth.getPassword().getSalt());
boolean hasEmail = auth.getEmail() != null;
String emailPlaceholder = hasEmail ? "?" : "DEFAULT";
String sql = "INSERT INTO " + tableName + "("
+ col.NAME + "," + col.PASSWORD + "," + col.REAL_NAME
+ "," + col.EMAIL + "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP
+ (useSalt ? "," + col.SALT : "")
+ ") VALUES (?,?,?," + emailPlaceholder + ",?,?" + (useSalt ? ",?" : "") + ");";
try (PreparedStatement pst = con.prepareStatement(sql)) {
int index = 1;
pst.setString(index++, auth.getNickname());
pst.setString(index++, auth.getPassword().getHash());
pst.setString(index++, auth.getRealName());
if (hasEmail) {
pst.setString(index++, auth.getEmail());
}
pst.setObject(index++, auth.getRegistrationDate());
pst.setString(index++, auth.getRegistrationIp());
if (useSalt) {
pst.setString(index++, auth.getPassword().getSalt());
}
pst.executeUpdate();
}
if (!columnOthers.isEmpty()) {
for (String column : columnOthers) {
try (PreparedStatement pst = con.prepareStatement("UPDATE " + tableName + " SET " + column + "=? WHERE " + col.NAME + "=?;")) {
try (PreparedStatement pst = con.prepareStatement(
"UPDATE " + tableName + " SET " + column + "=? WHERE " + col.NAME + "=?;")) {
pst.setString(1, auth.getRealName());
pst.setString(2, auth.getNickname());
pst.executeUpdate();
@ -360,59 +311,6 @@ public class MySQL implements DataSource {
return false;
}
@Override
public boolean updatePassword(PlayerAuth auth) {
return updatePassword(auth.getNickname(), auth.getPassword());
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
user = user.toLowerCase();
try (Connection con = getConnection()) {
boolean useSalt = !col.SALT.isEmpty();
if (useSalt) {
String sql = String.format("UPDATE %s SET %s = ?, %s = ? WHERE %s = ?;",
tableName, col.PASSWORD, col.SALT, col.NAME);
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, password.getHash());
pst.setString(2, password.getSalt());
pst.setString(3, user);
pst.executeUpdate();
}
} else {
String sql = String.format("UPDATE %s SET %s = ? WHERE %s = ?;",
tableName, col.PASSWORD, col.NAME);
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, password.getHash());
pst.setString(2, user);
pst.executeUpdate();
}
}
sqlExtension.changePassword(user, password, con);
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateSession(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET "
+ col.LAST_IP + "=?, " + col.LAST_LOGIN + "=?, " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, auth.getLastIp());
pst.setObject(2, auth.getLastLogin());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public Set<String> getRecordsToPurge(long until) {
Set<String> list = new HashSet<>();
@ -450,42 +348,6 @@ public class MySQL implements DataSource {
return false;
}
@Override
public boolean updateQuitLoc(PlayerAuth auth) {
String sql = "UPDATE " + tableName
+ " SET " + col.LASTLOC_X + " =?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, "
+ col.LASTLOC_WORLD + "=?, " + col.LASTLOC_YAW + "=?, " + col.LASTLOC_PITCH + "=?"
+ " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setDouble(1, auth.getQuitLocX());
pst.setDouble(2, auth.getQuitLocY());
pst.setDouble(3, auth.getQuitLocZ());
pst.setString(4, auth.getWorld());
pst.setFloat(5, auth.getYaw());
pst.setFloat(6, auth.getPitch());
pst.setString(7, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateEmail(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET " + col.EMAIL + " =? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, auth.getEmail());
pst.setString(2, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void closeConnection() {
if (ds != null && !ds.isClosed()) {
@ -493,39 +355,6 @@ public class MySQL implements DataSource {
}
}
@Override
public List<String> getAllAuthsByIp(String ip) {
List<String> result = new ArrayList<>();
String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.LAST_IP + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, ip);
try (ResultSet rs = pst.executeQuery()) {
while (rs.next()) {
result.add(rs.getString(col.NAME));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return result;
}
@Override
public int countAuthsByEmail(String email) {
String sql = "SELECT COUNT(1) FROM " + tableName + " WHERE UPPER(" + col.EMAIL + ") = UPPER(?)";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, email);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return 0;
}
@Override
public void purgeRecords(Collection<String> toPurge) {
String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
@ -544,140 +373,6 @@ public class MySQL implements DataSource {
return DataSourceType.MYSQL;
}
@Override
public boolean isLogged(String user) {
String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
return rs.next() && (rs.getInt(col.IS_LOGGED) == 1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void setLogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void setUnlogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public boolean hasSession(String user) {
String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
return rs.next() && (rs.getInt(col.HAS_SESSION) == 1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void grantSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void revokeSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void purgeLogged() {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setInt(2, 1);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public int getAccountsRegistered() {
int result = 0;
String sql = "SELECT COUNT(*) FROM " + tableName;
try (Connection con = getConnection();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
if (rs.next()) {
result = rs.getInt(1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return result;
}
@Override
public boolean updateRealName(String user, String realName) {
String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, realName);
pst.setString(2, user);
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public DataSourceResult<String> getEmail(String user) {
String sql = "SELECT " + col.EMAIL + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return DataSourceResult.of(rs.getString(1));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return DataSourceResult.unknownPlayer();
}
@Override
public List<PlayerAuth> getAllAuths() {
List<PlayerAuth> auths = new ArrayList<>();
@ -728,6 +423,27 @@ public class MySQL implements DataSource {
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
/**
* Creates a {@link PlayerAuth} object with the data from the provided result set.
*
* @param row the result set to read from
* @return generated player auth object with the data from the result set
* @throws SQLException .
*/
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);
@ -735,6 +451,7 @@ public class MySQL implements DataSource {
.name(row.getString(col.NAME))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.email(row.getString(col.EMAIL))

View File

@ -3,10 +3,9 @@ package fr.xephi.authme.datasource;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.util.StringUtils;
import java.io.File;
import java.sql.Connection;
@ -28,7 +27,8 @@ import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* SQLite data source.
*/
public class SQLite implements DataSource {
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class SQLite extends AbstractSqlDataSource {
private final Settings settings;
private final File dataFolder;
@ -41,6 +41,7 @@ public class SQLite implements DataSource {
* Constructor for SQLite.
*
* @param settings The settings instance
* @param dataFolder The data folder
*
* @throws SQLException when initialization of a SQL datasource failed
*/
@ -69,6 +70,7 @@ public class SQLite implements DataSource {
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.col = new Columns(settings);
this.con = connection;
this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings);
}
/**
@ -83,6 +85,7 @@ public class SQLite implements DataSource {
ConsoleLogger.debug("SQLite driver loaded");
this.con = DriverManager.getConnection("jdbc:sqlite:plugins/AuthMe/" + database + ".db");
this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings);
}
/**
@ -170,6 +173,11 @@ public class SQLite implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';");
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);");
}
}
ConsoleLogger.info("SQLite Setup finished");
}
@ -202,40 +210,6 @@ public class SQLite implements DataSource {
}
}
@Override
public boolean isAuthAvailable(String user) {
String sql = "SELECT 1 FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
return rs.next();
}
} catch (SQLException ex) {
ConsoleLogger.warning(ex.getMessage());
return false;
}
}
@Override
public HashedPassword getPassword(String user) {
boolean useSalt = !col.SALT.isEmpty();
String sql = "SELECT " + col.PASSWORD
+ (useSalt ? ", " + col.SALT : "")
+ " FROM " + tableName + " WHERE " + col.NAME + "=?";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return new HashedPassword(rs.getString(col.PASSWORD),
useSalt ? rs.getString(col.SALT) : null);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return null;
}
@Override
public PlayerAuth getAuth(String user) {
String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);";
@ -252,95 +226,6 @@ public class SQLite implements DataSource {
return null;
}
@Override
public boolean saveAuth(PlayerAuth auth) {
PreparedStatement pst = null;
try {
HashedPassword password = auth.getPassword();
if (col.SALT.isEmpty()) {
if (!StringUtils.isEmpty(auth.getPassword().getSalt())) {
ConsoleLogger.warning("Warning! Detected hashed password with separate salt but the salt column "
+ "is not set in the config!");
}
pst = con.prepareStatement("INSERT INTO " + tableName + "(" + col.NAME + "," + col.PASSWORD
+ "," + col.REAL_NAME + "," + col.EMAIL
+ "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP
+ ") VALUES (?,?,?,?,?,?);");
pst.setString(1, auth.getNickname());
pst.setString(2, password.getHash());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getEmail());
pst.setLong(5, auth.getRegistrationDate());
pst.setString(6, auth.getRegistrationIp());
pst.executeUpdate();
} else {
pst = con.prepareStatement("INSERT INTO " + tableName + "(" + col.NAME + "," + col.PASSWORD
+ "," + col.REAL_NAME + "," + col.EMAIL
+ "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP + "," + col.SALT
+ ") VALUES (?,?,?,?,?,?,?);");
pst.setString(1, auth.getNickname());
pst.setString(2, password.getHash());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getEmail());
pst.setLong(5, auth.getRegistrationDate());
pst.setString(6, auth.getRegistrationIp());
pst.setString(7, password.getSalt());
pst.executeUpdate();
}
} catch (SQLException ex) {
logSqlException(ex);
} finally {
close(pst);
}
return true;
}
@Override
public boolean updatePassword(PlayerAuth auth) {
return updatePassword(auth.getNickname(), auth.getPassword());
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
user = user.toLowerCase();
boolean useSalt = !col.SALT.isEmpty();
String sql = "UPDATE " + tableName + " SET " + col.PASSWORD + " = ?"
+ (useSalt ? ", " + col.SALT + " = ?" : "")
+ " WHERE " + col.NAME + " = ?";
try (PreparedStatement pst = con.prepareStatement(sql)){
pst.setString(1, password.getHash());
if (useSalt) {
pst.setString(2, password.getSalt());
pst.setString(3, user);
} else {
pst.setString(2, user);
}
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateSession(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET " + col.LAST_IP + "=?, " + col.LAST_LOGIN + "=?, "
+ col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)){
pst.setString(1, auth.getLastIp());
pst.setObject(2, auth.getLastLogin());
pst.setString(3, auth.getRealName());
pst.setString(4, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public Set<String> getRecordsToPurge(long until) {
Set<String> list = new HashSet<>();
@ -388,42 +273,6 @@ public class SQLite implements DataSource {
return false;
}
@Override
public boolean updateQuitLoc(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET "
+ col.LASTLOC_X + "=?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, "
+ col.LASTLOC_WORLD + "=?, " + col.LASTLOC_YAW + "=?, " + col.LASTLOC_PITCH + "=? "
+ "WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setDouble(1, auth.getQuitLocX());
pst.setDouble(2, auth.getQuitLocY());
pst.setDouble(3, auth.getQuitLocZ());
pst.setString(4, auth.getWorld());
pst.setFloat(5, auth.getYaw());
pst.setFloat(6, auth.getPitch());
pst.setString(7, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public boolean updateEmail(PlayerAuth auth) {
String sql = "UPDATE " + tableName + " SET " + col.EMAIL + "=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, auth.getEmail());
pst.setString(2, auth.getNickname());
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void closeConnection() {
try {
@ -435,180 +284,11 @@ public class SQLite implements DataSource {
}
}
@Override
public List<String> getAllAuthsByIp(String ip) {
List<String> countIp = new ArrayList<>();
String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.LAST_IP + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, ip);
try (ResultSet rs = pst.executeQuery()) {
while (rs.next()) {
countIp.add(rs.getString(col.NAME));
}
return countIp;
}
} catch (SQLException ex) {
logSqlException(ex);
}
return new ArrayList<>();
}
@Override
public int countAuthsByEmail(String email) {
String sql = "SELECT COUNT(1) FROM " + tableName + " WHERE " + col.EMAIL + " = ? COLLATE NOCASE;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, email);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return 0;
}
@Override
public DataSourceType getType() {
return DataSourceType.SQLITE;
}
@Override
public boolean isLogged(String user) {
String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(col.IS_LOGGED) == 1;
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void setLogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void setUnlogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public boolean hasSession(String user) {
String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase());
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(col.HAS_SESSION) == 1;
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void grantSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 1);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void revokeSession(String user) {
String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public void purgeLogged() {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, 0);
pst.setInt(2, 1);
pst.executeUpdate();
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public int getAccountsRegistered() {
String sql = "SELECT COUNT(*) FROM " + tableName + ";";
try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return 0;
}
@Override
public boolean updateRealName(String user, String realName) {
String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, realName);
pst.setString(2, user);
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public DataSourceResult<String> getEmail(String user) {
String sql = "SELECT " + col.EMAIL + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return DataSourceResult.of(rs.getString(1));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return DataSourceResult.unknownPlayer();
}
@Override
public List<PlayerAuth> getAllAuths() {
List<PlayerAuth> auths = new ArrayList<>();
@ -653,6 +333,21 @@ public class SQLite implements DataSource {
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase());
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
@ -661,6 +356,7 @@ public class SQLite implements DataSource {
.email(row.getString(col.EMAIL))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.registrationDate(row.getLong(col.REGISTRATION_DATE))
@ -693,16 +389,6 @@ public class SQLite implements DataSource {
+ currentTimestamp + ", to all " + updatedRows + " rows");
}
private static void close(Statement st) {
if (st != null) {
try {
st.close();
} catch (SQLException ex) {
logSqlException(ex);
}
}
}
private static void close(Connection con) {
if (con != null) {
try {

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.datasource;
import com.google.common.io.Files;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
@ -8,14 +9,10 @@ import fr.xephi.authme.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Migrates the SQLite database when necessary.
@ -70,11 +67,10 @@ class SqLiteMigrater {
File backupDirectory = new File(dataFolder, "backups");
FileUtils.createDirectory(backupDirectory);
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String backupName = "backup-" + databaseName + dateFormat.format(new Date()) + ".db";
String backupName = "backup-" + databaseName + FileUtils.createCurrentTimeString() + ".db";
File backup = new File(backupDirectory, backupName);
try {
Files.copy(sqLite.toPath(), backup.toPath());
Files.copy(sqLite, backup);
return backupName;
} catch (IOException e) {
throw new IllegalStateException("Failed to create SQLite backup before migration", e);

View File

@ -0,0 +1,82 @@
package fr.xephi.authme.datasource.columnshandler;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.DEFAULT_FOR_NULL;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.OPTIONAL;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createDouble;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createFloat;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createInteger;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createLong;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createString;
/**
* Contains column definitions for the AuthMe table.
*/
public final class AuthMeColumns {
public static final PlayerAuthColumn<String> NAME = createString(
DatabaseSettings.MYSQL_COL_NAME, PlayerAuth::getNickname);
public static final PlayerAuthColumn<String> NICK_NAME = createString(
DatabaseSettings.MYSQL_COL_REALNAME, PlayerAuth::getRealName);
public static final PlayerAuthColumn<String> PASSWORD = createString(
DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash());
public static final PlayerAuthColumn<String> SALT = createString(
DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), OPTIONAL);
public static final PlayerAuthColumn<String> EMAIL = createString(
DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail, DEFAULT_FOR_NULL);
public static final PlayerAuthColumn<String> LAST_IP = createString(
DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp);
public static final PlayerAuthColumn<Integer> GROUP_ID = createInteger(
DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL);
public static final PlayerAuthColumn<Long> LAST_LOGIN = createLong(
DatabaseSettings.MYSQL_COL_LASTLOGIN, PlayerAuth::getLastLogin);
public static final PlayerAuthColumn<String> REGISTRATION_IP = createString(
DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp);
public static final PlayerAuthColumn<Long> REGISTRATION_DATE = createLong(
DatabaseSettings.MYSQL_COL_REGISTER_DATE, PlayerAuth::getRegistrationDate);
// --------
// Location columns
// --------
public static final PlayerAuthColumn<Double> LOCATION_X = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX);
public static final PlayerAuthColumn<Double> LOCATION_Y = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_Y, PlayerAuth::getQuitLocY);
public static final PlayerAuthColumn<Double> LOCATION_Z = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_Z, PlayerAuth::getQuitLocZ);
public static final PlayerAuthColumn<String> LOCATION_WORLD = createString(
DatabaseSettings.MYSQL_COL_LASTLOC_WORLD, PlayerAuth::getWorld);
public static final PlayerAuthColumn<Float> LOCATION_YAW = createFloat(
DatabaseSettings.MYSQL_COL_LASTLOC_YAW, PlayerAuth::getYaw);
public static final PlayerAuthColumn<Float> LOCATION_PITCH = createFloat(
DatabaseSettings.MYSQL_COL_LASTLOC_PITCH, PlayerAuth::getPitch);
// --------
// Columns not on PlayerAuth
// --------
public static final DataSourceColumn<Integer> IS_LOGGED = createInteger(
DatabaseSettings.MYSQL_COL_ISLOGGED);
public static final DataSourceColumn<Integer> HAS_SESSION = createInteger(
DatabaseSettings.MYSQL_COL_HASSESSION);
private AuthMeColumns() {
}
}

View File

@ -0,0 +1,83 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.ColumnType;
import ch.jalu.datasourcecolumns.StandardTypes;
import fr.xephi.authme.data.auth.PlayerAuth;
import java.util.function.Function;
/**
* Util class for initializing {@link DataSourceColumn} objects.
*/
final class AuthMeColumnsFactory {
private AuthMeColumnsFactory() {
}
static DataSourceColumn<Integer> createInteger(Property<String> nameProperty,
ColumnOptions... options) {
return new DataSourceColumn<>(StandardTypes.INTEGER, nameProperty,
isOptional(options), hasDefaultForNull(options));
}
static PlayerAuthColumn<Integer> createInteger(Property<String> nameProperty,
Function<PlayerAuth, Integer> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.INTEGER, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Long> createLong(Property<String> nameProperty,
Function<PlayerAuth, Long> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.LONG, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<String> createString(Property<String> nameProperty,
Function<PlayerAuth, String> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.STRING, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Double> createDouble(Property<String> nameProperty,
Function<PlayerAuth, Double> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.DOUBLE, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Float> createFloat(Property<String> nameProperty,
Function<PlayerAuth, Float> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.FLOAT, nameProperty, playerAuthGetter, options);
}
private static <T> PlayerAuthColumn<T> createInternal(ColumnType<T> type, Property<String> nameProperty,
Function<PlayerAuth, T> authGetter,
ColumnOptions... options) {
return new PlayerAuthColumn<>(type, nameProperty, isOptional(options), hasDefaultForNull(options), authGetter);
}
private static boolean isOptional(ColumnOptions[] options) {
return containsInArray(ColumnOptions.OPTIONAL, options);
}
private static boolean hasDefaultForNull(ColumnOptions[] options) {
return containsInArray(ColumnOptions.DEFAULT_FOR_NULL, options);
}
private static boolean containsInArray(ColumnOptions needle, ColumnOptions[] haystack) {
for (ColumnOptions option : haystack) {
if (option == needle) {
return true;
}
}
return false;
}
enum ColumnOptions {
OPTIONAL,
DEFAULT_FOR_NULL
}
}

View File

@ -0,0 +1,206 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValues;
import ch.jalu.datasourcecolumns.data.UpdateValues;
import ch.jalu.datasourcecolumns.predicate.Predicate;
import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator;
import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler;
import ch.jalu.datasourcecolumns.sqlimplementation.statementgenerator.ConnectionSupplier;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forConnectionPool;
import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forSingleConnection;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* Wrapper of {@link SqlColumnsHandler} for the AuthMe data table.
* Wraps exceptions and provides better support for operations based on a {@link PlayerAuth} object.
*/
public final class AuthMeColumnsHandler {
private final SqlColumnsHandler<ColumnContext, String> internalHandler;
private AuthMeColumnsHandler(SqlColumnsHandler<ColumnContext, String> internalHandler) {
this.internalHandler = internalHandler;
}
/**
* Creates a column handler for SQLite.
*
* @param connection the connection to the database
* @param settings plugin settings
* @return created column handler
*/
public static AuthMeColumnsHandler createForSqlite(Connection connection, Settings settings) {
ColumnContext columnContext = new ColumnContext(settings, false);
String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
SqlColumnsHandler<ColumnContext, String> sqlColHandler = new SqlColumnsHandler<>(
forSingleConnection(connection, tableName, nameColumn, columnContext)
.setPredicateSqlGenerator(new PredicateSqlGenerator<>(columnContext, true))
);
return new AuthMeColumnsHandler(sqlColHandler);
}
/**
* Creates a column handler for MySQL.
*
* @param connectionSupplier supplier of connections from the connection pool
* @param settings plugin settings
* @return created column handler
*/
public static AuthMeColumnsHandler createForMySql(ConnectionSupplier connectionSupplier, Settings settings) {
ColumnContext columnContext = new ColumnContext(settings, true);
String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
SqlColumnsHandler<ColumnContext, String> sqlColHandler = new SqlColumnsHandler<>(
forConnectionPool(connectionSupplier, tableName, nameColumn, columnContext));
return new AuthMeColumnsHandler(sqlColHandler);
}
/**
* Changes a column from a specific row to the given value.
*
* @param name name of the account to modify
* @param column the column to modify
* @param value the value to set the column to
* @param <T> the column type
* @return true upon success, false otherwise
*/
public <T> boolean update(String name, DataSourceColumn<T> column, T value) {
try {
return internalHandler.update(name, column, value);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Updates a row to have the values as retrieved from the PlayerAuth object.
*
* @param auth the player auth object to modify and to get values from
* @param columns the columns to update in the row
* @return true upon success, false otherwise
*/
public boolean update(PlayerAuth auth, PlayerAuthColumn<?>... columns) {
try {
return internalHandler.update(auth.getNickname(), auth, columns);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Updates a row to have the given values.
*
* @param name the name of the account to modify
* @param updateValues the values to set on the row
* @return true upon success, false otherwise
*/
public boolean update(String name, UpdateValues<ColumnContext> updateValues) {
try {
return internalHandler.update(name.toLowerCase(), updateValues);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Sets the given value to the provided column for all rows which match the predicate.
*
* @param predicate the predicate to filter rows by
* @param column the column to modify on the matched rows
* @param value the new value to set
* @param <T> the column type
* @return number of modified rows
*/
public <T> int update(Predicate<ColumnContext> predicate, DataSourceColumn<T> column, T value) {
try {
return internalHandler.update(predicate, column, value);
} catch (SQLException e) {
logSqlException(e);
return 0;
}
}
/**
* Retrieves the given column from a given row.
*
* @param name the account name to look up
* @param column the column whose value should be retrieved
* @param <T> the column type
* @return the result of the lookup
* @throws SQLException .
*/
public <T> DataSourceValue<T> retrieve(String name, DataSourceColumn<T> column) throws SQLException {
return internalHandler.retrieve(name.toLowerCase(), column);
}
/**
* Retrieves multiple values from a given row.
*
* @param name the account name to look up
* @param columns the columns to retrieve
* @return map-like object with the requested values
* @throws SQLException .
*/
public DataSourceValues retrieve(String name, DataSourceColumn<?>... columns) throws SQLException {
return internalHandler.retrieve(name.toLowerCase(), columns);
}
/**
* Retrieves a column's value for all rows that satisfy the given predicate.
*
* @param predicate the predicate to fulfill
* @param column the column to retrieve from the matching rows
* @param <T> the column's value type
* @return the values of the matching rows
* @throws SQLException .
*/
public <T> List<T> retrieve(Predicate<ColumnContext> predicate, DataSourceColumn<T> column) throws SQLException {
return internalHandler.retrieve(predicate, column);
}
/**
* Inserts the given values into a new row, as taken from the player auth.
*
* @param auth the player auth to get values from
* @param columns the columns to insert
* @return true upon success, false otherwise
*/
public boolean insert(PlayerAuth auth, PlayerAuthColumn<?>... columns) {
try {
return internalHandler.insert(auth, columns);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Returns the number of rows that match the provided predicate.
*
* @param predicate the predicate to test the rows for
* @return number of rows fulfilling the predicate
*/
public int count(Predicate<ColumnContext> predicate) {
try {
return internalHandler.count(predicate);
} catch (SQLException e) {
logSqlException(e);
return 0;
}
}
}

View File

@ -0,0 +1,35 @@
package fr.xephi.authme.datasource.columnshandler;
import fr.xephi.authme.settings.Settings;
import java.util.HashMap;
import java.util.Map;
/**
* Context for resolving the properties of {@link AuthMeColumns} entries.
*/
public class ColumnContext {
private final Settings settings;
private final Map<DataSourceColumn<?>, String> columnNames = new HashMap<>();
private final boolean hasDefaultSupport;
/**
* Constructor.
*
* @param settings plugin settings
* @param hasDefaultSupport whether or not the underlying database has support for the {@code DEFAULT} keyword
*/
public ColumnContext(Settings settings, boolean hasDefaultSupport) {
this.settings = settings;
this.hasDefaultSupport = hasDefaultSupport;
}
public String getName(DataSourceColumn<?> column) {
return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty()));
}
public boolean hasDefaultSupport() {
return hasDefaultSupport;
}
}

View File

@ -0,0 +1,57 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.Column;
import ch.jalu.datasourcecolumns.ColumnType;
/**
* Basic {@link Column} implementation for AuthMe.
*
* @param <T> column type
*/
public class DataSourceColumn<T> implements Column<T, ColumnContext> {
private final ColumnType<T> columnType;
private final Property<String> nameProperty;
private final boolean isOptional;
private final boolean useDefaultForNull;
/**
* Constructor.
*
* @param type type of the column
* @param nameProperty property defining the column name
* @param isOptional whether or not the column can be skipped (if name is configured to empty string)
* @param useDefaultForNull whether SQL DEFAULT should be used for null values (if supported by the database)
*/
DataSourceColumn(ColumnType<T> type, Property<String> nameProperty, boolean isOptional, boolean useDefaultForNull) {
this.columnType = type;
this.nameProperty = nameProperty;
this.isOptional = isOptional;
this.useDefaultForNull = useDefaultForNull;
}
public Property<String> getNameProperty() {
return nameProperty;
}
@Override
public String resolveName(ColumnContext columnContext) {
return columnContext.getName(this);
}
@Override
public ColumnType<T> getType() {
return columnType;
}
@Override
public boolean isColumnUsed(ColumnContext columnContext) {
return !isOptional || !resolveName(columnContext).isEmpty();
}
@Override
public boolean useDefaultForNullValue(ColumnContext columnContext) {
return useDefaultForNull && columnContext.hasDefaultSupport();
}
}

View File

@ -0,0 +1,32 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.ColumnType;
import ch.jalu.datasourcecolumns.DependentColumn;
import fr.xephi.authme.data.auth.PlayerAuth;
import java.util.function.Function;
/**
* Implementation for columns which can also be retrieved from a {@link PlayerAuth} object.
*
* @param <T> column type
*/
public class PlayerAuthColumn<T> extends DataSourceColumn<T> implements DependentColumn<T, ColumnContext, PlayerAuth> {
private final Function<PlayerAuth, T> playerAuthGetter;
/*
* Constructor. See parent class for details.
*/
PlayerAuthColumn(ColumnType<T> type, Property<String> nameProperty, boolean isOptional, boolean useDefaultForNull,
Function<PlayerAuth, T> playerAuthGetter) {
super(type, nameProperty, isOptional, useDefaultForNull);
this.playerAuthGetter = playerAuthGetter;
}
@Override
public T getValueFromDependent(PlayerAuth auth) {
return playerAuthGetter.apply(auth);
}
}

View File

@ -85,6 +85,7 @@ class XfBcryptExtension extends MySqlExtension {
}
}
@Override
public void extendAuth(PlayerAuth auth, int id, Connection con) throws SQLException {
try (PreparedStatement pst = con.prepareStatement(
"SELECT data FROM " + xfPrefix + "user_authenticate WHERE " + col.ID + "=?;")) {

View File

@ -0,0 +1,70 @@
package fr.xephi.authme.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* This event is called when a player uses the register command,
* it's fired even when a user does a /register with invalid arguments.
* {@link #setCanRegister(boolean) event.setCanRegister(false)} prevents the player from registering.
*/
public class AuthMeAsyncPreRegisterEvent extends CustomEvent {
private static final HandlerList handlers = new HandlerList();
private final Player player;
private boolean canRegister = true;
/**
* Constructor.
*
* @param player The player
* @param isAsync True if the event is async, false otherwise
*/
public AuthMeAsyncPreRegisterEvent(Player player, boolean isAsync) {
super(isAsync);
this.player = player;
}
/**
* Return the player concerned by this event.
*
* @return The player who executed a valid {@code /login} command
*/
public Player getPlayer() {
return player;
}
/**
* Return whether the player is allowed to register.
*
* @return True if the player can log in, false otherwise
*/
public boolean canRegister() {
return canRegister;
}
/**
* Define whether or not the player may register.
*
* @param canRegister True to allow the player to log in; false to prevent him
*/
public void setCanRegister(boolean canRegister) {
this.canRegister = canRegister;
}
/**
* Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}.
*
* @return The list of handlers
*/
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,87 @@
package fr.xephi.authme.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import javax.annotation.Nullable;
/**
* This event is called when a player adds or changes his email address.
*/
public class EmailChangedEvent extends CustomEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private final Player player;
private final String oldEmail;
private final String newEmail;
private boolean isCancelled;
/**
* Constructor
*
* @param player The player that changed email
* @param oldEmail Old email player had on file. Can be null when user adds an email
* @param newEmail New email that player tries to set. In case of adding email, this will contain
* the email is trying to set.
* @param isAsync should this event be called asynchronously?
*/
public EmailChangedEvent(Player player, @Nullable String oldEmail, String newEmail, boolean isAsync) {
super(isAsync);
this.player = player;
this.oldEmail = oldEmail;
this.newEmail = newEmail;
}
@Override
public boolean isCancelled() {
return isCancelled;
}
/**
* Gets the player who changes the email
*
* @return The player who changed the email
*/
public Player getPlayer() {
return player;
}
/**
* Gets the old email in case user tries to change existing email.
*
* @return old email stored on file. Can be null when user never had an email and adds a new one.
*/
public @Nullable String getOldEmail() {
return this.oldEmail;
}
/**
* Gets the new email.
*
* @return the email user is trying to set. If user adds email and never had one before,
* this is where such email can be found.
*/
public String getNewEmail() {
return this.newEmail;
}
@Override
public void setCancelled(boolean cancelled) {
this.isCancelled = cancelled;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
/**
* Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}.
*
* @return The list of handlers
*/
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,47 @@
package fr.xephi.authme.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Event fired when a player has successfully registered.
*/
public class RegisterEvent extends CustomEvent {
private static final HandlerList handlers = new HandlerList();
private final Player player;
/**
* Constructor.
*
* @param player The player
*/
public RegisterEvent(Player player) {
this.player = player;
}
/**
* Return the player that has successfully logged in or registered.
*
* @return The player
*/
public Player getPlayer() {
return player;
}
/**
* Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}.
*
* @return The list of handlers
*/
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -83,6 +83,10 @@ public class OnStartupTasks {
logger.addFilter(new Log4JFilter());
}
/**
* Starts a task that regularly reminds players without a defined email to set their email,
* if enabled.
*/
public void scheduleRecallEmailTask() {
if (!settings.getProperty(RECALL_PLAYERS)) {
return;

View File

@ -2,7 +2,7 @@ package fr.xephi.authme.initialization;
import ch.jalu.configme.configurationdata.ConfigurationData;
import ch.jalu.configme.resource.PropertyResource;
import ch.jalu.configme.resource.YamlFileResource;
import fr.xephi.authme.service.yaml.YamlFileResourceProvider;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SettingsMigrationService;
import fr.xephi.authme.settings.properties.AuthMeSettingsRetriever;
@ -37,7 +37,7 @@ public class SettingsProvider implements Provider<Settings> {
if (!configFile.exists()) {
FileUtils.create(configFile);
}
PropertyResource resource = new YamlFileResource(configFile);
PropertyResource resource = YamlFileResourceProvider.loadFromFile(configFile);
ConfigurationData configurationData = AuthMeSettingsRetriever.buildConfigurationData();
return new Settings(dataFolder, resource, migrationService, configurationData);
}

View File

@ -123,7 +123,7 @@ public class OnJoinVerifier implements Reloadable {
return false;
} else if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) {
// Server is full and player is NOT VIP; set kick message and proceed with kick
event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER));
event.setKickMessage(messages.retrieveSingle(player, MessageKey.KICK_FULL_SERVER));
return true;
}
@ -135,12 +135,12 @@ public class OnJoinVerifier implements Reloadable {
}
Player nonVipPlayer = generateKickPlayer(onlinePlayers);
if (nonVipPlayer != null) {
nonVipPlayer.kickPlayer(messages.retrieveSingle(MessageKey.KICK_FOR_VIP));
nonVipPlayer.kickPlayer(messages.retrieveSingle(player, MessageKey.KICK_FOR_VIP));
event.allow();
return false;
} else {
ConsoleLogger.info("VIP player " + player.getName() + " tried to join, but the server was full");
event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER));
event.setKickMessage(messages.retrieveSingle(player, MessageKey.KICK_FULL_SERVER));
return true;
}
}

View File

@ -1,10 +1,14 @@
package fr.xephi.authme.listener;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.QuickCommandsProtectionManager;
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.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.permission.handlers.PermissionLoadUserException;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.AntiBotService;
import fr.xephi.authme.service.BukkitService;
@ -50,6 +54,8 @@ import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerShearEntityEvent;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS;
import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT;
@ -62,7 +68,7 @@ public class PlayerListener implements Listener {
@Inject
private Settings settings;
@Inject
private Messages m;
private Messages messages;
@Inject
private DataSource dataSource;
@Inject
@ -86,9 +92,12 @@ public class PlayerListener implements Listener {
@Inject
private PermissionsManager permissionsManager;
@Inject
private QuickCommandsProtectionManager quickCommandsProtectionManager;
@Inject
private BungeeSender bungeeSender;
private boolean isAsyncPlayerPreLoginEventCalled = false;
private List<String> unresolvedPlayerHostname = new ArrayList<>();
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
@ -100,9 +109,14 @@ public class PlayerListener implements Listener {
return;
}
final Player player = event.getPlayer();
if (!quickCommandsProtectionManager.isAllowed(player.getName())) {
event.setCancelled(true);
player.kickPlayer(messages.retrieveSingle(player, MessageKey.QUICK_COMMAND_PROTECTION_KICK));
return;
}
if (listenerService.shouldCancelEvent(player)) {
event.setCancelled(true);
m.send(player, MessageKey.DENIED_COMMAND);
messages.send(player, MessageKey.DENIED_COMMAND);
}
}
@ -113,10 +127,18 @@ public class PlayerListener implements Listener {
}
final Player player = event.getPlayer();
if (listenerService.shouldCancelEvent(player)) {
final boolean mayPlayerSendChat = !listenerService.shouldCancelEvent(player)
|| permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN);
if (mayPlayerSendChat) {
removeUnauthorizedRecipients(event);
} else {
event.setCancelled(true);
m.send(player, MessageKey.DENIED_CHAT);
} else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) {
messages.send(player, MessageKey.DENIED_CHAT);
}
}
private void removeUnauthorizedRecipients(AsyncPlayerChatEvent event) {
if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) {
event.getRecipients().removeIf(listenerService::shouldCancelEvent);
if (event.getRecipients().isEmpty()) {
event.setCancelled(true);
@ -178,6 +200,7 @@ public class PlayerListener implements Listener {
String customJoinMessage = settings.getProperty(RegistrationSettings.CUSTOM_JOIN_MESSAGE);
if (!customJoinMessage.isEmpty()) {
customJoinMessage = ChatColor.translateAlternateColorCodes('&', customJoinMessage);
event.setJoinMessage(customJoinMessage
.replace("{PLAYERNAME}", player.getName())
.replace("{DISPLAYNAME}", player.getDisplayName())
@ -206,6 +229,9 @@ public class PlayerListener implements Listener {
if (!PlayerListener19Spigot.isPlayerSpawnLocationEventCalled()) {
teleportationService.teleportOnJoin(player);
}
quickCommandsProtectionManager.processJoin(player);
management.performJoin(player);
teleportationService.teleportNewPlayerToFirstSpawn(player);
@ -240,6 +266,11 @@ public class PlayerListener implements Listener {
return;
}
if (event.getAddress() == null) {
unresolvedPlayerHostname.add(event.getName());
return;
}
final String name = event.getName();
if (validationService.isUnrestricted(name)) {
@ -248,15 +279,19 @@ public class PlayerListener implements Listener {
// Keep pre-UUID compatibility
try {
permissionsManager.loadUserData(event.getUniqueId());
} catch (NoSuchMethodError e) {
permissionsManager.loadUserData(name);
try {
permissionsManager.loadUserData(event.getUniqueId());
} catch (NoSuchMethodError e) {
permissionsManager.loadUserData(name);
}
} catch (PermissionLoadUserException e) {
ConsoleLogger.logException("Unable to load the permission data of user " + name, e);
}
try {
runOnJoinChecks(JoiningPlayer.fromName(name), event.getAddress().getHostAddress());
} catch (FailedVerificationException e) {
event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs()));
event.setKickMessage(messages.retrieveSingle(name, e.getReason(), e.getArgs()));
event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER);
}
}
@ -280,11 +315,13 @@ public class PlayerListener implements Listener {
return;
}
if (!isAsyncPlayerPreLoginEventCalled || !settings.getProperty(PluginSettings.USE_ASYNC_PRE_LOGIN_EVENT)) {
if (!isAsyncPlayerPreLoginEventCalled || !settings.getProperty(PluginSettings.USE_ASYNC_PRE_LOGIN_EVENT)
|| unresolvedPlayerHostname.remove(name)) {
try {
runOnJoinChecks(JoiningPlayer.fromPlayerObject(player), event.getAddress().getHostAddress());
} catch (FailedVerificationException e) {
event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs()));
event.setKickMessage(messages.retrieveSingle(player, e.getReason(), e.getArgs()));
event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
}
}

View File

@ -12,10 +12,11 @@ import javax.inject.Inject;
public class PlayerListener19Spigot implements Listener {
private static boolean isPlayerSpawnLocationEventCalled = false;
@Inject
private TeleportationService teleportationService;
private static boolean isPlayerSpawnLocationEventCalled = false;
public static boolean isPlayerSpawnLocationEventCalled() {
return isPlayerSpawnLocationEventCalled;

View File

@ -5,9 +5,9 @@ import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
@ -46,7 +46,7 @@ public class ProtocolLibService implements SettingsDependent {
if (protectInvBeforeLogin) {
ConsoleLogger.warning("WARNING! The protectInventory feature requires ProtocolLib! Disabling it...");
}
if (denyTabCompleteBeforeLogin) {
ConsoleLogger.warning("WARNING! The denyTabComplete feature requires ProtocolLib! Disabling it...");
}
@ -56,16 +56,21 @@ public class ProtocolLibService implements SettingsDependent {
}
// Set up packet adapters
if (protectInvBeforeLogin && inventoryPacketAdapter == null) {
inventoryPacketAdapter = new InventoryPacketAdapter(plugin, playerCache);
inventoryPacketAdapter.register();
if (protectInvBeforeLogin) {
if (inventoryPacketAdapter == null) {
inventoryPacketAdapter = new InventoryPacketAdapter(plugin, playerCache);
inventoryPacketAdapter.register();
}
} else if (inventoryPacketAdapter != null) {
inventoryPacketAdapter.unregister();
inventoryPacketAdapter = null;
}
if (denyTabCompleteBeforeLogin && tabCompletePacketAdapter == null) {
tabCompletePacketAdapter = new TabCompletePacketAdapter(plugin, playerCache);
tabCompletePacketAdapter.register();
if (denyTabCompleteBeforeLogin) {
if (tabCompletePacketAdapter == null) {
tabCompletePacketAdapter = new TabCompletePacketAdapter(plugin, playerCache);
tabCompletePacketAdapter.register();
}
} else if (tabCompletePacketAdapter != null) {
tabCompletePacketAdapter.unregister();
tabCompletePacketAdapter = null;
@ -106,7 +111,7 @@ public class ProtocolLibService implements SettingsDependent {
this.denyTabCompleteBeforeLogin = settings.getProperty(RestrictionSettings.DENY_TABCOMPLETE_BEFORE_LOGIN);
//it was true and will be deactivated now, so we need to restore the inventory for every player
if (oldProtectInventory && !protectInvBeforeLogin) {
if (oldProtectInventory && !protectInvBeforeLogin && inventoryPacketAdapter != null) {
inventoryPacketAdapter.unregister();
for (Player onlinePlayer : bukkitService.getOnlinePlayers()) {
if (!playerCache.isAuthenticated(onlinePlayer.getName())) {

View File

@ -0,0 +1,124 @@
package fr.xephi.authme.message;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.util.FileUtils;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.File;
/**
* Handles a YAML message file with a default file fallback.
*/
public abstract class AbstractMessageFileHandler implements Reloadable {
protected static final String DEFAULT_LANGUAGE = "en";
@DataFolder
@Inject
private File dataFolder;
@Inject
private Settings settings;
private String filename;
private FileConfiguration configuration;
private final String defaultFile;
protected AbstractMessageFileHandler() {
this.defaultFile = createFilePath(DEFAULT_LANGUAGE);
}
@Override
@PostConstruct
public void reload() {
String language = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
filename = createFilePath(language);
File messagesFile = initializeFile(filename);
configuration = YamlConfiguration.loadConfiguration(messagesFile);
}
protected String getLanguage() {
return settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
}
protected File getUserLanguageFile() {
return new File(dataFolder, filename);
}
protected String getFilename() {
return filename;
}
/**
* Returns whether the message file configuration has an entry at the given path.
*
* @param path the path to verify
* @return true if an entry exists for the path in the messages file, false otherwise
*/
public boolean hasSection(String path) {
return configuration.get(path) != null;
}
/**
* Returns the message for the given key.
*
* @param key the key to retrieve the message for
* @return the message
*/
public String getMessage(String key) {
String message = configuration.getString(key);
return message == null
? "Error retrieving message '" + key + "'"
: message;
}
/**
* Returns the message for the given key only if it exists,
* i.e. without falling back to the default file.
*
* @param key the key to retrieve the message for
* @return the message, or {@code null} if not available
*/
public String getMessageIfExists(String key) {
return configuration.getString(key);
}
/**
* Creates the path to the messages file for the given language code.
*
* @param language the language code
* @return path to the message file for the given language
*/
protected abstract String createFilePath(String language);
/**
* Copies the messages file from the JAR to the local messages/ folder if it doesn't exist.
*
* @param filePath path to the messages file to use
* @return the messages file to use
*/
@VisibleForTesting
File initializeFile(String filePath) {
File file = new File(dataFolder, filePath);
// Check that JAR file exists to avoid logging an error
if (FileUtils.getResourceFromJar(filePath) != null && FileUtils.copyFileFromResource(file, filePath)) {
return file;
}
if (FileUtils.copyFileFromResource(file, defaultFile)) {
return file;
} else {
ConsoleLogger.warning("Wanted to copy default messages file '" + defaultFile
+ "' from JAR but it didn't exist");
return null;
}
}
}

View File

@ -0,0 +1,62 @@
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;
import javax.inject.Inject;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* File handler for the help_xx.yml resource.
*/
public class HelpMessagesFileHandler extends AbstractMessageFileHandler {
private FileConfiguration defaultConfiguration;
@Inject // Trigger injection in the superclass
HelpMessagesFileHandler() {
}
/**
* Returns the message for the given key.
*
* @param key the key to retrieve the message for
* @return the message
*/
@Override
public String getMessage(String key) {
String message = getMessageIfExists(key);
if (message == null) {
ConsoleLogger.warning("Error getting message with key '" + key + "'. "
+ "Please update your config file '" + getFilename() + "' or run /authme messages help");
return getDefault(key);
}
return message;
}
/**
* Gets the message from the default file.
*
* @param key the key to retrieve the message for
* @return the message from the default file
*/
private String getDefault(String key) {
if (defaultConfiguration == null) {
InputStream stream = FileUtils.getResourceFromJar(createFilePath(DEFAULT_LANGUAGE));
defaultConfiguration = YamlConfiguration.loadConfiguration(new InputStreamReader(stream));
}
String message = defaultConfiguration.getString(key);
return message == null
? "Error retrieving message '" + key + "'"
: message;
}
@Override
protected String createFilePath(String language) {
return "messages/help_" + language + ".yml";
}
}

View File

@ -1,95 +0,0 @@
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;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* Handles a YAML message file with a default file fallback.
*/
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;
/**
* Constructor.
*
* @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, String updateCommand) {
this.filename = file.getName();
this.configuration = YamlConfiguration.loadConfiguration(file);
this.defaultFile = defaultFile;
this.updateAddition = updateCommand == null
? ""
: " (or run " + updateCommand + ")";
}
/**
* Returns whether the message file configuration has an entry at the given path.
*
* @param path the path to verify
* @return true if an entry exists for the path in the messages file, false otherwise
*/
public boolean hasSection(String path) {
return configuration.get(path) != null;
}
/**
* Returns the message for the given key.
*
* @param key the key to retrieve the message for
* @return the message
*/
public String getMessage(String key) {
String message = configuration.getString(key);
if (message == null) {
ConsoleLogger.warning("Error getting message with key '" + key + "'. "
+ "Please update your config file '" + filename + "'" + updateAddition);
return getDefault(key);
}
return message;
}
/**
* Returns the message for the given key only if it exists,
* i.e. without falling back to the default file.
*
* @param key the key to retrieve the message for
* @return the message, or {@code null} if not available
*/
public String getMessageIfExists(String key) {
return configuration.getString(key);
}
/**
* Gets the message from the default file.
*
* @param key the key to retrieve the message for
* @return the message from the default file
*/
private String getDefault(String key) {
if (defaultConfiguration == null) {
InputStream stream = FileUtils.getResourceFromJar(defaultFile);
defaultConfiguration = YamlConfiguration.loadConfiguration(new InputStreamReader(stream));
}
String message = defaultConfiguration.getString(key);
return message == null
? "Error retrieving message '" + key + "'"
: message;
}
}

View File

@ -1,81 +0,0 @@
package fr.xephi.authme.message;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.util.FileUtils;
import javax.inject.Inject;
import java.io.File;
import java.util.function.Function;
/**
* Injectable creator of {@link MessageFileHandler} instances.
*
* @see MessageFileHandler
*/
public class MessageFileHandlerProvider {
private static final String DEFAULT_LANGUAGE = "en";
@Inject
@DataFolder
private File dataFolder;
@Inject
private Settings settings;
MessageFileHandlerProvider() {
}
/**
* 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
* @return the message file handler
*/
public MessageFileHandler initializeHandler(Function<String, String> pathBuilder) {
return initializeHandler(pathBuilder, null);
}
/**
* Initializes a message file handler with the messages file of the configured language.
* Ensures beforehand that the messages file exists or creates it otherwise.
*
* @param pathBuilder function taking the configured language code as argument and returning the messages file
* @param updateCommand command to run to update the languages file (nullable)
* @return the message file handler
*/
public MessageFileHandler initializeHandler(Function<String, String> pathBuilder, String updateCommand) {
String language = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
return new MessageFileHandler(
initializeFile(language, pathBuilder),
pathBuilder.apply(DEFAULT_LANGUAGE),
updateCommand);
}
/**
* Copies the messages file from the JAR if it doesn't exist.
*
* @param language the configured language code
* @param pathBuilder function returning message file name with language as argument
* @return the messages file to use
*/
@VisibleForTesting
File initializeFile(String language, Function<String, String> pathBuilder) {
String filePath = pathBuilder.apply(language);
File file = new File(dataFolder, filePath);
// Check that JAR file exists to avoid logging an error
if (FileUtils.getResourceFromJar(filePath) != null && FileUtils.copyFileFromResource(file, filePath)) {
return file;
}
String defaultFilePath = pathBuilder.apply(DEFAULT_LANGUAGE);
if (FileUtils.copyFileFromResource(file, defaultFilePath)) {
return file;
}
return null;
}
}

View File

@ -6,298 +6,328 @@ package fr.xephi.authme.message;
public enum MessageKey {
/** In order to use this command you must be authenticated! */
DENIED_COMMAND("denied_command"),
DENIED_COMMAND("error.denied_command"),
/** A player with the same IP is already in game! */
SAME_IP_ONLINE("same_ip_online"),
SAME_IP_ONLINE("on_join_validation.same_ip_online"),
/** In order to chat you must be authenticated! */
DENIED_CHAT("denied_chat"),
DENIED_CHAT("error.denied_chat"),
/** AntiBot protection mode is enabled! You have to wait some minutes before joining the server. */
KICK_ANTIBOT("kick_antibot"),
KICK_ANTIBOT("antibot.kick_antibot"),
/** This user isn't registered! */
UNKNOWN_USER("unknown_user"),
UNKNOWN_USER("error.unregistered_user"),
/** You're not logged in! */
NOT_LOGGED_IN("not_logged_in"),
NOT_LOGGED_IN("error.not_logged_in"),
/** Usage: /login &lt;password&gt; */
USAGE_LOGIN("usage_log"),
USAGE_LOGIN("login.command_usage"),
/** Wrong password! */
WRONG_PASSWORD("wrong_pwd"),
WRONG_PASSWORD("login.wrong_password"),
/** Successfully unregistered! */
UNREGISTERED_SUCCESS("unregistered"),
UNREGISTERED_SUCCESS("unregister.success"),
/** In-game registration is disabled! */
REGISTRATION_DISABLED("reg_disabled"),
REGISTRATION_DISABLED("registration.disabled"),
/** Logged-in due to Session Reconnection. */
SESSION_RECONNECTION("valid_session"),
SESSION_RECONNECTION("session.valid_session"),
/** Successful login! */
LOGIN_SUCCESS("login"),
LOGIN_SUCCESS("login.success"),
/** Your account isn't activated yet, please check your emails! */
ACCOUNT_NOT_ACTIVATED("vb_nonActiv"),
ACCOUNT_NOT_ACTIVATED("misc.account_not_activated"),
/** You already have registered this username! */
NAME_ALREADY_REGISTERED("user_regged"),
NAME_ALREADY_REGISTERED("registration.name_taken"),
/** You don't have the permission to perform this action! */
NO_PERMISSION("no_perm"),
NO_PERMISSION("error.no_permission"),
/** An unexpected error occurred, please contact an administrator! */
ERROR("error"),
ERROR("error.unexpected_error"),
/** Please, login with the command: /login &lt;password&gt; */
LOGIN_MESSAGE("login_msg"),
LOGIN_MESSAGE("login.login_request"),
/** Please, register to the server with the command: /register &lt;password&gt; &lt;ConfirmPassword&gt; */
REGISTER_MESSAGE("reg_msg"),
REGISTER_MESSAGE("registration.register_request"),
/** 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"),
MAX_REGISTER_EXCEEDED("error.max_registration", "%max_acc", "%reg_count", "%reg_names"),
/** Usage: /register &lt;password&gt; &lt;ConfirmPassword&gt; */
USAGE_REGISTER("usage_reg"),
USAGE_REGISTER("registration.command_usage"),
/** Usage: /unregister &lt;password&gt; */
USAGE_UNREGISTER("usage_unreg"),
USAGE_UNREGISTER("unregister.command_usage"),
/** Password changed successfully! */
PASSWORD_CHANGED_SUCCESS("pwd_changed"),
PASSWORD_CHANGED_SUCCESS("misc.password_changed"),
/** Passwords didn't match, check them again! */
PASSWORD_MATCH_ERROR("password_error"),
PASSWORD_MATCH_ERROR("password.match_error"),
/** You can't use your name as password, please choose another one... */
PASSWORD_IS_USERNAME_ERROR("password_error_nick"),
PASSWORD_IS_USERNAME_ERROR("password.name_in_password"),
/** The chosen password isn't safe, please choose another one... */
PASSWORD_UNSAFE_ERROR("password_error_unsafe"),
PASSWORD_UNSAFE_ERROR("password.unsafe_password"),
/** Your password contains illegal characters. Allowed chars: REG_EX */
PASSWORD_CHARACTERS_ERROR("password_error_chars", "REG_EX"),
/** Your password contains illegal characters. Allowed chars: %valid_chars */
PASSWORD_CHARACTERS_ERROR("password.forbidden_characters", "%valid_chars"),
/** Your IP has been changed and your session data has expired! */
SESSION_EXPIRED("invalid_session"),
SESSION_EXPIRED("session.invalid_session"),
/** Only registered users can join the server! Please visit http://example.com to register yourself! */
MUST_REGISTER_MESSAGE("reg_only"),
MUST_REGISTER_MESSAGE("registration.reg_only"),
/** You're already logged in! */
ALREADY_LOGGED_IN_ERROR("logged_in"),
ALREADY_LOGGED_IN_ERROR("error.logged_in"),
/** Logged out successfully! */
LOGOUT_SUCCESS("logout"),
LOGOUT_SUCCESS("misc.logout"),
/** The same username is already playing on the server! */
USERNAME_ALREADY_ONLINE_ERROR("same_nick"),
USERNAME_ALREADY_ONLINE_ERROR("on_join_validation.same_nick_online"),
/** Successfully registered! */
REGISTER_SUCCESS("registered"),
REGISTER_SUCCESS("registration.success"),
/** Your password is too short or too long! Please try with another one! */
INVALID_PASSWORD_LENGTH("pass_len"),
INVALID_PASSWORD_LENGTH("password.wrong_length"),
/** Configuration and database have been reloaded correctly! */
CONFIG_RELOAD_SUCCESS("reload"),
CONFIG_RELOAD_SUCCESS("misc.reload"),
/** Login timeout exceeded, you have been kicked from the server, please try again! */
LOGIN_TIMEOUT_ERROR("timeout"),
LOGIN_TIMEOUT_ERROR("login.timeout_error"),
/** Usage: /changepassword &lt;oldPassword&gt; &lt;newPassword&gt; */
USAGE_CHANGE_PASSWORD("usage_changepassword"),
USAGE_CHANGE_PASSWORD("misc.usage_change_password"),
/** Your username is either too short or too long! */
INVALID_NAME_LENGTH("name_len"),
INVALID_NAME_LENGTH("on_join_validation.name_length"),
/** Your username contains illegal characters. Allowed chars: REG_EX */
INVALID_NAME_CHARACTERS("regex", "REG_EX"),
/** Your username contains illegal characters. Allowed chars: %valid_chars */
INVALID_NAME_CHARACTERS("on_join_validation.characters_in_name", "%valid_chars"),
/** Please add your email to your account with the command: /email add &lt;yourEmail&gt; &lt;confirmEmail&gt; */
ADD_EMAIL_MESSAGE("add_email"),
ADD_EMAIL_MESSAGE("email.add_email_request"),
/** Forgot your password? Please use the command: /email recovery &lt;yourEmail&gt; */
FORGOT_PASSWORD_MESSAGE("recovery_email"),
FORGOT_PASSWORD_MESSAGE("recovery.forgot_password_hint"),
/** To login you have to solve a captcha code, please use the command: /captcha &lt;theCaptcha&gt; */
USAGE_CAPTCHA("usage_captcha", "<theCaptcha>"),
/** To log in you have to solve a captcha code, please use the command: /captcha %captcha_code */
USAGE_CAPTCHA("captcha.usage_captcha", "%captcha_code"),
/** Wrong captcha, please type "/captcha THE_CAPTCHA" into the chat! */
CAPTCHA_WRONG_ERROR("wrong_captcha", "THE_CAPTCHA"),
/** Wrong captcha, please type "/captcha %captcha_code" into the chat! */
CAPTCHA_WRONG_ERROR("captcha.wrong_captcha", "%captcha_code"),
/** Captcha code solved correctly! */
CAPTCHA_SUCCESS("valid_captcha"),
CAPTCHA_SUCCESS("captcha.valid_captcha"),
/** To register you have to solve a captcha code first, please use the command: /captcha &lt;theCaptcha&gt; */
CAPTCHA_FOR_REGISTRATION_REQUIRED("captcha_for_registration", "<theCaptcha>"),
/** To register you have to solve a captcha first, please use the command: /captcha %captcha_code */
CAPTCHA_FOR_REGISTRATION_REQUIRED("captcha.captcha_for_registration", "%captcha_code"),
/** Valid captcha! You may now register with /register */
REGISTER_CAPTCHA_SUCCESS("register_captcha_valid"),
REGISTER_CAPTCHA_SUCCESS("captcha.register_captcha_valid"),
/** A VIP player has joined the server when it was full! */
KICK_FOR_VIP("kick_forvip"),
KICK_FOR_VIP("error.kick_for_vip"),
/** The server is full, try again later! */
KICK_FULL_SERVER("kick_fullserver"),
KICK_FULL_SERVER("on_join_validation.kick_full_server"),
/** Usage: /email add &lt;email&gt; &lt;confirmEmail&gt; */
USAGE_ADD_EMAIL("usage_email_add"),
USAGE_ADD_EMAIL("email.usage_email_add"),
/** Usage: /email change &lt;oldEmail&gt; &lt;newEmail&gt; */
USAGE_CHANGE_EMAIL("usage_email_change"),
USAGE_CHANGE_EMAIL("email.usage_email_change"),
/** Usage: /email recovery &lt;Email&gt; */
USAGE_RECOVER_EMAIL("usage_email_recovery"),
USAGE_RECOVER_EMAIL("recovery.command_usage"),
/** Invalid new email, try again! */
INVALID_NEW_EMAIL("new_email_invalid"),
INVALID_NEW_EMAIL("email.new_email_invalid"),
/** Invalid old email, try again! */
INVALID_OLD_EMAIL("old_email_invalid"),
INVALID_OLD_EMAIL("email.old_email_invalid"),
/** Invalid email address, try again! */
INVALID_EMAIL("email_invalid"),
INVALID_EMAIL("email.invalid"),
/** Email address successfully added to your account! */
EMAIL_ADDED_SUCCESS("email_added"),
EMAIL_ADDED_SUCCESS("email.added"),
/** Adding email was not allowed */
EMAIL_ADD_NOT_ALLOWED("email.add_not_allowed"),
/** Please confirm your email address! */
CONFIRM_EMAIL_MESSAGE("email_confirm"),
CONFIRM_EMAIL_MESSAGE("email.request_confirmation"),
/** Email address changed correctly! */
EMAIL_CHANGED_SUCCESS("email_changed"),
EMAIL_CHANGED_SUCCESS("email.changed"),
/** Changing email was not allowed */
EMAIL_CHANGE_NOT_ALLOWED("email.change_not_allowed"),
/** Your current email address is: %email */
EMAIL_SHOW("email_show", "%email"),
EMAIL_SHOW("email.email_show", "%email"),
/** You currently don't have email address associated with this account. */
SHOW_NO_EMAIL("show_no_email"),
SHOW_NO_EMAIL("email.no_email_for_account"),
/** Recovery email sent successfully! Please check your email inbox! */
RECOVERY_EMAIL_SENT_MESSAGE("email_send"),
RECOVERY_EMAIL_SENT_MESSAGE("recovery.email_sent"),
/** Your country is banned from this server! */
COUNTRY_BANNED_ERROR("country_banned"),
COUNTRY_BANNED_ERROR("on_join_validation.country_banned"),
/** [AntiBotService] AntiBot enabled due to the huge number of connections! */
ANTIBOT_AUTO_ENABLED_MESSAGE("antibot_auto_enabled"),
ANTIBOT_AUTO_ENABLED_MESSAGE("antibot.auto_enabled"),
/** [AntiBotService] AntiBot disabled after %m minutes! */
ANTIBOT_AUTO_DISABLED_MESSAGE("antibot_auto_disabled", "%m"),
ANTIBOT_AUTO_DISABLED_MESSAGE("antibot.auto_disabled", "%m"),
/** The email address is already being used */
EMAIL_ALREADY_USED_ERROR("email_already_used"),
EMAIL_ALREADY_USED_ERROR("email.already_used"),
/** Your secret code is %code. You can scan it from here %url */
TWO_FACTOR_CREATE("two_factor_create", "%code", "%url"),
TWO_FACTOR_CREATE("two_factor.code_created", "%code", "%url"),
/** Please confirm your code with /2fa confirm &lt;code&gt; */
TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED("two_factor.confirmation_required"),
/** Please submit your two-factor authentication code with /2fa code &lt;code&gt; */
TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"),
/** Two-factor authentication is already enabled for your account! */
TWO_FACTOR_ALREADY_ENABLED("two_factor.already_enabled"),
/** No 2fa key has been generated for you or it has expired. Please run /2fa add */
TWO_FACTOR_ENABLE_ERROR_NO_CODE("two_factor.enable_error_no_code"),
/** Successfully enabled two-factor authentication for your account */
TWO_FACTOR_ENABLE_SUCCESS("two_factor.enable_success"),
/** Wrong code or code has expired. Please run /2fa add */
TWO_FACTOR_ENABLE_ERROR_WRONG_CODE("two_factor.enable_error_wrong_code"),
/** Two-factor authentication is not enabled for your account. Run /2fa add */
TWO_FACTOR_NOT_ENABLED_ERROR("two_factor.not_enabled_error"),
/** Successfully removed two-factor auth from your account */
TWO_FACTOR_REMOVED_SUCCESS("two_factor.removed_success"),
/** Invalid code! */
TWO_FACTOR_INVALID_CODE("two_factor.invalid_code"),
/** You are not the owner of this account. Please choose another name! */
NOT_OWNER_ERROR("not_owner_error"),
NOT_OWNER_ERROR("on_join_validation.not_owner_error"),
/** You should join using username %valid, not %invalid. */
INVALID_NAME_CASE("invalid_name_case", "%valid", "%invalid"),
INVALID_NAME_CASE("on_join_validation.invalid_name_case", "%valid", "%invalid"),
/** You have been temporarily banned for failing to log in too many times. */
TEMPBAN_MAX_LOGINS("tempban_max_logins"),
TEMPBAN_MAX_LOGINS("error.tempban_max_logins"),
/** You own %count accounts: */
ACCOUNTS_OWNED_SELF("accounts_owned_self", "%count"),
ACCOUNTS_OWNED_SELF("misc.accounts_owned_self", "%count"),
/** The player %name has %count accounts: */
ACCOUNTS_OWNED_OTHER("accounts_owned_other", "%name", "%count"),
ACCOUNTS_OWNED_OTHER("misc.accounts_owned_other", "%name", "%count"),
/** An admin just registered you; please log in again */
KICK_FOR_ADMIN_REGISTER("kicked_admin_registered"),
KICK_FOR_ADMIN_REGISTER("registration.kicked_admin_registered"),
/** Error: not all required settings are set for sending emails. Please contact an admin. */
INCOMPLETE_EMAIL_SETTINGS("incomplete_email_settings"),
INCOMPLETE_EMAIL_SETTINGS("email.incomplete_settings"),
/** The email could not be sent. Please contact an administrator. */
EMAIL_SEND_FAILURE("email_send_failure"),
EMAIL_SEND_FAILURE("email.send_failure"),
/** A recovery code to reset your password has been sent to your email. */
RECOVERY_CODE_SENT("recovery_code_sent"),
RECOVERY_CODE_SENT("recovery.code.code_sent"),
/** The recovery code is not correct! You have %count tries remaining. */
INCORRECT_RECOVERY_CODE("recovery_code_incorrect", "%count"),
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_TRIES_EXCEEDED("recovery.code.tries_exceeded"),
/** Recovery code entered correctly! */
RECOVERY_CODE_CORRECT("recovery_code_correct"),
RECOVERY_CODE_CORRECT("recovery.code.correct"),
/** Please use the command /email setpassword to change your password immediately. */
RECOVERY_CHANGE_PASSWORD("recovery_change_password"),
RECOVERY_CHANGE_PASSWORD("recovery.code.change_password"),
/** You cannot change your password using this command anymore. */
CHANGE_PASSWORD_EXPIRED("change_password_expired"),
CHANGE_PASSWORD_EXPIRED("email.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"),
EMAIL_COOLDOWN_ERROR("email.email_cooldown_error", "%time"),
/**
* The command you are trying to execute is sensitive and requires a verification!
* A verification code has been sent to your email,
* run the command "/verification [code]" to verify your identity.
* This command is sensitive and requires an email verification!
* Check your inbox and follow the email's instructions.
*/
VERIFICATION_CODE_REQUIRED("verification_code_required"),
VERIFICATION_CODE_REQUIRED("verification.code_required"),
/** Usage: /verification &lt;code&gt; */
USAGE_VERIFICATION_CODE("usage_verification_code"),
USAGE_VERIFICATION_CODE("verification.command_usage"),
/** Incorrect code, please type "/verification &lt;code&gt;" into the chat! */
INCORRECT_VERIFICATION_CODE("incorrect_verification_code"),
/** Incorrect code, please type "/verification &lt;code&gt;" into the chat, using the code you received by email */
INCORRECT_VERIFICATION_CODE("verification.incorrect_code"),
/**
* Your identity has been verified!
* You can now execute every sensitive command within the current session!
*/
VERIFICATION_CODE_VERIFIED("verification_code_verified"),
/** Your identity has been verified! You can now execute all commands within the current session! */
VERIFICATION_CODE_VERIFIED("verification.success"),
/**
* You can already execute every sensitive command within the current session!
*/
VERIFICATION_CODE_ALREADY_VERIFIED("verification_code_already_verified"),
/** You can already execute every sensitive command within the current session! */
VERIFICATION_CODE_ALREADY_VERIFIED("verification.already_verified"),
/** Your code has expired! Execute another sensitive command to get a new code! */
VERIFICATION_CODE_EXPIRED("verification_code_expired"),
VERIFICATION_CODE_EXPIRED("verification.code_expired"),
/** To verify your identity you need to link an email address with your account! */
VERIFICATION_CODE_EMAIL_NEEDED("verification_code_email_needed"),
VERIFICATION_CODE_EMAIL_NEEDED("verification.email_needed"),
/** You used a command too fast! Please, join the server again and wait more before using any command. */
QUICK_COMMAND_PROTECTION_KICK("on_join_validation.quick_command"),
/** second */
SECOND("second"),
SECOND("time.second"),
/** seconds */
SECONDS("seconds"),
SECONDS("time.seconds"),
/** minute */
MINUTE("minute"),
MINUTE("time.minute"),
/** minutes */
MINUTES("minutes"),
MINUTES("time.minutes"),
/** hour */
HOUR("hour"),
HOUR("time.hour"),
/** hours */
HOURS("hours"),
HOURS("time.hours"),
/** day */
DAY("day"),
DAY("time.day"),
/** days */
DAYS("days");
DAYS("time.days");
private String key;

View File

@ -2,10 +2,10 @@ 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 org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Map;
@ -14,11 +14,15 @@ import java.util.concurrent.TimeUnit;
/**
* Class for retrieving and sending translatable messages to players.
*/
public class Messages implements Reloadable {
public class Messages {
// Custom Authme tag replaced to new line
private static final String NEWLINE_TAG = "%nl%";
// Global tag replacements
private static final String USERNAME_TAG = "%username%";
private static final String DISPLAYNAME_TAG = "%displayname%";
/** Contains the keys of the singular messages for time units. */
private static final Map<TimeUnit, MessageKey> TIME_UNIT_SINGULARS = ImmutableMap.<TimeUnit, MessageKey>builder()
.put(TimeUnit.SECONDS, MessageKey.SECOND)
@ -33,16 +37,14 @@ public class Messages implements Reloadable {
.put(TimeUnit.HOURS, MessageKey.HOURS)
.put(TimeUnit.DAYS, MessageKey.DAYS).build();
private final MessageFileHandlerProvider messageFileHandlerProvider;
private MessageFileHandler messageFileHandler;
private MessagesFileHandler messagesFileHandler;
/*
* Constructor.
*/
@Inject
Messages(MessageFileHandlerProvider messageFileHandlerProvider) {
this.messageFileHandlerProvider = messageFileHandlerProvider;
reload();
Messages(MessagesFileHandler messagesFileHandler) {
this.messagesFileHandler = messagesFileHandler;
}
/**
@ -52,7 +54,7 @@ public class Messages implements Reloadable {
* @param key The key of the message to send
*/
public void send(CommandSender sender, MessageKey key) {
String[] lines = retrieve(key);
String[] lines = retrieve(key, sender);
for (String line : lines) {
sender.sendMessage(line);
}
@ -68,7 +70,7 @@ public class Messages implements Reloadable {
* @param replacements The replacements to apply for the tags
*/
public void send(CommandSender sender, MessageKey key, String... replacements) {
String message = retrieveSingle(key, replacements);
String message = retrieveSingle(sender, key, replacements);
for (String line : message.split("\n")) {
sender.sendMessage(line);
}
@ -78,10 +80,11 @@ public class Messages implements Reloadable {
* Retrieve the message from the text file and return it split by new line as an array.
*
* @param key The message key to retrieve
* @param sender The entity to send the message to
* @return The message split by new lines
*/
public String[] retrieve(MessageKey key) {
String message = retrieveMessage(key);
public String[] retrieve(MessageKey key, CommandSender sender) {
String message = retrieveMessage(key, sender);
if (message.isEmpty()) {
// Return empty array instead of array with 1 empty string as entry
return new String[0];
@ -102,18 +105,43 @@ public class Messages implements Reloadable {
? TIME_UNIT_SINGULARS.get(duration.getTimeUnit())
: TIME_UNIT_PLURALS.get(duration.getTimeUnit());
return value + " " + retrieveMessage(timeUnitKey);
return value + " " + retrieveMessage(timeUnitKey, "");
}
/**
* Retrieve the message from the text file.
*
* @param key The message key to retrieve
* @param sender The entity to send the message to
* @return The message from the file
*/
private String retrieveMessage(MessageKey key) {
return formatMessage(
messageFileHandler.getMessage(key.getKey()));
private String retrieveMessage(MessageKey key, CommandSender sender) {
String message = messagesFileHandler.getMessage(key.getKey());
String displayName = sender.getName();
if (sender instanceof Player) {
displayName = ((Player) sender).getDisplayName();
}
return ChatColor.translateAlternateColorCodes('&', message)
.replace(NEWLINE_TAG, "\n")
.replace(USERNAME_TAG, sender.getName())
.replace(DISPLAYNAME_TAG, displayName);
}
/**
* Retrieve the message from the text file.
*
* @param key The message key to retrieve
* @param name The name of the entity to send the message to
* @return The message from the file
*/
private String retrieveMessage(MessageKey key, String name) {
String message = messagesFileHandler.getMessage(key.getKey());
return ChatColor.translateAlternateColorCodes('&', message)
.replace(NEWLINE_TAG, "\n")
.replace(USERNAME_TAG, name)
.replace(DISPLAYNAME_TAG, name);
}
/**
@ -121,12 +149,13 @@ public class Messages implements Reloadable {
* logs an error if the number of supplied replacements doesn't correspond to the number of tags
* the message key contains.
*
* @param sender The entity to send the message to
* @param key The key of the message to send
* @param replacements The replacements to apply for the tags
* @return The message from the file with replacements
*/
public String retrieveSingle(MessageKey key, String... replacements) {
String message = retrieveMessage(key);
public String retrieveSingle(CommandSender sender, MessageKey key, String... replacements) {
String message = retrieveMessage(key, sender);
String[] tags = key.getTags();
if (replacements.length == tags.length) {
for (int i = 0; i < tags.length; ++i) {
@ -138,15 +167,26 @@ public class Messages implements Reloadable {
return message;
}
@Override
public void reload() {
this.messageFileHandler = messageFileHandlerProvider
.initializeHandler(lang -> "messages/messages_" + lang + ".yml", "/authme messages");
/**
* Retrieve the given message code with the given tag replacements. Note that this method
* logs an error if the number of supplied replacements doesn't correspond to the number of tags
* the message key contains.
*
* @param name The name of the entity to send the message to
* @param key The key of the message to send
* @param replacements The replacements to apply for the tags
* @return The message from the file with replacements
*/
public String retrieveSingle(String name, MessageKey key, String... replacements) {
String message = retrieveMessage(key, name);
String[] tags = key.getTags();
if (replacements.length == tags.length) {
for (int i = 0; i < tags.length; ++i) {
message = message.replace(tags[i], replacements[i]);
}
} else {
ConsoleLogger.warning("Invalid number of replacements for message key '" + key + "'");
}
return message;
}
private static String formatMessage(String message) {
return ChatColor.translateAlternateColorCodes('&', message)
.replace(NEWLINE_TAG, "\n");
}
}

View File

@ -0,0 +1,43 @@
package fr.xephi.authme.message;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.message.updater.MessageUpdater;
import javax.inject.Inject;
/**
* File handler for the messages_xx.yml resource.
*/
public class MessagesFileHandler extends AbstractMessageFileHandler {
@Inject
private MessageUpdater messageUpdater;
MessagesFileHandler() {
}
@Override
public void reload() {
reloadInternal(false);
}
private void reloadInternal(boolean isFromReload) {
super.reload();
String language = getLanguage();
boolean hasChange = messageUpdater.migrateAndSave(
getUserLanguageFile(), createFilePath(language), createFilePath(DEFAULT_LANGUAGE));
if (hasChange) {
if (isFromReload) {
ConsoleLogger.warning("Migration after reload attempt");
} else {
reloadInternal(true);
}
}
}
@Override
protected String createFilePath(String language) {
return "messages/messages_" + language + ".yml";
}
}

View File

@ -0,0 +1,57 @@
package fr.xephi.authme.message.updater;
import ch.jalu.configme.properties.Property;
import ch.jalu.configme.resource.PropertyReader;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.util.FileUtils;
import java.io.IOException;
import java.io.InputStream;
/**
* Returns messages from the JAR's message files. Favors a local JAR (e.g. messages_nl.yml)
* before falling back to the default language (messages_en.yml).
*/
public class JarMessageSource {
private final PropertyReader localJarMessages;
private final PropertyReader defaultJarMessages;
/**
* Constructor.
*
* @param localJarPath path to the messages file of the language the plugin is configured to use (may not exist)
* @param defaultJarPath path to the default messages file in the JAR (must exist)
*/
public JarMessageSource(String localJarPath, String defaultJarPath) {
localJarMessages = localJarPath.equals(defaultJarPath) ? null : loadJarFile(localJarPath);
defaultJarMessages = loadJarFile(defaultJarPath);
if (defaultJarMessages == null) {
throw new IllegalStateException("Default JAR file '" + defaultJarPath + "' could not be loaded");
}
}
public String getMessageFromJar(Property<?> property) {
String key = property.getPath();
String message = getString(key, localJarMessages);
return message == null ? getString(key, defaultJarMessages) : message;
}
private static String getString(String path, PropertyReader reader) {
return reader == null ? null : reader.getTypedObject(path, String.class);
}
private static MessageMigraterPropertyReader loadJarFile(String jarPath) {
try (InputStream stream = FileUtils.getResourceFromJar(jarPath)) {
if (stream == null) {
ConsoleLogger.debug("Could not load '" + jarPath + "' from JAR");
return null;
}
return MessageMigraterPropertyReader.loadFromStream(stream);
} catch (IOException e) {
ConsoleLogger.logException("Exception while handling JAR path '" + jarPath + "'", e);
}
return null;
}
}

View File

@ -0,0 +1,152 @@
package fr.xephi.authme.message.updater;
import ch.jalu.configme.exception.ConfigMeException;
import ch.jalu.configme.resource.PropertyReader;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of {@link PropertyReader} which can read a file or a stream with
* a specified charset.
*/
public final class MessageMigraterPropertyReader implements PropertyReader {
public static final Charset CHARSET = StandardCharsets.UTF_8;
private Map<String, Object> root;
/** See same field in {@link ch.jalu.configme.resource.YamlFileReader} for details. */
private boolean hasObjectAsRoot = false;
private MessageMigraterPropertyReader(Map<String, Object> valuesMap) {
root = valuesMap;
}
/**
* Creates a new property reader for the given file.
*
* @param file the file to load
* @return the created property reader
*/
public static MessageMigraterPropertyReader loadFromFile(File file) {
Map<String, Object> valuesMap;
try (InputStream is = new FileInputStream(file)) {
valuesMap = readStreamToMap(is);
} catch (IOException e) {
throw new IllegalStateException("Error while reading file '" + file + "'", e);
}
return new MessageMigraterPropertyReader(valuesMap);
}
public static MessageMigraterPropertyReader loadFromStream(InputStream inputStream) {
Map<String, Object> valuesMap = readStreamToMap(inputStream);
return new MessageMigraterPropertyReader(valuesMap);
}
@Override
public Object getObject(String path) {
if (path.isEmpty()) {
return hasObjectAsRoot ? root.get("") : root;
}
Object node = root;
String[] keys = path.split("\\.");
for (String key : keys) {
node = getIfIsMap(key, node);
if (node == null) {
return null;
}
}
return node;
}
@Override
public <T> T getTypedObject(String path, Class<T> clazz) {
Object value = getObject(path);
if (clazz.isInstance(value)) {
return clazz.cast(value);
}
return null;
}
@Override
public void set(String path, Object value) {
Objects.requireNonNull(path);
if (path.isEmpty()) {
root.clear();
root.put("", value);
hasObjectAsRoot = true;
} else if (hasObjectAsRoot) {
throw new ConfigMeException("The root path is a bean property; you cannot set values to any subpath. "
+ "Modify the bean at the root or set a new one instead.");
} else {
setValueInChildPath(path, value);
}
}
/**
* Sets the value at the given path. This method is used when the root is a map and not a specific object.
*
* @param path the path to set the value at
* @param value the value to set
*/
@SuppressWarnings("unchecked")
private void setValueInChildPath(String path, Object value) {
Map<String, Object> node = root;
String[] keys = path.split("\\.");
for (int i = 0; i < keys.length - 1; ++i) {
Object child = node.get(keys[i]);
if (child instanceof Map<?, ?>) {
node = (Map<String, Object>) child;
} else { // child is null or some other value - replace with map
Map<String, Object> newEntry = new HashMap<>();
node.put(keys[i], newEntry);
if (value == null) {
// For consistency, replace whatever value/null here with an empty map,
// but if the value is null our work here is done.
return;
}
node = newEntry;
}
}
// node now contains the parent map (existing or newly created)
if (value == null) {
node.remove(keys[keys.length - 1]);
} else {
node.put(keys[keys.length - 1], value);
}
}
@Override
public void reload() {
throw new UnsupportedOperationException("Reload not supported by this implementation");
}
private static Map<String, Object> readStreamToMap(InputStream inputStream) {
try (InputStreamReader isr = new InputStreamReader(inputStream, CHARSET)) {
Object obj = new Yaml().load(isr);
return obj == null ? new HashMap<>() : (Map<String, Object>) obj;
} catch (IOException e) {
throw new ConfigMeException("Could not read stream", e);
} catch (ClassCastException e) {
throw new ConfigMeException("Top-level is not a map", e);
}
}
private static Object getIfIsMap(String key, Object value) {
if (value instanceof Map<?, ?>) {
return ((Map<?, ?>) value).get(key);
}
return null;
}
}

View File

@ -0,0 +1,168 @@
package fr.xephi.authme.message.updater;
import ch.jalu.configme.SettingsManager;
import ch.jalu.configme.configurationdata.ConfigurationData;
import ch.jalu.configme.configurationdata.PropertyListBuilder;
import ch.jalu.configme.properties.Property;
import ch.jalu.configme.properties.StringProperty;
import ch.jalu.configme.resource.PropertyResource;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Migrates the used messages file to a complete, up-to-date version when necessary.
*/
public class MessageUpdater {
/**
* Configuration data object for all message keys incl. comments associated to sections.
*/
private static final ConfigurationData CONFIGURATION_DATA = buildConfigurationData();
public static ConfigurationData getConfigurationData() {
return CONFIGURATION_DATA;
}
/**
* Applies any necessary migrations to the user's messages file and saves it if it has been modified.
*
* @param userFile the user's messages file (yml file in the plugin's folder)
* @param localJarPath path to the messages file in the JAR for the same language (may not exist)
* @param defaultJarPath path to the messages file in the JAR for the default language
* @return true if the file has been migrated and saved, false if it is up-to-date
*/
public boolean migrateAndSave(File userFile, String localJarPath, String defaultJarPath) {
JarMessageSource jarMessageSource = new JarMessageSource(localJarPath, defaultJarPath);
return migrateAndSave(userFile, jarMessageSource);
}
/**
* Performs the migration.
*
* @param userFile the file to verify and migrate
* @param jarMessageSource jar message source to get texts from if missing
* @return true if the file has been migrated and saved, false if it is up-to-date
*/
private boolean migrateAndSave(File userFile, JarMessageSource jarMessageSource) {
// YamlConfiguration escapes all special characters when saving, making the file hard to use, so use ConfigMe
PropertyResource userResource = new MigraterYamlFileResource(userFile);
// Step 1: Migrate any old keys in the file to the new paths
boolean movedOldKeys = migrateOldKeys(userResource);
// Step 2: Perform newer migrations
boolean movedNewerKeys = migrateKeys(userResource);
// Step 3: Take any missing messages from the message files shipped in the AuthMe JAR
boolean addedMissingKeys = addMissingKeys(jarMessageSource, userResource);
if (movedOldKeys || movedNewerKeys || addedMissingKeys) {
backupMessagesFile(userFile);
SettingsManager settingsManager = new SettingsManager(userResource, null, CONFIGURATION_DATA);
settingsManager.save();
ConsoleLogger.debug("Successfully saved {0}", userFile);
return true;
}
return false;
}
private boolean migrateKeys(PropertyResource userResource) {
return moveIfApplicable(userResource, "misc.two_factor_create", MessageKey.TWO_FACTOR_CREATE.getKey());
}
private static boolean moveIfApplicable(PropertyResource resource, String oldPath, String newPath) {
if (resource.getString(newPath) == null && resource.getString(oldPath) != null) {
resource.setValue(newPath, resource.getString(oldPath));
return true;
}
return false;
}
private boolean migrateOldKeys(PropertyResource userResource) {
boolean hasChange = OldMessageKeysMigrater.migrateOldPaths(userResource);
if (hasChange) {
ConsoleLogger.info("Old keys have been moved to the new ones in your messages_xx.yml file");
}
return hasChange;
}
private boolean addMissingKeys(JarMessageSource jarMessageSource, PropertyResource userResource) {
List<String> addedKeys = new ArrayList<>();
for (Property<?> property : CONFIGURATION_DATA.getProperties()) {
final String key = property.getPath();
if (userResource.getString(key) == null) {
userResource.setValue(key, jarMessageSource.getMessageFromJar(property));
addedKeys.add(key);
}
}
if (!addedKeys.isEmpty()) {
ConsoleLogger.info(
"Added " + addedKeys.size() + " missing keys to your messages_xx.yml file: " + addedKeys);
return true;
}
return false;
}
private static void backupMessagesFile(File messagesFile) {
String backupName = FileUtils.createBackupFilePath(messagesFile);
File backupFile = new File(backupName);
try {
Files.copy(messagesFile, backupFile);
} catch (IOException e) {
throw new IllegalStateException("Could not back up '" + messagesFile + "' to '" + backupFile + "'", e);
}
}
/**
* Constructs the {@link ConfigurationData} for exporting a messages file in its entirety.
*
* @return the configuration data to export with
*/
private static ConfigurationData buildConfigurationData() {
Map<String, String[]> comments = ImmutableMap.<String, String[]>builder()
.put("registration", new String[]{"Registration"})
.put("password", new String[]{"Password errors on registration"})
.put("login", new String[]{"Login"})
.put("error", new String[]{"Errors"})
.put("antibot", new String[]{"AntiBot"})
.put("unregister", new String[]{"Unregister"})
.put("misc", new String[]{"Other messages"})
.put("session", new String[]{"Session messages"})
.put("on_join_validation", new String[]{"Error messages when joining"})
.put("email", new String[]{"Email"})
.put("recovery", new String[]{"Password recovery by email"})
.put("captcha", new String[]{"Captcha"})
.put("verification", new String[]{"Verification code"})
.put("time", new String[]{"Time units"})
.put("two_factor", new String[]{"Two-factor authentication"})
.build();
Set<String> addedKeys = new HashSet<>();
PropertyListBuilder builder = new PropertyListBuilder();
// Add one key per section based on the comments map above so that the order is clear
for (String path : comments.keySet()) {
MessageKey key = Arrays.stream(MessageKey.values()).filter(p -> p.getKey().startsWith(path + "."))
.findFirst().orElseThrow(() -> new IllegalStateException(path));
builder.add(new StringProperty(key.getKey(), ""));
addedKeys.add(key.getKey());
}
// Add all remaining keys to the property list builder
Arrays.stream(MessageKey.values())
.filter(key -> !addedKeys.contains(key.getKey()))
.forEach(key -> builder.add(new StringProperty(key.getKey(), "")));
return new ConfigurationData(builder.create(), comments);
}
}

View File

@ -0,0 +1,103 @@
package fr.xephi.authme.message.updater;
import ch.jalu.configme.beanmapper.leafproperties.LeafPropertiesGenerator;
import ch.jalu.configme.configurationdata.ConfigurationData;
import ch.jalu.configme.exception.ConfigMeException;
import ch.jalu.configme.properties.Property;
import ch.jalu.configme.resource.PropertyPathTraverser;
import ch.jalu.configme.resource.YamlFileResource;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.List;
import static fr.xephi.authme.message.updater.MessageMigraterPropertyReader.CHARSET;
/**
* Extension of {@link YamlFileResource} to fine-tune the export style
* and to be able to specify the character encoding.
*/
public class MigraterYamlFileResource extends YamlFileResource {
private static final String INDENTATION = " ";
private final File file;
private Yaml singleQuoteYaml;
public MigraterYamlFileResource(File file) {
super(file, MessageMigraterPropertyReader.loadFromFile(file), new LeafPropertiesGenerator());
this.file = file;
}
@Override
protected Yaml getSingleQuoteYaml() {
if (singleQuoteYaml == null) {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setAllowUnicode(true);
options.setDefaultScalarStyle(DumperOptions.ScalarStyle.SINGLE_QUOTED);
// Overridden setting: don't split lines
options.setSplitLines(false);
singleQuoteYaml = new Yaml(options);
}
return singleQuoteYaml;
}
@Override
public void exportProperties(ConfigurationData configurationData) {
try (FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(fos, CHARSET)) {
PropertyPathTraverser pathTraverser = new PropertyPathTraverser(configurationData);
for (Property<?> property : convertPropertiesToExportableTypes(configurationData.getProperties())) {
List<PropertyPathTraverser.PathElement> pathElements = pathTraverser.getPathElements(property);
for (PropertyPathTraverser.PathElement pathElement : pathElements) {
writeComments(writer, pathElement.indentationLevel, pathElement.comments);
writer.append("\n")
.append(indent(pathElement.indentationLevel))
.append(pathElement.name)
.append(":");
}
writer.append(" ")
.append(toYaml(property, pathElements.get(pathElements.size() - 1).indentationLevel));
}
writer.flush();
writer.close();
} catch (IOException e) {
throw new ConfigMeException("Could not save config to '" + file.getPath() + "'", e);
} finally {
singleQuoteYaml = null;
}
}
private void writeComments(Writer writer, int indentation, String[] comments) throws IOException {
if (comments.length == 0) {
return;
}
String commentStart = "\n" + indent(indentation) + "# ";
for (String comment : comments) {
writer.append(commentStart).append(comment);
}
}
private <T> String toYaml(Property<T> property, int indent) {
Object value = property.getValue(this);
String representation = transformValue(property, value);
String[] lines = representation.split("\\n");
return String.join("\n" + indent(indent), lines);
}
private static String indent(int level) {
String result = "";
for (int i = 0; i < level; i++) {
result += INDENTATION;
}
return result;
}
}

View File

@ -0,0 +1,168 @@
package fr.xephi.authme.message.updater;
import ch.jalu.configme.resource.PropertyResource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import fr.xephi.authme.message.MessageKey;
import java.util.Map;
import static com.google.common.collect.ImmutableMap.of;
/**
* Migrates message files from the old keys (before 5.5) to the new ones.
*
* @see <a href="https://github.com/AuthMe/AuthMeReloaded/issues/1467">Issue #1467</a>
*/
final class OldMessageKeysMigrater {
@VisibleForTesting
static final Map<MessageKey, String> KEYS_TO_OLD_PATH = ImmutableMap.<MessageKey, String>builder()
.put(MessageKey.LOGIN_SUCCESS, "login")
.put(MessageKey.ERROR, "error")
.put(MessageKey.DENIED_COMMAND, "denied_command")
.put(MessageKey.SAME_IP_ONLINE, "same_ip_online")
.put(MessageKey.DENIED_CHAT, "denied_chat")
.put(MessageKey.KICK_ANTIBOT, "kick_antibot")
.put(MessageKey.UNKNOWN_USER, "unknown_user")
.put(MessageKey.NOT_LOGGED_IN, "not_logged_in")
.put(MessageKey.USAGE_LOGIN, "usage_log")
.put(MessageKey.WRONG_PASSWORD, "wrong_pwd")
.put(MessageKey.UNREGISTERED_SUCCESS, "unregistered")
.put(MessageKey.REGISTRATION_DISABLED, "reg_disabled")
.put(MessageKey.SESSION_RECONNECTION, "valid_session")
.put(MessageKey.ACCOUNT_NOT_ACTIVATED, "vb_nonActiv")
.put(MessageKey.NAME_ALREADY_REGISTERED, "user_regged")
.put(MessageKey.NO_PERMISSION, "no_perm")
.put(MessageKey.LOGIN_MESSAGE, "login_msg")
.put(MessageKey.REGISTER_MESSAGE, "reg_msg")
.put(MessageKey.MAX_REGISTER_EXCEEDED, "max_reg")
.put(MessageKey.USAGE_REGISTER, "usage_reg")
.put(MessageKey.USAGE_UNREGISTER, "usage_unreg")
.put(MessageKey.PASSWORD_CHANGED_SUCCESS, "pwd_changed")
.put(MessageKey.PASSWORD_MATCH_ERROR, "password_error")
.put(MessageKey.PASSWORD_IS_USERNAME_ERROR, "password_error_nick")
.put(MessageKey.PASSWORD_UNSAFE_ERROR, "password_error_unsafe")
.put(MessageKey.PASSWORD_CHARACTERS_ERROR, "password_error_chars")
.put(MessageKey.SESSION_EXPIRED, "invalid_session")
.put(MessageKey.MUST_REGISTER_MESSAGE, "reg_only")
.put(MessageKey.ALREADY_LOGGED_IN_ERROR, "logged_in")
.put(MessageKey.LOGOUT_SUCCESS, "logout")
.put(MessageKey.USERNAME_ALREADY_ONLINE_ERROR, "same_nick")
.put(MessageKey.REGISTER_SUCCESS, "registered")
.put(MessageKey.INVALID_PASSWORD_LENGTH, "pass_len")
.put(MessageKey.CONFIG_RELOAD_SUCCESS, "reload")
.put(MessageKey.LOGIN_TIMEOUT_ERROR, "timeout")
.put(MessageKey.USAGE_CHANGE_PASSWORD, "usage_changepassword")
.put(MessageKey.INVALID_NAME_LENGTH, "name_len")
.put(MessageKey.INVALID_NAME_CHARACTERS, "regex")
.put(MessageKey.ADD_EMAIL_MESSAGE, "add_email")
.put(MessageKey.FORGOT_PASSWORD_MESSAGE, "recovery_email")
.put(MessageKey.USAGE_CAPTCHA, "usage_captcha")
.put(MessageKey.CAPTCHA_WRONG_ERROR, "wrong_captcha")
.put(MessageKey.CAPTCHA_SUCCESS, "valid_captcha")
.put(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, "captcha_for_registration")
.put(MessageKey.REGISTER_CAPTCHA_SUCCESS, "register_captcha_valid")
.put(MessageKey.KICK_FOR_VIP, "kick_forvip")
.put(MessageKey.KICK_FULL_SERVER, "kick_fullserver")
.put(MessageKey.USAGE_ADD_EMAIL, "usage_email_add")
.put(MessageKey.USAGE_CHANGE_EMAIL, "usage_email_change")
.put(MessageKey.USAGE_RECOVER_EMAIL, "usage_email_recovery")
.put(MessageKey.INVALID_NEW_EMAIL, "new_email_invalid")
.put(MessageKey.INVALID_OLD_EMAIL, "old_email_invalid")
.put(MessageKey.INVALID_EMAIL, "email_invalid")
.put(MessageKey.EMAIL_ADDED_SUCCESS, "email_added")
.put(MessageKey.CONFIRM_EMAIL_MESSAGE, "email_confirm")
.put(MessageKey.EMAIL_CHANGED_SUCCESS, "email_changed")
.put(MessageKey.EMAIL_SHOW, "email_show")
.put(MessageKey.SHOW_NO_EMAIL, "show_no_email")
.put(MessageKey.RECOVERY_EMAIL_SENT_MESSAGE, "email_send")
.put(MessageKey.COUNTRY_BANNED_ERROR, "country_banned")
.put(MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE, "antibot_auto_enabled")
.put(MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE, "antibot_auto_disabled")
.put(MessageKey.EMAIL_ALREADY_USED_ERROR, "email_already_used")
.put(MessageKey.TWO_FACTOR_CREATE, "two_factor_create")
.put(MessageKey.NOT_OWNER_ERROR, "not_owner_error")
.put(MessageKey.INVALID_NAME_CASE, "invalid_name_case")
.put(MessageKey.TEMPBAN_MAX_LOGINS, "tempban_max_logins")
.put(MessageKey.ACCOUNTS_OWNED_SELF, "accounts_owned_self")
.put(MessageKey.ACCOUNTS_OWNED_OTHER, "accounts_owned_other")
.put(MessageKey.KICK_FOR_ADMIN_REGISTER, "kicked_admin_registered")
.put(MessageKey.INCOMPLETE_EMAIL_SETTINGS, "incomplete_email_settings")
.put(MessageKey.EMAIL_SEND_FAILURE, "email_send_failure")
.put(MessageKey.RECOVERY_CODE_SENT, "recovery_code_sent")
.put(MessageKey.INCORRECT_RECOVERY_CODE, "recovery_code_incorrect")
.put(MessageKey.RECOVERY_TRIES_EXCEEDED, "recovery_tries_exceeded")
.put(MessageKey.RECOVERY_CODE_CORRECT, "recovery_code_correct")
.put(MessageKey.RECOVERY_CHANGE_PASSWORD, "recovery_change_password")
.put(MessageKey.CHANGE_PASSWORD_EXPIRED, "change_password_expired")
.put(MessageKey.EMAIL_COOLDOWN_ERROR, "email_cooldown_error")
.put(MessageKey.VERIFICATION_CODE_REQUIRED, "verification_code_required")
.put(MessageKey.USAGE_VERIFICATION_CODE, "usage_verification_code")
.put(MessageKey.INCORRECT_VERIFICATION_CODE, "incorrect_verification_code")
.put(MessageKey.VERIFICATION_CODE_VERIFIED, "verification_code_verified")
.put(MessageKey.VERIFICATION_CODE_ALREADY_VERIFIED, "verification_code_already_verified")
.put(MessageKey.VERIFICATION_CODE_EXPIRED, "verification_code_expired")
.put(MessageKey.VERIFICATION_CODE_EMAIL_NEEDED, "verification_code_email_needed")
.put(MessageKey.SECOND, "second")
.put(MessageKey.SECONDS, "seconds")
.put(MessageKey.MINUTE, "minute")
.put(MessageKey.MINUTES, "minutes")
.put(MessageKey.HOUR, "hour")
.put(MessageKey.HOURS, "hours")
.put(MessageKey.DAY, "day")
.put(MessageKey.DAYS, "days")
.build();
private static final Map<MessageKey, Map<String, String>> PLACEHOLDER_REPLACEMENTS =
ImmutableMap.<MessageKey, Map<String, String>>builder()
.put(MessageKey.PASSWORD_CHARACTERS_ERROR, of("REG_EX", "%valid_chars"))
.put(MessageKey.INVALID_NAME_CHARACTERS, of("REG_EX", "%valid_chars"))
.put(MessageKey.USAGE_CAPTCHA, of("<theCaptcha>", "%captcha_code"))
.put(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, of("<theCaptcha>", "%captcha_code"))
.put(MessageKey.CAPTCHA_WRONG_ERROR, of("THE_CAPTCHA", "%captcha_code"))
.build();
private OldMessageKeysMigrater() {
}
/**
* Migrates any existing old key paths to their new paths if no text has been defined for the new key.
*
* @param resource the resource to modify and read from
* @return true if at least one message could be migrated, false otherwise
*/
static boolean migrateOldPaths(PropertyResource resource) {
boolean wasPropertyMoved = false;
for (Map.Entry<MessageKey, String> migrationEntry : KEYS_TO_OLD_PATH.entrySet()) {
wasPropertyMoved |= moveIfApplicable(resource, migrationEntry.getKey(), migrationEntry.getValue());
}
return wasPropertyMoved;
}
private static boolean moveIfApplicable(PropertyResource resource, MessageKey messageKey, String oldPath) {
if (resource.getString(messageKey.getKey()) == null) {
String textAtOldPath = resource.getString(oldPath);
if (textAtOldPath != null) {
textAtOldPath = replaceOldPlaceholders(messageKey, textAtOldPath);
resource.setValue(messageKey.getKey(), textAtOldPath);
return true;
}
}
return false;
}
private static String replaceOldPlaceholders(MessageKey key, String text) {
Map<String, String> replacements = PLACEHOLDER_REPLACEMENTS.get(key);
if (replacements == null) {
return text;
}
String newText = text;
for (Map.Entry<String, String> replacement : replacements.entrySet()) {
newText = newText.replace(replacement.getKey(), replacement.getValue());
}
return newText;
}
}

View File

@ -8,6 +8,7 @@ import fr.xephi.authme.permission.handlers.BPermissionsHandler;
import fr.xephi.authme.permission.handlers.LuckPermsHandler;
import fr.xephi.authme.permission.handlers.PermissionHandler;
import fr.xephi.authme.permission.handlers.PermissionHandlerException;
import fr.xephi.authme.permission.handlers.PermissionLoadUserException;
import fr.xephi.authme.permission.handlers.PermissionsExHandler;
import fr.xephi.authme.permission.handlers.VaultHandler;
import fr.xephi.authme.permission.handlers.ZPermissionsHandler;
@ -50,14 +51,8 @@ public class PermissionsManager implements Reloadable {
*/
private PermissionHandler handler = null;
/**
* Constructor.
*
* @param server Server instance
* @param pluginManager Bukkit plugin manager
*/
@Inject
public PermissionsManager(Server server, PluginManager pluginManager, Settings settings) {
PermissionsManager(Server server, PluginManager pluginManager, Settings settings) {
this.server = server;
this.pluginManager = pluginManager;
this.settings = settings;
@ -116,7 +111,9 @@ public class PermissionsManager implements Reloadable {
* 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 {
@ -234,8 +231,9 @@ public class PermissionsManager implements Reloadable {
/**
* Check if the given player has permission for the given permission node.
*
* @param joiningPlayer The player to check
* @param joiningPlayer The player to check
* @param permissionNode The permission node to verify
*
* @return true if the player has permission, false otherwise
*/
public boolean hasPermission(JoiningPlayer joiningPlayer, PermissionNode permissionNode) {
@ -264,6 +262,15 @@ public class PermissionsManager implements Reloadable {
return handler.hasPermissionOffline(player.getName(), permissionNode);
}
/**
* Check whether the offline player with the given name has permission for the given permission node.
* This method is used as a last resort when nothing besides the name is known.
*
* @param name The name of the player
* @param permissionNode The permission node to verify
*
* @return true if the player has permission, false otherwise
*/
public boolean hasPermissionOffline(String name, PermissionNode permissionNode) {
if (permissionNode == null) {
return true;
@ -314,7 +321,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName 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.
*/
public boolean isInGroup(OfflinePlayer player, String groupName) {
return isEnabled() && handler.isInGroup(player, groupName);
@ -327,7 +334,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName 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.
*/
public boolean addGroup(OfflinePlayer player, String groupName) {
if (!isEnabled() || StringUtils.isEmpty(groupName)) {
@ -343,7 +350,7 @@ public class PermissionsManager implements Reloadable {
* @param groupNames The name of the groups to add.
*
* @return True if at least one group was added, 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.
*/
public boolean addGroups(OfflinePlayer player, Collection<String> groupNames) {
// If no permissions system is used, return false
@ -370,7 +377,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName 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.
*/
public boolean removeGroup(OfflinePlayer player, String groupName) {
return isEnabled() && handler.removeFromGroup(player, groupName);
@ -383,7 +390,7 @@ public class PermissionsManager implements Reloadable {
* @param groupNames The name of the groups to remove.
*
* @return True if at least one group was removed, 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.
*/
public boolean removeGroups(OfflinePlayer player, Collection<String> groupNames) {
// If no permissions system is used, return false
@ -411,7 +418,7 @@ public class PermissionsManager implements Reloadable {
* @param groupName 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.
*/
public boolean setGroup(OfflinePlayer player, String groupName) {
return isEnabled() && handler.setGroup(player, groupName);
@ -425,7 +432,7 @@ public class PermissionsManager implements Reloadable {
* @param player The player to remove all groups from.
*
* @return True if succeed, false otherwise.
* False will also be returned if this feature isn't supported for the used permissions system.
* False will also be returned if this feature isn't supported for the used permissions system.
*/
public boolean removeAllGroups(OfflinePlayer player) {
// If no permissions system is used, return false
@ -440,15 +447,47 @@ public class PermissionsManager implements Reloadable {
return removeGroups(player, groupNames);
}
public void loadUserData(UUID uuid) {
if(!isEnabled()) {
/**
* Loads the permission data of the given player.
*
* @param offlinePlayer the offline player.
* @return true if the load was successful.
*/
public boolean loadUserData(OfflinePlayer offlinePlayer) {
try {
try {
loadUserData(offlinePlayer.getUniqueId());
} catch (NoSuchMethodError e) {
loadUserData(offlinePlayer.getName());
}
} catch (PermissionLoadUserException e) {
ConsoleLogger.logException("Unable to load the permission data of user " + offlinePlayer.getName(), e);
return false;
}
return true;
}
/**
* Loads the permission data of the given player unique identifier.
*
* @param uuid the {@link UUID} of the player.
* @throws PermissionLoadUserException if the action failed.
*/
public void loadUserData(UUID uuid) throws PermissionLoadUserException {
if (!isEnabled()) {
return;
}
handler.loadUserData(uuid);
}
public void loadUserData(String name) {
if(!isEnabled()) {
/**
* Loads the permission data of the given player name.
*
* @param name the name of the player.
* @throws PermissionLoadUserException if the action failed.
*/
public void loadUserData(String name) throws PermissionLoadUserException {
if (!isEnabled()) {
return;
}
handler.loadUserData(name);

View File

@ -68,7 +68,22 @@ public enum PlayerPermission implements PermissionNode {
/**
* Permission to use the email verification codes feature.
*/
VERIFICATION_CODE("authme.player.security.verificationcode");
VERIFICATION_CODE("authme.player.security.verificationcode"),
/**
* Permission that enables on join quick commands checks for the player.
*/
QUICK_COMMANDS_PROTECTION("authme.player.protection.quickcommandsprotection"),
/**
* Permission to enable two-factor authentication.
*/
ENABLE_TWO_FACTOR_AUTH("authme.player.totpadd"),
/**
* Permission to disable two-factor authentication.
*/
DISABLE_TWO_FACTOR_AUTH("authme.player.totpremove");
/**
* The permission node.

View File

@ -19,7 +19,7 @@ public enum PlayerStatePermission implements PermissionNode {
/**
* When the server is full and someone with this permission joins the server, someone will be kicked.
*/
IS_VIP("authme.vip", DefaultPermission.OP_ONLY),
IS_VIP("authme.vip", DefaultPermission.NOT_ALLOWED),
/**
* Permission to be able to register multiple accounts.
@ -34,7 +34,12 @@ public enum PlayerStatePermission implements PermissionNode {
/**
* Permission to bypass the GeoIp country code check.
*/
BYPASS_COUNTRY_CHECK("authme.bypasscountrycheck", DefaultPermission.NOT_ALLOWED);
BYPASS_COUNTRY_CHECK("authme.bypasscountrycheck", DefaultPermission.NOT_ALLOWED),
/**
* Permission to send chat messages before being logged in.
*/
ALLOW_CHAT_BEFORE_LOGIN("authme.allowchatbeforelogin", DefaultPermission.NOT_ALLOWED);
/**
* The permission node.
@ -42,7 +47,7 @@ public enum PlayerStatePermission implements PermissionNode {
private String node;
/**
* The default permission level
* The default permission level.
*/
private DefaultPermission defaultPermission;

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.permission.handlers;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PermissionsSystemType;
import me.lucko.luckperms.LuckPerms;
@ -41,13 +42,8 @@ public class LuckPermsHandler implements PermissionHandler {
}
private void saveUser(User user) {
luckPermsApi.getStorage().saveUser(user)
.thenAcceptAsync(wasSuccessful -> {
if (!wasSuccessful) {
return;
}
user.refreshPermissions();
}, luckPermsApi.getStorage().getAsyncExecutor());
luckPermsApi.getUserManager().saveUser(user)
.thenAcceptAsync(wasSuccessful -> user.refreshCachedData());
}
@Override
@ -62,7 +58,8 @@ public class LuckPermsHandler implements PermissionHandler {
return false;
}
DataMutateResult result = user.setPermissionUnchecked(luckPermsApi.getNodeFactory().makeGroupNode(newGroup).build());
DataMutateResult result = user.setPermission(
luckPermsApi.getNodeFactory().makeGroupNode(newGroup).build());
if (result == DataMutateResult.FAIL) {
return false;
}
@ -82,6 +79,8 @@ public class LuckPermsHandler implements PermissionHandler {
public boolean hasPermissionOffline(String name, PermissionNode node) {
User user = luckPermsApi.getUser(name);
if (user == null) {
ConsoleLogger.warning("LuckPermsHandler: tried to check permission for offline user "
+ name + " but it isn't loaded!");
return false;
}
@ -97,6 +96,8 @@ public class LuckPermsHandler implements PermissionHandler {
public boolean isInGroup(OfflinePlayer player, String group) {
User user = luckPermsApi.getUser(player.getName());
if (user == null) {
ConsoleLogger.warning("LuckPermsHandler: tried to check group for offline user "
+ player.getName() + " but it isn't loaded!");
return false;
}
@ -111,6 +112,8 @@ public class LuckPermsHandler implements PermissionHandler {
public boolean removeFromGroup(OfflinePlayer player, String group) {
User user = luckPermsApi.getUser(player.getName());
if (user == null) {
ConsoleLogger.warning("LuckPermsHandler: tried to remove group for offline user "
+ player.getName() + " but it isn't loaded!");
return false;
}
@ -120,7 +123,7 @@ public class LuckPermsHandler implements PermissionHandler {
}
Node groupNode = luckPermsApi.getNodeFactory().makeGroupNode(permissionGroup).build();
boolean result = user.unsetPermissionUnchecked(groupNode) != DataMutateResult.FAIL;
boolean result = user.unsetPermission(groupNode) != DataMutateResult.FAIL;
luckPermsApi.cleanupUser(user);
return result;
@ -130,6 +133,8 @@ public class LuckPermsHandler implements PermissionHandler {
public boolean setGroup(OfflinePlayer player, String group) {
User user = luckPermsApi.getUser(player.getName());
if (user == null) {
ConsoleLogger.warning("LuckPermsHandler: tried to set group for offline user "
+ player.getName() + " but it isn't loaded!");
return false;
}
Group permissionGroup = luckPermsApi.getGroup(group);
@ -137,7 +142,7 @@ public class LuckPermsHandler implements PermissionHandler {
return false;
}
Node groupNode = luckPermsApi.getNodeFactory().makeGroupNode(permissionGroup).build();
DataMutateResult result = user.setPermissionUnchecked(groupNode);
DataMutateResult result = user.setPermission(groupNode);
if (result == DataMutateResult.FAIL) {
return false;
}
@ -152,6 +157,8 @@ public class LuckPermsHandler implements PermissionHandler {
public List<String> getGroups(OfflinePlayer player) {
User user = luckPermsApi.getUser(player.getName());
if (user == null) {
ConsoleLogger.warning("LuckPermsHandler: tried to get groups for offline user "
+ player.getName() + " but it isn't loaded!");
return Collections.emptyList();
}
@ -182,22 +189,21 @@ public class LuckPermsHandler implements PermissionHandler {
}
@Override
public void loadUserData(UUID uuid) {
public void loadUserData(UUID uuid) throws PermissionLoadUserException {
try {
luckPermsApi.getStorage().loadUser(uuid).get(5, TimeUnit.SECONDS);
luckPermsApi.getUserManager().loadUser(uuid).get(5, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
throw new PermissionLoadUserException("Unable to load the permission data of the user " + uuid, e);
}
}
@Override
public void loadUserData(String name) {
public void loadUserData(String name) throws PermissionLoadUserException {
try {
UUID uuid = luckPermsApi.getStorage().getUUID(name).get(5, TimeUnit.SECONDS);
loadUserData(uuid);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
throw new PermissionLoadUserException("Unable to load the permission data of the user " + name, e);
}
}
}

View File

@ -107,10 +107,9 @@ public interface PermissionHandler {
*/
PermissionsSystemType getPermissionSystem();
default void loadUserData(UUID uuid) {
default void loadUserData(UUID uuid) throws PermissionLoadUserException {
}
default void loadUserData(String name) {
default void loadUserData(String name) throws PermissionLoadUserException {
}
}

View File

@ -0,0 +1,13 @@
package fr.xephi.authme.permission.handlers;
import java.util.UUID;
/**
* Exception thrown when a {@link PermissionHandler#loadUserData(UUID uuid)} request fails.
*/
public class PermissionLoadUserException extends Exception {
public PermissionLoadUserException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -12,7 +12,6 @@ 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.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

View File

@ -4,8 +4,10 @@ import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.EmailChangedEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.AsynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
@ -35,6 +37,9 @@ public class AsyncAddEmail implements AsynchronousProcess {
@Inject
private BungeeSender bungeeSender;
@Inject
private BukkitService bukkitService;
AsyncAddEmail() { }
/**
@ -57,6 +62,13 @@ public class AsyncAddEmail implements AsynchronousProcess {
} else if (!validationService.isEmailFreeForRegistration(email, player)) {
service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR);
} else {
EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync
-> new EmailChangedEvent(player, null, email, isAsync));
if (event.isCancelled()) {
ConsoleLogger.info("Could not add email to player '" + player + "' event was cancelled");
service.send(player, MessageKey.EMAIL_ADD_NOT_ALLOWED);
return;
}
auth.setEmail(email);
if (dataSource.updateEmail(auth)) {
playerCache.updatePlayer(auth);

View File

@ -1,10 +1,13 @@
package fr.xephi.authme.process.email;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.EmailChangedEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.AsynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
@ -32,6 +35,9 @@ public class AsyncChangeEmail implements AsynchronousProcess {
@Inject
private BungeeSender bungeeSender;
@Inject
private BukkitService bukkitService;
AsyncChangeEmail() { }
@ -57,14 +63,22 @@ public class AsyncChangeEmail implements AsynchronousProcess {
} else if (!validationService.isEmailFreeForRegistration(newEmail, player)) {
service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR);
} else {
saveNewEmail(auth, player, newEmail);
saveNewEmail(auth, player, oldEmail, newEmail);
}
} else {
outputUnloggedMessage(player);
}
}
private void saveNewEmail(PlayerAuth auth, Player player, String newEmail) {
private void saveNewEmail(PlayerAuth auth, Player player, String oldEmail, String newEmail) {
EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync
-> new EmailChangedEvent(player, oldEmail, newEmail, isAsync));
if (event.isCancelled()) {
ConsoleLogger.info("Could not change email for player '" + player + "' event was cancelled");
service.send(player, MessageKey.EMAIL_CHANGE_NOT_ALLOWED);
return;
}
auth.setEmail(newEmail);
if (dataSource.updateEmail(auth)) {
playerCache.updatePlayer(auth);

View File

@ -18,9 +18,9 @@ import fr.xephi.authme.settings.commandconfig.CommandManager;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.util.InternetProtocolUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
@ -139,7 +139,7 @@ public class AsynchronousJoin implements AsynchronousProcess {
private void handlePlayerWithUnmetNameRestriction(Player player, String ip) {
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> {
player.kickPlayer(service.retrieveSingleMessage(MessageKey.NOT_OWNER_ERROR));
player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.NOT_OWNER_ERROR));
if (service.getProperty(RestrictionSettings.BAN_UNKNOWN_IP)) {
server.banIP(ip);
}
@ -184,12 +184,11 @@ public class AsynchronousJoin implements AsynchronousProcess {
private boolean validatePlayerCountForIp(final Player player, String ip) {
if (service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP) > 0
&& !service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)
&& !"127.0.0.1".equalsIgnoreCase(ip)
&& !"localhost".equalsIgnoreCase(ip)
&& !InternetProtocolUtils.isLoopbackAddress(ip)
&& countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP)) {
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(
() -> player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE)));
() -> player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.SAME_IP_ONLINE)));
return false;
}
return true;

View File

@ -6,6 +6,8 @@ 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.captcha.LoginCaptchaManager;
import fr.xephi.authme.data.limbo.LimboMessageType;
import fr.xephi.authme.data.limbo.LimboPlayerState;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent;
@ -28,6 +30,7 @@ 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.util.InternetProtocolUtils;
import fr.xephi.authme.util.PlayerUtils;
import fr.xephi.authme.util.Utils;
import org.bukkit.ChatColor;
@ -90,7 +93,13 @@ public class AsynchronousLogin implements AsynchronousProcess {
public void login(Player player, String password) {
PlayerAuth auth = getPlayerAuth(player);
if (auth != null && checkPlayerInfo(player, auth, password)) {
performLogin(player, auth);
if (auth.getTotpKey() != null) {
limboService.resetMessageTask(player, LimboMessageType.TOTP_CODE);
limboService.getLimboPlayer(player.getName()).setState(LimboPlayerState.TOTP_REQUIRED);
// TODO #1141: Check if we should check limbo state before processing password
} else {
performLogin(player, auth);
}
}
}
@ -125,7 +134,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
limboService.resetMessageTask(player, false);
limboService.resetMessageTask(player, LimboMessageType.REGISTER);
return null;
}
@ -197,7 +206,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
tempbanManager.tempbanPlayer(player);
} else if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) {
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(
() -> player.kickPlayer(service.retrieveSingleMessage(MessageKey.WRONG_PASSWORD)));
() -> player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.WRONG_PASSWORD)));
} else {
service.send(player, MessageKey.WRONG_PASSWORD);
@ -218,7 +227,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
* @param player the player to log in
* @param auth the associated PlayerAuth object
*/
private void performLogin(Player player, PlayerAuth auth) {
public void performLogin(Player player, PlayerAuth auth) {
if (player.isOnline()) {
final boolean isFirstLogin = (auth.getLastLogin() == null);
@ -317,8 +326,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
// 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
|| service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)
|| "127.0.0.1".equalsIgnoreCase(ip)
|| "localhost".equalsIgnoreCase(ip)) {
|| InternetProtocolUtils.isLoopbackAddress(ip)) {
return false;
}

View File

@ -4,17 +4,19 @@ import ch.jalu.injector.factory.SingletonStore;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.AuthMeAsyncPreRegisterEvent;
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.RegistrationMethod;
import fr.xephi.authme.process.register.executors.RegistrationParameters;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
import fr.xephi.authme.service.bungeecord.MessageType;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.util.InternetProtocolUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.entity.Player;
@ -33,9 +35,9 @@ public class AsyncRegister implements AsynchronousProcess {
@Inject
private PlayerCache playerCache;
@Inject
private CommonService service;
private BukkitService bukkitService;
@Inject
private PermissionsManager permissionsManager;
private CommonService service;
@Inject
private SingletonStore<RegistrationExecutor> registrationExecutorFactory;
@Inject
@ -47,9 +49,9 @@ public class AsyncRegister implements AsynchronousProcess {
/**
* Performs the registration process for the given player.
*
* @param variant the registration method
* @param variant the registration method
* @param parameters the parameters
* @param <P> parameters type
* @param <P> parameters type
*/
public <P extends RegistrationParameters> void register(RegistrationMethod<P> variant, P parameters) {
if (preRegisterCheck(parameters.getPlayer())) {
@ -60,6 +62,13 @@ public class AsyncRegister implements AsynchronousProcess {
}
}
/**
* Checks if the player is able to register, in that case the {@link AuthMeAsyncPreRegisterEvent} is invoked.
*
* @param player the player which is trying to register.
*
* @return true if the checks are successful and the event hasn't marked the action as denied, false otherwise.
*/
private boolean preRegisterCheck(Player player) {
final String name = player.getName().toLowerCase();
if (playerCache.isAuthenticated(name)) {
@ -73,6 +82,12 @@ public class AsyncRegister implements AsynchronousProcess {
return false;
}
AuthMeAsyncPreRegisterEvent event = bukkitService.createAndCallEvent(
isAsync -> new AuthMeAsyncPreRegisterEvent(player, isAsync));
if (!event.canRegister()) {
return false;
}
return isPlayerIpAllowedToRegister(player);
}
@ -80,11 +95,11 @@ public class AsyncRegister implements AsynchronousProcess {
* Executes the registration.
*
* @param parameters the registration parameters
* @param executor the executor to perform the registration process with
* @param <P> registration params type
* @param executor the executor to perform the registration process with
* @param <P> registration params type
*/
private <P extends RegistrationParameters>
void executeRegistration(P parameters, RegistrationExecutor<P> executor) {
void executeRegistration(P parameters, RegistrationExecutor<P> executor) {
PlayerAuth auth = executor.buildPlayerAuth(parameters);
if (database.saveAuth(auth)) {
executor.executePostPersistAction(parameters);
@ -98,15 +113,15 @@ public class AsyncRegister implements AsynchronousProcess {
* Checks whether the registration threshold has been exceeded for the given player's IP address.
*
* @param player the player to check
*
* @return true if registration may take place, false otherwise (IP check failed)
*/
private boolean isPlayerIpAllowedToRegister(Player player) {
final int maxRegPerIp = service.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP);
final String ip = PlayerUtils.getPlayerIp(player);
if (maxRegPerIp > 0
&& !"127.0.0.1".equalsIgnoreCase(ip)
&& !"localhost".equalsIgnoreCase(ip)
&& !permissionsManager.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) {
&& !InternetProtocolUtils.isLoopbackAddress(ip)
&& !service.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) {
List<String> otherAccounts = database.getAllAuthsByIp(ip);
if (otherAccounts.size() >= maxRegPerIp) {
service.send(player, MessageKey.MAX_REGISTER_EXCEEDED, Integer.toString(maxRegPerIp),

View File

@ -2,17 +2,24 @@ package fr.xephi.authme.process.register;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.events.RegisterEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.SynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Performs synchronous tasks after a successful {@link RegistrationType#EMAIL email registration}.
*/
public class ProcessSyncEmailRegister implements SynchronousProcess {
@Inject
private BukkitService bukkitService;
@Inject
private CommonService service;
@ -22,11 +29,17 @@ public class ProcessSyncEmailRegister implements SynchronousProcess {
ProcessSyncEmailRegister() {
}
/**
* Performs sync tasks for a player which has just registered by email.
*
* @param player the recently registered player
*/
public void processEmailRegister(Player player) {
service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED);
limboService.replaceTasksAfterRegistration(player);
player.saveData();
bukkitService.callEvent(new RegisterEvent(player));
ConsoleLogger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player));
}

View File

@ -2,8 +2,10 @@ package fr.xephi.authme.process.register;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.events.RegisterEvent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.SynchronousProcess;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.bungeecord.BungeeSender;
import fr.xephi.authme.settings.commandconfig.CommandManager;
@ -15,6 +17,7 @@ import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Performs synchronous tasks after a successful {@link RegistrationType#PASSWORD password registration}.
*/
public class ProcessSyncPasswordRegister implements SynchronousProcess {
@ -30,6 +33,9 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess {
@Inject
private CommandManager commandManager;
@Inject
private BukkitService bukkitService;
ProcessSyncPasswordRegister() {
}
@ -46,6 +52,11 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess {
}
}
/**
* Processes a player having registered with a password.
*
* @param player the newly registered player
*/
public void processPasswordRegister(Player player) {
service.send(player, MessageKey.REGISTER_SUCCESS);
@ -54,11 +65,12 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess {
}
player.saveData();
bukkitService.callEvent(new RegisterEvent(player));
ConsoleLogger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player));
// Kick Player after Registration is enabled, kick the player
if (service.getProperty(RegistrationSettings.FORCE_KICK_AFTER_REGISTER)) {
player.kickPlayer(service.retrieveSingleMessage(MessageKey.REGISTER_SUCCESS));
player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.REGISTER_SUCCESS));
return;
}

View File

@ -4,7 +4,6 @@ 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;
@ -25,9 +24,6 @@ import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWOR
*/
class EmailRegisterExecutor implements RegistrationExecutor<EmailRegisterParams> {
@Inject
private PermissionsManager permissionsManager;
@Inject
private DataSource dataSource;
@ -46,7 +42,7 @@ class EmailRegisterExecutor implements RegistrationExecutor<EmailRegisterParams>
@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)) {
if (maxRegPerEmail > 0 && !commonService.hasPermission(params.getPlayer(), ALLOW_MULTIPLE_ACCOUNTS)) {
int otherAccounts = dataSource.countAuthsByEmail(params.getEmail());
if (otherAccounts >= maxRegPerEmail) {
commonService.send(params.getPlayer(), MessageKey.MAX_REGISTER_EXCEEDED,

View File

@ -1,6 +1,7 @@
package fr.xephi.authme.security;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -78,6 +79,20 @@ public final class HashUtils {
return hash.length() > 3 && hash.substring(0, 2).equals("$2");
}
/**
* Checks whether the two strings are equal to each other in a time-constant manner.
* This helps to avoid timing side channel attacks,
* cf. <a href="https://github.com/AuthMe/AuthMeReloaded/issues/1561">issue #1561</a>.
*
* @param string1 first string
* @param string2 second string
* @return true if the strings are equal to each other, false otherwise
*/
public static boolean isEqual(String string1, String string2) {
return MessageDigest.isEqual(
string1.getBytes(StandardCharsets.UTF_8), string2.getBytes(StandardCharsets.UTF_8));
}
/**
* Hash the message with the given algorithm and return the hash in its hexadecimal notation.
*

View File

@ -8,7 +8,7 @@ import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.ExceptionUtils;
import javax.inject.Inject;
@ -39,7 +39,7 @@ public class BCrypt implements EncryptionMethod {
try {
return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash());
} catch (IllegalArgumentException e) {
ConsoleLogger.warning("Bcrypt checkpw() returned " + StringUtils.formatException(e));
ConsoleLogger.warning("Bcrypt checkpw() returned " + ExceptionUtils.formatException(e));
}
return false;
}

View File

@ -3,6 +3,8 @@ 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.isEqual;
@Recommendation(Usage.RECOMMENDED)
public class BCrypt2y extends HexSaltedMethod {
@ -23,7 +25,7 @@ public class BCrypt2y extends HexSaltedMethod {
// The salt is the first 29 characters of the hash
String salt = hash.substring(0, 29);
return hash.equals(computeHash(password, salt, null));
return isEqual(hash, computeHash(password, salt, null));
}
@Override

View File

@ -2,12 +2,12 @@ package fr.xephi.authme.security.crypts;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.security.crypts.description.HasSalt;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.ExceptionUtils;
import fr.xephi.authme.util.RandomStringUtils;
/**
@ -37,7 +37,7 @@ public class Ipb4 implements EncryptionMethod {
try {
return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash());
} catch (IllegalArgumentException e) {
ConsoleLogger.warning("Bcrypt checkpw() returned " + StringUtils.formatException(e));
ConsoleLogger.warning("Bcrypt checkpw() returned " + ExceptionUtils.formatException(e));
}
return false;
}

View File

@ -4,6 +4,8 @@ import fr.xephi.authme.security.HashUtils;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.security.crypts.description.Usage;
import static fr.xephi.authme.security.HashUtils.isEqual;
@Recommendation(Usage.ACCEPTABLE)
public class Joomla extends HexSaltedMethod {
@ -16,7 +18,7 @@ public class Joomla extends HexSaltedMethod {
public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) {
String hash = hashedPassword.getHash();
String[] hashParts = hash.split(":");
return hashParts.length == 2 && hash.equals(computeHash(password, hashParts[1], null));
return hashParts.length == 2 && isEqual(hash, computeHash(password, hashParts[1], null));
}
@Override

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