diff options
author | Nay <dnpd.dd@gmail.com> | 2013-09-01 15:52:13 -0700 |
---|---|---|
committer | Nay <dnpd.dd@gmail.com> | 2013-09-01 15:52:13 -0700 |
commit | 68b447b24c659cc13a5c49bdea641808d3884ad4 (patch) | |
tree | 300e8ed2f1a618e80632a91246bc30a4cf1df6ff | |
parent | 37bdc7a62b5d719f9c78d8db99837c769ce900e0 (diff) | |
parent | 722a6c143ae9adbab020df4bae4495e612a677ee (diff) |
Merge pull request #10563 from Ascathor/master
Core/Account: Make account password change security variable and various changes
-rw-r--r-- | sql/base/auth_database.sql | 11 | ||||
-rw-r--r-- | sql/updates/auth/2013_08_17_00_auth_rbac.sql | 35 | ||||
-rw-r--r-- | sql/updates/auth/2013_09_01_00_auth_misc.sql | 34 | ||||
-rw-r--r-- | sql/updates/world/2013_08_19_00_world_trinity_strings.sql | 26 | ||||
-rw-r--r-- | src/server/game/Accounts/AccountMgr.cpp | 85 | ||||
-rw-r--r-- | src/server/game/Accounts/AccountMgr.h | 17 | ||||
-rw-r--r-- | src/server/game/Accounts/RBAC.h | 8 | ||||
-rw-r--r-- | src/server/game/Miscellaneous/Language.h | 13 | ||||
-rw-r--r-- | src/server/game/World/World.cpp | 3 | ||||
-rw-r--r-- | src/server/game/World/World.h | 1 | ||||
-rw-r--r-- | src/server/scripts/Commands/cs_account.cpp | 358 | ||||
-rw-r--r-- | src/server/scripts/Commands/cs_learn.cpp | 8 | ||||
-rw-r--r-- | src/server/scripts/Commands/cs_misc.cpp | 60 | ||||
-rw-r--r-- | src/server/shared/Database/Implementation/LoginDatabase.cpp | 7 | ||||
-rw-r--r-- | src/server/shared/Database/Implementation/LoginDatabase.h | 3 | ||||
-rw-r--r-- | src/server/worldserver/worldserver.conf.dist | 11 |
16 files changed, 612 insertions, 68 deletions
diff --git a/sql/base/auth_database.sql b/sql/base/auth_database.sql index 77f997a1718..2f52b5f33f2 100644 --- a/sql/base/auth_database.sql +++ b/sql/base/auth_database.sql @@ -30,7 +30,8 @@ CREATE TABLE `account` ( `v` varchar(64) NOT NULL DEFAULT '', `s` varchar(64) NOT NULL DEFAULT '', `token_key` varchar(100) NOT NULL DEFAULT '', - `email` varchar(254) NOT NULL DEFAULT '', + `email` varchar(255) NOT NULL DEFAULT '', + `reg_mail` varchar(255) NOT NULL DEFAULT '', `joindate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_ip` varchar(15) NOT NULL DEFAULT '127.0.0.1', `failed_logins` int(10) unsigned NOT NULL DEFAULT '0', @@ -307,7 +308,7 @@ CREATE TABLE `rbac_group_roles` ( LOCK TABLES `rbac_group_roles` WRITE; /*!40000 ALTER TABLE `rbac_group_roles` DISABLE KEYS */; -INSERT INTO `rbac_group_roles` VALUES (1,1),(2,2),(3,3),(4,4),(2,5),(1,6),(1,7),(2,8),(3,8),(4,8),(2,9),(3,9),(4,9),(2,10),(3,10),(4,10),(2,11),(3,11),(4,11),(2,12),(3,12),(4,12),(2,13),(3,13),(4,13),(2,14),(3,14),(4,14),(2,15),(3,15),(4,15),(2,16),(3,16),(4,16),(2,17),(3,17),(4,17),(4,18),(2,19),(3,19),(4,19),(2,20),(3,20),(4,20),(2,21),(3,21),(4,21),(2,22),(3,22),(4,22),(4,23),(2,24),(3,24),(4,24),(2,25),(3,25),(4,25),(2,26),(3,26),(4,26),(2,27),(3,27),(4,27),(2,28),(3,28),(4,28),(2,29),(3,29),(4,29),(2,30),(3,30),(4,30),(2,32),(3,32),(4,32),(2,33),(3,33),(4,33),(1,34),(2,35),(3,35),(4,35),(2,36),(3,36),(4,36),(2,37),(3,37),(4,37),(2,38),(3,38),(4,38),(3,39),(4,39); +INSERT INTO `rbac_group_roles` VALUES (1,1),(2,2),(3,3),(4,4),(2,5),(1,6),(1,7),(2,8),(3,8),(4,8),(2,9),(3,9),(4,9),(2,10),(3,10),(4,10),(2,11),(3,11),(4,11),(2,12),(3,12),(4,12),(2,13),(3,13),(4,13),(2,14),(3,14),(4,14),(2,15),(3,15),(4,15),(2,16),(3,16),(4,16),(2,17),(3,17),(4,17),(4,18),(2,19),(3,19),(4,19),(2,20),(3,20),(4,20),(2,21),(3,21),(4,21),(2,22),(3,22),(4,22),(4,23),(2,24),(3,24),(4,24),(2,25),(3,25),(4,25),(2,26),(3,26),(4,26),(2,27),(3,27),(4,27),(2,28),(3,28),(4,28),(2,29),(3,29),(4,29),(2,30),(3,30),(4,30),(2,32),(3,32),(4,32),(2,33),(3,33),(4,33),(1,34),(2,35),(3,35),(4,35),(2,36),(3,36),(4,36),(2,37),(3,37),(4,37),(2,38),(3,38),(4,38),(3,39),(4,39),(1,40),(2,40),(3,40),(4,40); /*!40000 ALTER TABLE `rbac_group_roles` ENABLE KEYS */; UNLOCK TABLES; @@ -355,7 +356,7 @@ CREATE TABLE `rbac_permissions` ( LOCK TABLES `rbac_permissions` WRITE; /*!40000 ALTER TABLE `rbac_permissions` DISABLE KEYS */; -INSERT INTO `rbac_permissions` VALUES (1,'Instant logout'),(2,'Skip Queue'),(3,'Join Normal Battleground'),(4,'Join Random Battleground'),(5,'Join Arenas'),(6,'Join Dungeon Finder'),(7,'Player Commands (Temporal till commands moved to rbac)'),(8,'Moderator Commands (Temporal till commands moved to rbac)'),(9,'GameMaster Commands (Temporal till commands moved to rbac)'),(10,'Administrator Commands (Temporal till commands moved to rbac)'),(11,'Log GM trades'),(13,'Skip Instance required bosses check'),(14,'Skip character creation team mask check'),(15,'Skip character creation class mask check'),(16,'Skip character creation race mask check'),(17,'Skip character creation reserved name check'),(18,'Skip character creation heroic min level check'),(19,'Skip needed requirements to use channel check'),(20,'Skip disable map check'),(21,'Skip reset talents when used more than allowed check'),(22,'Skip spam chat check'),(23,'Skip over-speed ping check'),(24,'Two side faction characters on the same account'),(25,'Allow say chat between factions'),(26,'Allow channel chat between factions'),(27,'Two side mail interaction'),(28,'See two side who list'),(29,'Add friends of other faction'),(30,'Save character without delay with .save command'),(31,'Use params with .unstuck command'),(32,'Can be assigned tickets with .assign ticket command'),(33,'Notify if a command was not found'),(34,'Check if should appear in list using .gm ingame command'),(35,'See all security levels with who command'),(36,'Filter whispers'),(37,'Use staff badge in chat'),(38,'Resurrect with full Health Points'),(39,'Restore saved gm setting states'),(40,'Allows to add a gm to friend list'),(41,'Use Config option START_GM_LEVEL to assign new character level'),(42,'Allows to use CMSG_WORLD_TELEPORT opcode'),(43,'Allows to use CMSG_WHOIS opcode'),(44,'Receive global GM messages/texts'),(45,'Join channels without announce'),(46,'Change channel settings without being channel moderator'),(47,'Enables lower security than target check'),(48,'Enable IP, Last Login and EMail output in pinfo'); +INSERT INTO `rbac_permissions` VALUES (1,'Instant logout'),(2,'Skip Queue'),(3,'Join Normal Battleground'),(4,'Join Random Battleground'),(5,'Join Arenas'),(6,'Join Dungeon Finder'),(7,'Player Commands (Temporal till commands moved to rbac)'),(8,'Moderator Commands (Temporal till commands moved to rbac)'),(9,'GameMaster Commands (Temporal till commands moved to rbac)'),(10,'Administrator Commands (Temporal till commands moved to rbac)'),(11,'Log GM trades'),(13,'Skip Instance required bosses check'),(14,'Skip character creation team mask check'),(15,'Skip character creation class mask check'),(16,'Skip character creation race mask check'),(17,'Skip character creation reserved name check'),(18,'Skip character creation heroic min level check'),(19,'Skip needed requirements to use channel check'),(20,'Skip disable map check'),(21,'Skip reset talents when used more than allowed check'),(22,'Skip spam chat check'),(23,'Skip over-speed ping check'),(24,'Two side faction characters on the same account'),(25,'Allow say chat between factions'),(26,'Allow channel chat between factions'),(27,'Two side mail interaction'),(28,'See two side who list'),(29,'Add friends of other faction'),(30,'Save character without delay with .save command'),(31,'Use params with .unstuck command'),(32,'Can be assigned tickets with .assign ticket command'),(33,'Notify if a command was not found'),(34,'Check if should appear in list using .gm ingame command'),(35,'See all security levels with who command'),(36,'Filter whispers'),(37,'Use staff badge in chat'),(38,'Resurrect with full Health Points'),(39,'Restore saved gm setting states'),(40,'Allows to add a gm to friend list'),(41,'Use Config option START_GM_LEVEL to assign new character level'),(42,'Allows to use CMSG_WORLD_TELEPORT opcode'),(43,'Allows to use CMSG_WHOIS opcode'),(44,'Receive global GM messages/texts'),(45,'Join channels without announce'),(46,'Change channel settings without being channel moderator'),(47,'Enables lower security than target check'),(48,'Enable IP, Last Login and EMail output in pinfo'),(49,'Forces to enter the email for confirmation on password change'),(50,'Allow user to check his own email with .account'); /*!40000 ALTER TABLE `rbac_permissions` ENABLE KEYS */; UNLOCK TABLES; @@ -382,7 +383,7 @@ CREATE TABLE `rbac_role_permissions` ( LOCK TABLES `rbac_role_permissions` WRITE; /*!40000 ALTER TABLE `rbac_role_permissions` DISABLE KEYS */; -INSERT INTO `rbac_role_permissions` VALUES (5,1),(5,2),(6,3),(6,4),(6,5),(7,6),(1,7),(2,8),(3,9),(4,10),(8,11),(9,13),(33,14),(33,15),(33,16),(33,17),(33,18),(27,19),(22,20),(23,21),(24,22),(17,23),(34,24),(28,25),(30,26),(19,27),(35,28),(36,29),(11,30),(12,31),(10,32),(20,33),(14,34),(37,35),(29,36),(15,37),(13,38),(25,39),(38,40),(26,41),(18,42),(18,43),(16,44),(31,45),(32,46),(21,47),(39,48); +INSERT INTO `rbac_role_permissions` VALUES (5,1),(5,2),(6,3),(6,4),(6,5),(7,6),(1,7),(2,8),(3,9),(4,10),(8,11),(9,13),(33,14),(33,15),(33,16),(33,17),(33,18),(27,19),(22,20),(23,21),(24,22),(17,23),(34,24),(28,25),(30,26),(19,27),(35,28),(36,29),(11,30),(12,31),(10,32),(20,33),(14,34),(37,35),(29,36),(15,37),(13,38),(25,39),(38,40),(26,41),(18,42),(18,43),(16,44),(31,45),(32,46),(21,47),(39,48),(40,49), (41,50); /*!40000 ALTER TABLE `rbac_role_permissions` ENABLE KEYS */; UNLOCK TABLES; @@ -406,7 +407,7 @@ CREATE TABLE `rbac_roles` ( LOCK TABLES `rbac_roles` WRITE; /*!40000 ALTER TABLE `rbac_roles` DISABLE KEYS */; -INSERT INTO `rbac_roles` VALUES (1,'Player Commands'),(2,'Moderator Commands'),(3,'GameMaster Commands'),(4,'Administrator Commands'),(5,'Quick Login/Logout'),(6,'Use Battleground/Arenas'),(7,'Use Dungeon Finder'),(8,'Log GM trades'),(9,'Skip Instance required bosses check'),(10,'Ticket management'),(11,'Instant .save'),(12,'Allow params with .unstuck'),(13,'Full HP after resurrect'),(14,'Appear in GM ingame list'),(15,'Use staff badge in chat'),(16,'Receive global GM messages/texts'),(17,'Skip over-speed ping check'),(18,'Allows Admin Opcodes'),(19,'Two side mail interaction'),(20,'Notify if a command was not found'),(21,'Enables lower security than target check'),(22,'Skip disable map check'),(23,'Skip reset talents when used more than allowed check'),(24,'Skip spam chat check'),(25,'Restore saved gm setting states'),(26,'Use Config option START_GM_LEVEL to assign new character level'),(27,'Skip needed requirements to use channel check'),(28,'Allow say chat between factions'),(29,'Filter whispers'),(30,'Allow channel chat between factions'),(31,'Join channels without announce'),(32,'Change channel settings without being channel moderator'),(33,'Skip character creation checks'),(34,'Two side faction characters on the same account'),(35,'See two side who list'),(36,'Add friends of other faction'),(37,'See all security levels with who command'),(38,'Allows to add a gm to friend list'),(39,'Enable IP, Last Login and EMail output in pinfo'); +INSERT INTO `rbac_roles` VALUES (1,'Player Commands'),(2,'Moderator Commands'),(3,'GameMaster Commands'),(4,'Administrator Commands'),(5,'Quick Login/Logout'),(6,'Use Battleground/Arenas'),(7,'Use Dungeon Finder'),(8,'Log GM trades'),(9,'Skip Instance required bosses check'),(10,'Ticket management'),(11,'Instant .save'),(12,'Allow params with .unstuck'),(13,'Full HP after resurrect'),(14,'Appear in GM ingame list'),(15,'Use staff badge in chat'),(16,'Receive global GM messages/texts'),(17,'Skip over-speed ping check'),(18,'Allows Admin Opcodes'),(19,'Two side mail interaction'),(20,'Notify if a command was not found'),(21,'Enables lower security than target check'),(22,'Skip disable map check'),(23,'Skip reset talents when used more than allowed check'),(24,'Skip spam chat check'),(25,'Restore saved gm setting states'),(26,'Use Config option START_GM_LEVEL to assign new character level'),(27,'Skip needed requirements to use channel check'),(28,'Allow say chat between factions'),(29,'Filter whispers'),(30,'Allow channel chat between factions'),(31,'Join channels without announce'),(32,'Change channel settings without being channel moderator'),(33,'Skip character creation checks'),(34,'Two side faction characters on the same account'),(35,'See two side who list'),(36,'Add friends of other faction'),(37,'See all security levels with who command'),(38,'Allows to add a gm to friend list'),(39,'Enable IP, Last Login and EMail output in pinfo'),(40,'Forces to enter the email for confirmation on password change'),(41,'Allow user to check his own email with .account'); /*!40000 ALTER TABLE `rbac_roles` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sql/updates/auth/2013_08_17_00_auth_rbac.sql b/sql/updates/auth/2013_08_17_00_auth_rbac.sql new file mode 100644 index 00000000000..14c219deaf4 --- /dev/null +++ b/sql/updates/auth/2013_08_17_00_auth_rbac.sql @@ -0,0 +1,35 @@ +ALTER TABLE `account` + ADD COLUMN `reg_mail` VARCHAR(255) NOT NULL DEFAULT '' AFTER `s`; + +UPDATE `account` SET reg_mail = email; + +DELETE FROM `rbac_permissions` WHERE `id` = 49; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES (49, 'Forces to enter the email for confirmation on password change'); + +DELETE FROM `rbac_roles` WHERE `id` = 40; +INSERT INTO `rbac_roles` (`id`, `name`) VALUES (40, 'Forces to enter the email for confirmation on password change'); + +DELETE FROM `rbac_role_permissions` WHERE `roleId` = 40 AND `permissionId` = 49; +INSERT INTO `rbac_role_permissions` (`roleId`, `permissionId`) VALUES (40, 49); + +DELETE FROM `rbac_group_roles` WHERE `groupId` = 1 AND `roleId` = 40; +INSERT INTO `rbac_group_roles` (`groupId`, `roleId`) VALUES (1, 40); + +DELETE FROM `rbac_group_roles` WHERE `groupId` = 2 AND `roleId` = 40; +INSERT INTO `rbac_group_roles` (`groupId`, `roleId`) VALUES (2, 40); + +DELETE FROM `rbac_group_roles` WHERE `groupId` = 3 AND `roleId` = 40; +INSERT INTO `rbac_group_roles` (`groupId`, `roleId`) VALUES (3, 40); + +DELETE FROM `rbac_group_roles` WHERE `groupId` = 4 AND `roleId` = 40; +INSERT INTO `rbac_group_roles` (`groupId`, `roleId`) VALUES (4, 40); + +DELETE FROM `rbac_permissions` WHERE `id` = 50; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES (50, 'Allow user to check his own email with .account'); + +DELETE FROM `rbac_roles` WHERE `id` = 41; +INSERT INTO `rbac_roles` (`id`, `name`) VALUES (41, 'Allow user to check his own email with .account'); + +DELETE FROM `rbac_role_permissions` WHERE `roleId` = 41 AND `permissionId` = 50; +INSERT INTO `rbac_role_permissions` (`roleId`, `permissionId`) VALUES (41, 50); + diff --git a/sql/updates/auth/2013_09_01_00_auth_misc.sql b/sql/updates/auth/2013_09_01_00_auth_misc.sql new file mode 100644 index 00000000000..5a48a4233ba --- /dev/null +++ b/sql/updates/auth/2013_09_01_00_auth_misc.sql @@ -0,0 +1,34 @@ +/* account email */ +SET @amail = 263; + +-- Add new permissions for account mail +DELETE FROM `rbac_permissions` WHERE `id` = @amail; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(@amail, 'account email'); + +-- Add permissions to "corresponding Commands Role" +DELETE FROM `rbac_role_permissions` WHERE `permissionId` = @amail; +INSERT INTO `rbac_role_permissions` (`roleId`, `permissionId`) VALUES +(1, @amail), +(2, @amail), +(3, @amail), +(4, @amail); + + +/* account set sec email + account set sec regmail */ +SET @setsec = 264; + +-- Add new permissions for account set sec +DELETE FROM `rbac_permissions` WHERE `id` BETWEEN @setsec AND @setsec+2; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(@setsec+0, 'account set sec'), +(@setsec+1, 'account set sec email'), +(@setsec+2, 'account set sec regmail'); + +-- Add permissions to "corresponding Commands Role" +DELETE FROM `rbac_role_permissions` WHERE `permissionId` BETWEEN @setsec AND @setsec+2; +INSERT INTO `rbac_role_permissions` (`roleId`, `permissionId`) VALUES +(4, @setsec+0), +(4, @setsec+1), +(4, @setsec+2); diff --git a/sql/updates/world/2013_08_19_00_world_trinity_strings.sql b/sql/updates/world/2013_08_19_00_world_trinity_strings.sql new file mode 100644 index 00000000000..7dd8bb89ee6 --- /dev/null +++ b/sql/updates/world/2013_08_19_00_world_trinity_strings.sql @@ -0,0 +1,26 @@ +DELETE FROM `trinity_string` WHERE entry IN (749, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881); +INSERT INTO `trinity_string` (entry, content_default, content_loc1, content_loc2, content_loc3, content_loc4, content_loc5, content_loc6, content_loc7, content_loc8) VALUES +(749,'│ OS: %s - Latency: %u ms', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(872, 'Entered email is not equal to registration email, check input', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(873, 'The new emails do not match', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(874, 'The email was changed', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(875, 'Your email can''t be longer than 64 characters, email not changed!', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(876, 'Email not changed (unknown error)!', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(877, 'Email change unnecessary, new email is equal to old email', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(878, 'Your email is: %s', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(879, '│ Registration Email: %s - Email: %s', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(880, 'Security Level: %s', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +(881, 'You require an email to change your password.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +UPDATE `command` SET help = 'Syntax: .account password $old_password $new_password $new_password [$email]\r\n\r\nChange your account password. You may need to check the actual security mode to see if email input is necessary.' WHERE name = 'account password'; + +UPDATE `command` SET help = 'Syntax: .account\r\n\r\nDisplay the access level of your account and the email adress if you possess the necessary permissions.' WHERE name = 'account'; + +DELETE FROM `command` WHERE name = 'account email'; +INSERT INTO `command` (name, security, help) VALUES ('account email', 0, 'Syntax: .account email $oldemail $currentpassword $newemail $newemailconfirmation\r\n\r\n Change your account email. You may need to check the actual security mode to see if email input is necessary for password change'); + +DELETE FROM `command` WHERE name = 'account set sec email'; +INSERT INTO `command` (name, security, help) VALUES ('account set sec email', 3, 'Syntax: .account set sec email $accountname $email $emailconfirmation\r\n\r\nSet the email for entered player account.'); + +DELETE FROM `command` WHERE name = 'account set sec regmail'; +INSERT INTO `command` (name, security, help) VALUES ('account set sec regmail', 4, 'Syntax: .account set sec regmail $account $regmail $regmailconfirmation\r\n\r\nSets the regmail for entered player account.'); diff --git a/src/server/game/Accounts/AccountMgr.cpp b/src/server/game/Accounts/AccountMgr.cpp index a9f178685d9..819a3b85fe6 100644 --- a/src/server/game/Accounts/AccountMgr.cpp +++ b/src/server/game/Accounts/AccountMgr.cpp @@ -34,21 +34,24 @@ AccountMgr::~AccountMgr() ClearRBAC(); } -AccountOpResult AccountMgr::CreateAccount(std::string username, std::string password) +AccountOpResult AccountMgr::CreateAccount(std::string username, std::string password, std::string email = "") { if (utf8length(username) > MAX_ACCOUNT_STR) return AOR_NAME_TOO_LONG; // username's too long normalizeString(username); normalizeString(password); + normalizeString(email); if (GetId(username)) - return AOR_NAME_ALREDY_EXIST; // username does already exist + return AOR_NAME_ALREADY_EXIST; // username does already exist PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_ACCOUNT); stmt->setString(0, username); stmt->setString(1, CalculateShaPassHash(username, password)); + stmt->setString(2, email); + stmt->setString(3, email); LoginDatabase.DirectExecute(stmt); // Enforce saving, otherwise AddGroup can fail @@ -202,6 +205,52 @@ AccountOpResult AccountMgr::ChangePassword(uint32 accountId, std::string newPass return AOR_OK; } +AccountOpResult AccountMgr::ChangeEmail(uint32 accountId, std::string newEmail) +{ + std::string username; + + if (!GetName(accountId, username)) + return AOR_NAME_NOT_EXIST; // account doesn't exist + + if (utf8length(newEmail) > MAX_EMAIL_STR) + return AOR_EMAIL_TOO_LONG; + + normalizeString(username); + normalizeString(newEmail); + + PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_EMAIL); + + stmt->setString(0, newEmail); + stmt->setUInt32(1, accountId); + + LoginDatabase.Execute(stmt); + + return AOR_OK; +} + +AccountOpResult AccountMgr::ChangeRegEmail(uint32 accountId, std::string newEmail) +{ + std::string username; + + if (!GetName(accountId, username)) + return AOR_NAME_NOT_EXIST; // account doesn't exist + + if (utf8length(newEmail) > MAX_EMAIL_STR) + return AOR_EMAIL_TOO_LONG; + + normalizeString(username); + normalizeString(newEmail); + + PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_REG_EMAIL); + + stmt->setString(0, newEmail); + stmt->setUInt32(1, accountId); + + LoginDatabase.Execute(stmt); + + return AOR_OK; +} + uint32 AccountMgr::GetId(std::string const& username) { PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_GET_ACCOUNT_ID_BY_USERNAME); @@ -245,6 +294,21 @@ bool AccountMgr::GetName(uint32 accountId, std::string& name) return false; } +bool AccountMgr::GetEmail(uint32 accountId, std::string& email) +{ + PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_GET_EMAIL_BY_ID); + stmt->setUInt32(0, accountId); + PreparedQueryResult result = LoginDatabase.Query(stmt); + + if (result) + { + email = (*result)[0].GetString(); + return true; + } + + return false; +} + bool AccountMgr::CheckPassword(uint32 accountId, std::string password) { std::string username; @@ -263,6 +327,23 @@ bool AccountMgr::CheckPassword(uint32 accountId, std::string password) return (result) ? true : false; } +bool AccountMgr::CheckEmail(uint32 accountId, std::string newEmail) +{ + std::string oldEmail; + + // We simply return false for a non-existing email + if (!GetEmail(accountId, oldEmail)) + return false; + + normalizeString(oldEmail); + normalizeString(newEmail); + + if (strcmp(oldEmail.c_str(), newEmail.c_str()) == 0) + return true; + + return false; +} + uint32 AccountMgr::GetCharactersCount(uint32 accountId) { // check character count diff --git a/src/server/game/Accounts/AccountMgr.h b/src/server/game/Accounts/AccountMgr.h index 878ecde24f9..92c1e2292d0 100644 --- a/src/server/game/Accounts/AccountMgr.h +++ b/src/server/game/Accounts/AccountMgr.h @@ -27,12 +27,21 @@ enum AccountOpResult AOR_OK, AOR_NAME_TOO_LONG, AOR_PASS_TOO_LONG, - AOR_NAME_ALREDY_EXIST, + AOR_EMAIL_TOO_LONG, + AOR_NAME_ALREADY_EXIST, AOR_NAME_NOT_EXIST, AOR_DB_INTERNAL_ERROR }; +enum PasswordChangeSecurity +{ + PW_NONE, + PW_EMAIL, + PW_RBAC +}; + #define MAX_ACCOUNT_STR 16 +#define MAX_EMAIL_STR 64 typedef std::map<uint32, RBACPermission*> RBACPermissionsContainer; typedef std::map<uint32, RBACRole*> RBACRolesContainer; @@ -48,16 +57,20 @@ class AccountMgr ~AccountMgr(); public: - AccountOpResult CreateAccount(std::string username, std::string password); + AccountOpResult CreateAccount(std::string username, std::string password, std::string email); static AccountOpResult DeleteAccount(uint32 accountId); static AccountOpResult ChangeUsername(uint32 accountId, std::string newUsername, std::string newPassword); static AccountOpResult ChangePassword(uint32 accountId, std::string newPassword); + static AccountOpResult ChangeEmail(uint32 accountId, std::string newEmail); + static AccountOpResult ChangeRegEmail(uint32 accountId, std::string newEmail); static bool CheckPassword(uint32 accountId, std::string password); + static bool CheckEmail(uint32 accountId, std::string newEmail); static uint32 GetId(std::string const& username); static uint32 GetSecurity(uint32 accountId); static uint32 GetSecurity(uint32 accountId, int32 realmId); static bool GetName(uint32 accountId, std::string& name); + static bool GetEmail(uint32 accountId, std::string& email); static uint32 GetCharactersCount(uint32 accountId); static std::string CalculateShaPassHash(std::string const& name, std::string const& password); diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index be4356f6b54..ab8354554ec 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -96,6 +96,8 @@ enum RBACPermissions RBAC_PERM_CHANGE_CHANNEL_NOT_MODERATOR = 46,
RBAC_PERM_CHECK_FOR_LOWER_SECURITY = 47,
RBAC_PERM_COMMANDS_PINFO_CHECK_PERSONAL_DATA = 48,
+ RBAC_PERM_EMAIL_CONFIRM_FOR_PASS_CHANGE = 49,
+ RBAC_PERM_MAY_CHECK_OWN_EMAIL = 50, // Leave some space for core permissions
RBAC_PERM_COMMAND_RBAC = 200,
RBAC_PERM_COMMAND_RBAC_ACC = 201,
@@ -160,8 +162,12 @@ enum RBACPermissions RBAC_PERM_COMMAND_BF_SWITCH = 260,
RBAC_PERM_COMMAND_BF_TIMER = 261,
RBAC_PERM_COMMAND_BF_ENABLE = 262,
+ RBAC_PERM_COMMAND_ACCOUNT_EMAIL = 263,
+ RBAC_PERM_COMMAND_ACCOUNT_SET_SEC = 264,
+ RBAC_PERM_COMMAND_ACCOUNT_SET_SEC_EMAIL = 265,
+ RBAC_PERM_COMMAND_ACCOUNT_SET_SEC_REGMAIL = 266,
- // custom permissions 1000+
+ // custom permissions 1000+ RBAC_PERM_MAX
};
diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 772d5bcfb75..ce280a11fc3 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -811,6 +811,7 @@ enum TrinityStrings LANG_PINFO_ACC_ACCOUNT = 714, LANG_PINFO_ACC_LASTLOGIN = 716, LANG_PINFO_ACC_OS = 749, + LANG_PINFO_ACC_REGMAILS = 879, LANG_PINFO_ACC_IP = 752, LANG_PINFO_CHR_LEVEL_LOW = 843, LANG_PINFO_CHR_RACE = 844, @@ -844,7 +845,17 @@ enum TrinityStrings LANG_ARENA_INFO_MEMBERS = 869, LANG_ARENA_LOOKUP = 870, // = 871, see LANG_PINFO_CHR_LEVEL_HIGH - // Room for in-game strings 872-999 not used + LANG_COMMAND_WRONGEMAIL = 872, + LANG_NEW_EMAILS_NOT_MATCH = 873, + LANG_COMMAND_EMAIL = 874, + LANG_EMAIL_TOO_LONG = 875, + LANG_COMMAND_NOTCHANGEEMAIL = 876, + LANG_OLD_EMAIL_IS_NEW_EMAIL = 877, + LANG_COMMAND_EMAIL_OUTPUT = 878, + // = 879, see LANG_PINFO_CHR_REGMAILS + LANG_ACCOUNT_SEC_TYPE = 880, + LANG_RBAC_EMAIL_REQUIRED = 881, + // Room for in-game strings 882-999 not used // Level 4 (CLI only commands) LANG_COMMAND_EXIT = 1000, diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 12ec033dfa7..cf6ad3f9886 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1192,6 +1192,9 @@ void World::LoadConfigSettings(bool reload) // DBC_ItemAttributes m_bool_configs[CONFIG_DBC_ENFORCE_ITEM_ATTRIBUTES] = sConfigMgr->GetBoolDefault("DBC.EnforceItemAttributes", true); + // Accountpassword Secruity + m_int_configs[CONFIG_ACC_PASSCHANGESEC] = sConfigMgr->GetIntDefault("Account.PasswordChangeSecurity", 0); + // Max instances per hour m_int_configs[CONFIG_MAX_INSTANCES_PER_HOUR] = sConfigMgr->GetIntDefault("AccountInstancesPerHour", 5); diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index d4d9c4e2431..907491437c4 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -329,6 +329,7 @@ enum WorldIntConfigs CONFIG_PACKET_SPOOF_POLICY, CONFIG_PACKET_SPOOF_BANMODE, CONFIG_PACKET_SPOOF_BANDURATION, + CONFIG_ACC_PASSCHANGESEC, INT_CONFIG_VALUE_COUNT }; diff --git a/src/server/scripts/Commands/cs_account.cpp b/src/server/scripts/Commands/cs_account.cpp index 42e33faee1c..467b74f4954 100644 --- a/src/server/scripts/Commands/cs_account.cpp +++ b/src/server/scripts/Commands/cs_account.cpp @@ -35,35 +35,43 @@ public: ChatCommand* GetCommands() const OVERRIDE { + static ChatCommand accountSetSecTable[] = + { + { "regmail", RBAC_PERM_COMMAND_ACCOUNT_SET_SEC_REGMAIL, true, &HandleAccountSetRegEmailCommand, "", NULL }, + { "email", RBAC_PERM_COMMAND_ACCOUNT_SET_SEC_EMAIL, true, &HandleAccountSetEmailCommand, "", NULL }, + { NULL, 0, false, NULL, "", NULL } + }; static ChatCommand accountSetCommandTable[] = { - { "addon", RBAC_PERM_COMMAND_ACCOUNT_SET_ADDON, true, &HandleAccountSetAddonCommand, "", NULL }, - { "gmlevel", RBAC_PERM_COMMAND_ACCOUNT_SET_GMLEVEL, true, &HandleAccountSetGmLevelCommand, "", NULL }, - { "password", RBAC_PERM_COMMAND_ACCOUNT_SET_PASSWORD, true, &HandleAccountSetPasswordCommand, "", NULL }, - { NULL, 0, false, NULL, "", NULL } + { "addon", RBAC_PERM_COMMAND_ACCOUNT_SET_ADDON, true, &HandleAccountSetAddonCommand, "", NULL }, + { "sec", RBAC_PERM_COMMAND_ACCOUNT_SET_SEC, true, NULL, "", accountSetSecTable }, + { "gmlevel", RBAC_PERM_COMMAND_ACCOUNT_SET_GMLEVEL, true, &HandleAccountSetGmLevelCommand, "", NULL }, + { "password", RBAC_PERM_COMMAND_ACCOUNT_SET_PASSWORD, true, &HandleAccountSetPasswordCommand, "", NULL }, + { NULL, 0, false, NULL, "", NULL } }; static ChatCommand accountLockCommandTable[] = { - { "country", RBAC_PERM_COMMAND_ACCOUNT_LOCK_COUNTRY, true, &HandleAccountLockCountryCommand, "", NULL }, - { "ip", RBAC_PERM_COMMAND_ACCOUNT_LOCK_IP, true, &HandleAccountLockIpCommand, "", NULL }, - { NULL, 0, false, NULL, "", NULL } + { "country", RBAC_PERM_COMMAND_ACCOUNT_LOCK_COUNTRY, true, &HandleAccountLockCountryCommand, "", NULL }, + { "ip", RBAC_PERM_COMMAND_ACCOUNT_LOCK_IP, true, &HandleAccountLockIpCommand, "", NULL }, + { NULL, 0, false, NULL, "", NULL } }; static ChatCommand accountCommandTable[] = { - { "addon", RBAC_PERM_COMMAND_ACCOUNT_ADDON, false, &HandleAccountAddonCommand, "", NULL }, - { "create", RBAC_PERM_COMMAND_ACCOUNT_CREATE, true, &HandleAccountCreateCommand, "", NULL }, - { "delete", RBAC_PERM_COMMAND_ACCOUNT_DELETE, true, &HandleAccountDeleteCommand, "", NULL }, - { "onlinelist", RBAC_PERM_COMMAND_ACCOUNT_ONLINE_LIST, true, &HandleAccountOnlineListCommand, "", NULL }, - { "lock", RBAC_PERM_COMMAND_ACCOUNT_LOCK, false, NULL, "", accountLockCommandTable }, - { "set", RBAC_PERM_COMMAND_ACCOUNT_SET, true, NULL, "", accountSetCommandTable }, - { "password", RBAC_PERM_COMMAND_ACCOUNT_PASSWORD, false, &HandleAccountPasswordCommand, "", NULL }, - { "", RBAC_PERM_COMMAND_ACCOUNT, false, &HandleAccountCommand, "", NULL }, - { NULL, 0, false, NULL, "", NULL } + { "addon", RBAC_PERM_COMMAND_ACCOUNT_ADDON, false, &HandleAccountAddonCommand, "", NULL }, + { "create", RBAC_PERM_COMMAND_ACCOUNT_CREATE, true, &HandleAccountCreateCommand, "", NULL }, + { "delete", RBAC_PERM_COMMAND_ACCOUNT_DELETE, true, &HandleAccountDeleteCommand, "", NULL }, + { "email", RBAC_PERM_COMMAND_ACCOUNT_EMAIL, false, &HandleAccountEmailCommand, "", NULL }, + { "onlinelist", RBAC_PERM_COMMAND_ACCOUNT_ONLINE_LIST, true, &HandleAccountOnlineListCommand, "", NULL }, + { "lock", RBAC_PERM_COMMAND_ACCOUNT_LOCK, false, NULL, "", accountLockCommandTable }, + { "set", RBAC_PERM_COMMAND_ACCOUNT_SET, true, NULL, "", accountSetCommandTable }, + { "password", RBAC_PERM_COMMAND_ACCOUNT_PASSWORD, false, &HandleAccountPasswordCommand, "", NULL }, + { "", RBAC_PERM_COMMAND_ACCOUNT, false, &HandleAccountCommand, "", NULL }, + { NULL, 0, false, NULL, "", NULL } }; static ChatCommand commandTable[] = { - { "account", RBAC_PERM_COMMAND_ACCOUNT, true, NULL, "", accountCommandTable }, - { NULL, 0, false, NULL, "", NULL } + { "account", RBAC_PERM_COMMAND_ACCOUNT, true, NULL, "", accountCommandTable }, + { NULL, 0, false, NULL, "", NULL } }; return commandTable; } @@ -106,29 +114,35 @@ public: if (!*args) return false; + char* email; + ///- %Parse the command line arguments char* accountName = strtok((char*)args, " "); char* password = strtok(NULL, " "); + if (!(email = strtok(NULL, " '"))) + email = ""; + if (!accountName || !password) return false; - AccountOpResult result = sAccountMgr->CreateAccount(std::string(accountName), std::string(password)); + AccountOpResult result = sAccountMgr->CreateAccount(std::string(accountName), std::string(password), std::string(email)); switch (result) { case AOR_OK: handler->PSendSysMessage(LANG_ACCOUNT_CREATED, accountName); if (handler->GetSession()) { - TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %d (IP: %s) Character:[%s] (GUID: %u) Change Password.", + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %d (IP: %s) Character:[%s] (GUID: %u) created Account %s (Email: '%s')", handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), - handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow()); + handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow(), + accountName, email); } break; case AOR_NAME_TOO_LONG: handler->SendSysMessage(LANG_ACCOUNT_TOO_LONG); handler->SetSentErrorMessage(true); return false; - case AOR_NAME_ALREDY_EXIST: + case AOR_NAME_ALREADY_EXIST: handler->SendSysMessage(LANG_ACCOUNT_ALREADY_EXIST); handler->SetSentErrorMessage(true); return false; @@ -338,6 +352,89 @@ public: return false; } + static bool HandleAccountEmailCommand(ChatHandler* handler, char const* args) + { + if (!*args) + { + handler->SendSysMessage(LANG_CMD_SYNTAX); + handler->SetSentErrorMessage(true); + return false; + } + + char* oldEmail = strtok(NULL, " "); + char* password = strtok(NULL, " "); + char* email = strtok((char*)args, " "); + char* emailConfirmation = strtok(NULL, " "); + + if (!oldEmail || !password || !email || !emailConfirmation) + { + handler->SendSysMessage(LANG_CMD_SYNTAX); + handler->SetSentErrorMessage(true); + return false; + } + + if (!AccountMgr::CheckEmail(handler->GetSession()->GetAccountId(), std::string(oldEmail))) + { + handler->SendSysMessage(LANG_COMMAND_WRONGEMAIL); + handler->SetSentErrorMessage(true); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Tried to change email, but the provided email [%s] is not equal to registration email [%s].", + handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), + handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow(), + email, oldEmail); + return false; + } + + if (!AccountMgr::CheckPassword(handler->GetSession()->GetAccountId(), std::string(password))) + { + handler->SendSysMessage(LANG_COMMAND_WRONGOLDPASSWORD); + handler->SetSentErrorMessage(true); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Tried to change email, but the provided password is wrong.", + handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), + handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow()); + return false; + } + + if (strcmp(email, oldEmail) == 0) + { + handler->SendSysMessage(LANG_OLD_EMAIL_IS_NEW_EMAIL); + handler->SetSentErrorMessage(true); + return false; + } + + if (strcmp(email, emailConfirmation) != 0) + { + handler->SendSysMessage(LANG_NEW_EMAILS_NOT_MATCH); + handler->SetSentErrorMessage(true); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Tried to change email, but the provided password is wrong.", + handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), + handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow()); + return false; + } + + + AccountOpResult result = AccountMgr::ChangeEmail(handler->GetSession()->GetAccountId(), std::string(email)); + switch (result) + { + case AOR_OK: + handler->SendSysMessage(LANG_COMMAND_EMAIL); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Changed Email from [%s] to [%s].", + handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), + handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow(), + oldEmail, email); + break; + case AOR_EMAIL_TOO_LONG: + handler->SendSysMessage(LANG_EMAIL_TOO_LONG); + handler->SetSentErrorMessage(true); + return false; + default: + handler->SendSysMessage(LANG_COMMAND_NOTCHANGEEMAIL); + handler->SetSentErrorMessage(true); + return false; + } + + return true; + } + static bool HandleAccountPasswordCommand(ChatHandler* handler, char const* args) { if (!*args) @@ -347,9 +444,12 @@ public: return false; } + uint32 pwConfig = sWorld->getIntConfig(CONFIG_ACC_PASSCHANGESEC); // 0 - PW_NONE, 1 - PW_EMAIL, 2 - PW_RBAC + char* oldPassword = strtok((char*)args, " "); char* newPassword = strtok(NULL, " "); char* passwordConfirmation = strtok(NULL, " "); + char* emailConfirmation = strtok(NULL, " "); if (!oldPassword || !newPassword || !passwordConfirmation) { @@ -358,16 +458,37 @@ public: return false; } + if ((pwConfig == PW_EMAIL || pwConfig == PW_RBAC && handler->HasPermission(RBAC_PERM_EMAIL_CONFIRM_FOR_PASS_CHANGE)) && !emailConfirmation) + { + handler->SendSysMessage(LANG_CMD_SYNTAX); + handler->SetSentErrorMessage(true); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Tried to change password, but entered no email at all. Has Perm: [%s]", + handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), + handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow(), + handler->HasPermission(RBAC_PERM_EMAIL_CONFIRM_FOR_PASS_CHANGE) ? "Yes" : "No"); + } + if (!AccountMgr::CheckPassword(handler->GetSession()->GetAccountId(), std::string(oldPassword))) { handler->SendSysMessage(LANG_COMMAND_WRONGOLDPASSWORD); handler->SetSentErrorMessage(true); - TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Tried to change password.", + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Tried to change password, but the provided old password is wrong.", handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow()); return false; } + if ((pwConfig == PW_EMAIL || pwConfig == PW_RBAC && handler->HasPermission(RBAC_PERM_EMAIL_CONFIRM_FOR_PASS_CHANGE)) // Either PW_EMAIL or PW_RBAC with the Permission + && !AccountMgr::CheckEmail(handler->GetSession()->GetAccountId(), std::string(emailConfirmation))) + { + handler->SendSysMessage(LANG_COMMAND_WRONGEMAIL); + handler->SetSentErrorMessage(true); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "Account: %u (IP: %s) Character:[%s] (GUID: %u) Tried to change password, but the entered email [%s] is wrong.", + handler->GetSession()->GetAccountId(), handler->GetSession()->GetRemoteAddress().c_str(), + handler->GetSession()->GetPlayer()->GetName().c_str(), handler->GetSession()->GetPlayer()->GetGUIDLow(), + emailConfirmation); + } + if (strcmp(newPassword, passwordConfirmation) != 0) { handler->SendSysMessage(LANG_NEW_PASSWORDS_NOT_MATCH); @@ -399,8 +520,40 @@ public: static bool HandleAccountCommand(ChatHandler* handler, char const* /*args*/) { + // GM Level AccountTypes gmLevel = handler->GetSession()->GetSecurity(); handler->PSendSysMessage(LANG_ACCOUNT_LEVEL, uint32(gmLevel)); + + // Security level required + bool hasRBAC = (handler->HasPermission(RBAC_PERM_EMAIL_CONFIRM_FOR_PASS_CHANGE) ? true : false); + uint32 pwConfig = sWorld->getIntConfig(CONFIG_ACC_PASSCHANGESEC); // 0 - PW_NONE, 1 - PW_EMAIL, 2 - PW_RBAC + + handler->PSendSysMessage(LANG_ACCOUNT_SEC_TYPE, (pwConfig == PW_NONE ? "Lowest level: No Email input required." : + pwConfig == PW_EMAIL ? "Highest level: Email input required." : + pwConfig == PW_RBAC ? "Special level: Your account may require email input depending on settings. That is the case if another lien is printed." : + "Unknown security level: Notify technician for details.")); + + // RBAC required display - is not displayed for console + if (pwConfig == PW_RBAC && handler->GetSession() && hasRBAC) + handler->PSendSysMessage(LANG_RBAC_EMAIL_REQUIRED); + + // Email display if sufficient rights + if (handler->HasPermission(RBAC_PERM_MAY_CHECK_OWN_EMAIL)) + { + std::string emailoutput; + uint32 accountId = handler->GetSession()->GetAccountId(); + + PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_GET_EMAIL_BY_ID); + stmt->setUInt32(0, accountId); + PreparedQueryResult result = LoginDatabase.Query(stmt); + + if (result) + { + emailoutput = (*result)[0].GetString(); + handler->PSendSysMessage(LANG_COMMAND_EMAIL_OUTPUT, emailoutput.c_str()); + } + } + return true; } @@ -471,7 +624,11 @@ public: static bool HandleAccountSetGmLevelCommand(ChatHandler* handler, char const* args) { if (!*args) + { + handler->SendSysMessage(LANG_CMD_SYNTAX); + handler->SetSentErrorMessage(true); return false; + } std::string targetAccountName; uint32 targetAccountId = 0; @@ -524,7 +681,7 @@ public: playerSecurity = SEC_CONSOLE; // can set security level only for target with less security and to less security that we have - // This is also reject self apply in fact + // This also restricts setting handler's own security. targetSecurity = AccountMgr::GetSecurity(targetAccountId, gmRealmID); if (targetSecurity >= playerSecurity || gm >= playerSecurity) { @@ -570,7 +727,11 @@ public: static bool HandleAccountSetPasswordCommand(ChatHandler* handler, char const* args) { if (!*args) + { + handler->SendSysMessage(LANG_CMD_SYNTAX); + handler->SetSentErrorMessage(true); return false; + } ///- Get the command line arguments char* account = strtok((char*)args, " "); @@ -597,7 +758,7 @@ public: } /// can set password only for target with less security - /// This is also reject self apply in fact + /// This also restricts setting handler's own password if (handler->HasLowerSecurityAccount(NULL, targetAccountId, true)) return false; @@ -630,6 +791,153 @@ public: } return true; } + + /// Set normal email for account + static bool HandleAccountSetEmailCommand(ChatHandler* handler, char const* args) + { + if (!*args) + return false; + + ///- Get the command line arguments + char* account = strtok((char*)args, " "); + char* email = strtok(NULL, " "); + char* emailConfirmation = strtok(NULL, " "); + + if (!account || !email || !emailConfirmation) + { + handler->SendSysMessage(LANG_CMD_SYNTAX); + handler->SetSentErrorMessage(true); + return false; + } + + std::string accountName = account; + if (!AccountMgr::normalizeString(accountName)) + { + handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str()); + handler->SetSentErrorMessage(true); + return false; + } + + uint32 targetAccountId = AccountMgr::GetId(accountName); + if (!targetAccountId) + { + handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str()); + handler->SetSentErrorMessage(true); + return false; + } + + /// can set email only for target with less security + /// This also restricts setting handler's own email. + if (handler->HasLowerSecurityAccount(NULL, targetAccountId, true)) + return false; + + if (strcmp(email, emailConfirmation) != 0) + { + handler->SendSysMessage(LANG_NEW_EMAILS_NOT_MATCH); + handler->SetSentErrorMessage(true); + return false; + } + + AccountOpResult result = AccountMgr::ChangeEmail(targetAccountId, email); + switch (result) + { + case AOR_OK: + handler->SendSysMessage(LANG_COMMAND_EMAIL); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "ChangeEmail: Account %s [Id: %u] had it's email changed to %s.", + accountName.c_str(), targetAccountId, email); + break; + case AOR_NAME_NOT_EXIST: + handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str()); + handler->SetSentErrorMessage(true); + return false; + case AOR_EMAIL_TOO_LONG: + handler->SendSysMessage(LANG_EMAIL_TOO_LONG); + handler->SetSentErrorMessage(true); + return false; + default: + handler->SendSysMessage(LANG_COMMAND_NOTCHANGEEMAIL); + handler->SetSentErrorMessage(true); + return false; + } + + return true; + } + + /// Change registration email for account + static bool HandleAccountSetRegEmailCommand(ChatHandler* handler, char const* args) + { + if (!*args) + return false; + + //- We do not want anything short of console to use this by default. + //- So we force that. + if (handler->GetSession()) + return false; + + ///- Get the command line arguments + char* account = strtok((char*)args, " "); + char* email = strtok(NULL, " "); + char* emailConfirmation = strtok(NULL, " "); + + if (!account || !email || !emailConfirmation) + { + handler->SendSysMessage(LANG_CMD_SYNTAX); + handler->SetSentErrorMessage(true); + return false; + } + + std::string accountName = account; + if (!AccountMgr::normalizeString(accountName)) + { + handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str()); + handler->SetSentErrorMessage(true); + return false; + } + + uint32 targetAccountId = AccountMgr::GetId(accountName); + if (!targetAccountId) + { + handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str()); + handler->SetSentErrorMessage(true); + return false; + } + + /// can set email only for target with less security + /// This also restricts setting handler's own email. + if (handler->HasLowerSecurityAccount(NULL, targetAccountId, true)) + return false; + + if (strcmp(email, emailConfirmation) != 0) + { + handler->SendSysMessage(LANG_NEW_EMAILS_NOT_MATCH); + handler->SetSentErrorMessage(true); + return false; + } + + AccountOpResult result = AccountMgr::ChangeRegEmail(targetAccountId, email); + switch (result) + { + case AOR_OK: + handler->SendSysMessage(LANG_COMMAND_EMAIL); + TC_LOG_INFO(LOG_FILTER_CHARACTER, "ChangeRegEmail: Account %s [Id: %u] had it's Registration Email changed to %s.", + accountName.c_str(), targetAccountId, email); + break; + case AOR_NAME_NOT_EXIST: + handler->PSendSysMessage(LANG_ACCOUNT_NOT_EXIST, accountName.c_str()); + handler->SetSentErrorMessage(true); + return false; + case AOR_EMAIL_TOO_LONG: + handler->SendSysMessage(LANG_EMAIL_TOO_LONG); + handler->SetSentErrorMessage(true); + return false; + default: + handler->SendSysMessage(LANG_COMMAND_NOTCHANGEEMAIL); + handler->SetSentErrorMessage(true); + return false; + } + + return true; + } }; void AddSC_account_commandscript() diff --git a/src/server/scripts/Commands/cs_learn.cpp b/src/server/scripts/Commands/cs_learn.cpp index f2ff911fdf9..c165eebe3b5 100644 --- a/src/server/scripts/Commands/cs_learn.cpp +++ b/src/server/scripts/Commands/cs_learn.cpp @@ -339,8 +339,12 @@ public: return true; } - static bool HandleLearnAllCraftsCommand(ChatHandler* handler, char const* /*args*/) + static bool HandleLearnAllCraftsCommand(ChatHandler* handler, char const* args) { + Player* target; + if (!handler->extractPlayerTarget((char*)args, &target)) + return false; + for (uint32 i = 0; i < sSkillLineStore.GetNumRows(); ++i) { SkillLineEntry const* skillInfo = sSkillLineStore.LookupEntry(i); @@ -350,7 +354,7 @@ public: if ((skillInfo->categoryId == SKILL_CATEGORY_PROFESSION || skillInfo->categoryId == SKILL_CATEGORY_SECONDARY) && skillInfo->canLink) // only prof. with recipes have { - HandleLearnSkillRecipesHelper(handler->GetSession()->GetPlayer(), skillInfo->id); + HandleLearnSkillRecipesHelper(target, skillInfo->id); } } diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index ee31542c53a..9c29e31af96 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -1520,20 +1520,21 @@ public: * ** Muted: (Time, Reason, By) - IV. LANG_PINFO_MUTED (if muted) * * Account: %s (id: %u), GM Level: %u - V. LANG_PINFO_ACC_ACCOUNT * * Last Login: %u (Failed Logins: %u) - VI. LANG_PINFO_ACC_LASTLOGIN - * * Uses OS: %s - Latency: %u ms - Email %s - VII. LANG_PINFO_ACC_OS - * * Last IP: %u (Locked: %s) - VIII. LANG_PINFO_ACC_IP - * * Level: %u (%u/%u XP (%u XP left) - IX. LANG_PINFO_CHR_LEVEL - * * Race: %s %s, Class %s - X. LANG_PINFO_CHR_RACE - * * Alive ?: %s - XI. LANG_PINFO_CHR_ALIVE - * * Phase: %s - XII. LANG_PINFO_CHR_PHASE (if not GM) - * * Money: %ug%us%uc - XIII. LANG_PINFO_CHR_MONEY - * * Map: %s, Area: %s - XIV. LANG_PINFO_CHR_MAP - * * Guild: %s (Id: %u) - XV. LANG_PINFO_CHR_GUILD (if in guild) - * ** Rank: %s - XVI. LANG_PINFO_CHR_GUILD_RANK (if in guild) - * ** Note: %s - XVII. LANG_PINFO_CHR_GUILD_NOTE (if in guild and has note) - * ** O. Note: %s - XVIII.LANG_PINFO_CHR_GUILD_ONOTE (if in guild and has officer note) - * * Played time: %s - XIX. LANG_PINFO_CHR_PLAYEDTIME - * * Mails: %u Read/%u Total - XX. LANG_PINFO_CHR_MAILS (if has mails) + * * Uses OS: %s - Latency: %u ms - VII. LANG_PINFO_ACC_OS + * * Registration Email: %s - Email: %s - VIII. LANG_PINFO_ACC_REGMAILS + * * Last IP: %u (Locked: %s) - IX. LANG_PINFO_ACC_IP + * * Level: %u (%u/%u XP (%u XP left) - X. LANG_PINFO_CHR_LEVEL + * * Race: %s %s, Class %s - XI. LANG_PINFO_CHR_RACE + * * Alive ?: %s - XII. LANG_PINFO_CHR_ALIVE + * * Phase: %s - XIII. LANG_PINFO_CHR_PHASE (if not GM) + * * Money: %ug%us%uc - XIV. LANG_PINFO_CHR_MONEY + * * Map: %s, Area: %s - XV. LANG_PINFO_CHR_MAP + * * Guild: %s (Id: %u) - XVI. LANG_PINFO_CHR_GUILD (if in guild) + * ** Rank: %s - XVII. LANG_PINFO_CHR_GUILD_RANK (if in guild) + * ** Note: %s - XVIII.LANG_PINFO_CHR_GUILD_NOTE (if in guild and has note) + * ** O. Note: %s - XVIX. LANG_PINFO_CHR_GUILD_ONOTE (if in guild and has officer note) + * * Played time: %s - XX. LANG_PINFO_CHR_PLAYEDTIME + * * Mails: %u Read/%u Total - XXI. LANG_PINFO_CHR_MAILS (if has mails) * * Not all of them can be moved to the top. These should * place the most important ones to the head, though. @@ -1546,6 +1547,7 @@ public: uint32 accId = 0; uint32 lowguid = GUID_LOPART(targetGuid); std::string eMail = handler->GetTrinityString(LANG_ERROR); + std::string regMail = handler->GetTrinityString(LANG_ERROR); uint32 security = 0; std::string lastIp = handler->GetTrinityString(LANG_ERROR); uint8 locked = 0; @@ -1666,8 +1668,9 @@ public: (!handler->GetSession() || handler->GetSession()->GetSecurity() >= AccountTypes(security))) { eMail = fields[2].GetString(); - lastIp = fields[3].GetString(); - lastLogin = fields[4].GetString(); + regMail = fields[3].GetString(); + lastIp = fields[4].GetString(); + lastLogin = fields[5].GetString(); uint32 ip = inet_addr(lastIp.c_str()); EndianConvertReverse(ip); @@ -1689,12 +1692,12 @@ public: lastIp = "Unauthorized"; lastLogin = "Unauthorized"; } - muteTime = fields[5].GetUInt64(); - muteReason = fields[6].GetString(); - muteBy = fields[7].GetString(); - failedLogins = fields[8].GetUInt32(); - locked = fields[9].GetUInt8(); - OS = fields[10].GetString(); + muteTime = fields[6].GetUInt64(); + muteReason = fields[7].GetString(); + muteBy = fields[8].GetString(); + failedLogins = fields[9].GetUInt32(); + locked = fields[10].GetUInt8(); + OS = fields[11].GetString(); } // Creates a chat link to the character. Returns nameLink @@ -1783,8 +1786,11 @@ public: // Output VI. LANG_PINFO_ACC_LASTLOGIN handler->PSendSysMessage(LANG_PINFO_ACC_LASTLOGIN, lastLogin.c_str(), failedLogins); - // Output VIII. LANG_PINFO_ACC_OS - handler->PSendSysMessage(LANG_PINFO_ACC_OS, OS.c_str(), latency, eMail.c_str()); + // Output VII. LANG_PINFO_ACC_OS + handler->PSendSysMessage(LANG_PINFO_ACC_OS, OS.c_str(), latency); + + // Output VIII. LANG_PINFO_ACC_REGMAILS + handler->PSendSysMessage(LANG_PINFO_ACC_REGMAILS, regMail.c_str(), eMail.c_str()); // Output IX. LANG_PINFO_ACC_IP handler->PSendSysMessage(LANG_PINFO_ACC_IP, lastIp.c_str(), locked ? "Yes" : "No"); @@ -1828,7 +1834,7 @@ public: if (target) handler->PSendSysMessage(LANG_PINFO_CHR_MAP, map->name[locale], (!zoneName.empty() ? zoneName.c_str() : "<Unknown>"), (!areaName.empty() ? areaName.c_str() : "<Unknown>")); - // Output XVII. - XX. if they are not empty + // Output XVII. - XVIX. if they are not empty if (!guildName.empty()) { handler->PSendSysMessage(LANG_PINFO_CHR_GUILD, guildName.c_str(), guildId); @@ -1839,7 +1845,7 @@ public: handler->PSendSysMessage(LANG_PINFO_CHR_GUILD_ONOTE, officeNote.c_str()); } - // Output XXI. LANG_PINFO_CHR_PLAYEDTIME + // Output XX. LANG_PINFO_CHR_PLAYEDTIME handler->PSendSysMessage(LANG_PINFO_CHR_PLAYEDTIME, (secsToTimeString(totalPlayerTime, true, true)).c_str()); // Mail Data - an own query, because it may or may not be useful. @@ -1861,7 +1867,7 @@ public: // ... we have to convert it from Char to int. We can use totalmail as it is rmailint = atol(readmail.c_str()); - // Output XXII. LANG_INFO_CHR_MAILS if at least one mail is given + // Output XXI. LANG_INFO_CHR_MAILS if at least one mail is given if (totalmail >= 1) handler->PSendSysMessage(LANG_PINFO_CHR_MAILS, rmailint, totalmail); } diff --git a/src/server/shared/Database/Implementation/LoginDatabase.cpp b/src/server/shared/Database/Implementation/LoginDatabase.cpp index 26940c8a599..6e01e8f515a 100644 --- a/src/server/shared/Database/Implementation/LoginDatabase.cpp +++ b/src/server/shared/Database/Implementation/LoginDatabase.cpp @@ -56,7 +56,7 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_DEL_REALM_CHARACTERS, "DELETE FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_INS_REALM_CHARACTERS, "INSERT INTO realmcharacters (numchars, acctid, realmid) VALUES (?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_SUM_REALM_CHARACTERS, "SELECT SUM(numchars) FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC); - PrepareStatement(LOGIN_INS_ACCOUNT, "INSERT INTO account(username, sha_pass_hash, joindate) VALUES(?, ?, NOW())", CONNECTION_SYNCH); + PrepareStatement(LOGIN_INS_ACCOUNT, "INSERT INTO account(username, sha_pass_hash, reg_mail, email, joindate) VALUES(?, ?, ?, ?, NOW())", CONNECTION_SYNCH); PrepareStatement(LOGIN_INS_REALM_CHARACTERS_INIT, "INSERT INTO realmcharacters (realmid, acctid, numchars) SELECT realmlist.id, account.id, 0 FROM realmlist, account LEFT JOIN realmcharacters ON acctid=account.id WHERE acctid IS NULL", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_EXPANSION, "UPDATE account SET expansion = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_ACCOUNT_LOCK, "UPDATE account SET locked = ? WHERE id = ?", CONNECTION_ASYNC); @@ -64,6 +64,8 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_INS_LOG, "INSERT INTO logs (time, realm, type, level, string) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_USERNAME, "UPDATE account SET v = 0, s = 0, username = ?, sha_pass_hash = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_PASSWORD, "UPDATE account SET v = 0, s = 0, sha_pass_hash = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_UPD_EMAIL, "UPDATE account SET email = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_UPD_REG_EMAIL, "UPDATE account SET reg_mail = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_MUTE_TIME, "UPDATE account SET mutetime = ? , mutereason = ? , muteby = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_MUTE_TIME_LOGIN, "UPDATE account SET mutetime = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_LAST_IP, "UPDATE account SET last_ip = ? WHERE username = ?", CONNECTION_ASYNC); @@ -79,7 +81,7 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_GET_USERNAME_BY_ID, "SELECT username FROM account WHERE id = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_CHECK_PASSWORD, "SELECT 1 FROM account WHERE id = ? AND sha_pass_hash = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_CHECK_PASSWORD_BY_NAME, "SELECT 1 FROM account WHERE username = ? AND sha_pass_hash = ?", CONNECTION_SYNCH); - PrepareStatement(LOGIN_SEL_PINFO, "SELECT a.username, aa.gmlevel, a.email, a.last_ip, DATE_FORMAT(a.last_login, '%Y-%m-%d %T'), a.mutetime, a.mutereason, a.muteby, a.failed_logins, a.locked, a.OS FROM account a LEFT JOIN account_access aa ON (a.id = aa.id AND (aa.RealmID = ? OR aa.RealmID = -1)) WHERE a.id = ?", CONNECTION_SYNCH); + PrepareStatement(LOGIN_SEL_PINFO, "SELECT a.username, aa.gmlevel, a.email, a.reg_mail, a.last_ip, DATE_FORMAT(a.last_login, '%Y-%m-%d %T'), a.mutetime, a.mutereason, a.muteby, a.failed_logins, a.locked, a.OS FROM account a LEFT JOIN account_access aa ON (a.id = aa.id AND (aa.RealmID = ? OR aa.RealmID = -1)) WHERE a.id = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_PINFO_BANS, "SELECT unbandate, bandate = unbandate, bannedby, banreason FROM account_banned WHERE id = ? AND active ORDER BY bandate ASC LIMIT 1", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_GM_ACCOUNTS, "SELECT a.username, aa.gmlevel FROM account a, account_access aa WHERE a.id=aa.id AND aa.gmlevel >= ? AND (aa.realmid = -1 OR aa.realmid = ?)", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_ACCOUNT_INFO, "SELECT a.username, a.last_ip, aa.gmlevel, a.expansion FROM account a LEFT JOIN account_access aa ON (a.id = aa.id) WHERE a.id = ?", CONNECTION_SYNCH); @@ -92,6 +94,7 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_DEL_ACCOUNT, "DELETE FROM account WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_IP2NATION_COUNTRY, "SELECT c.country FROM ip2nationCountries c, ip2nation i WHERE i.ip < ? AND c.code = i.country ORDER BY i.ip DESC LIMIT 0,1", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_AUTOBROADCAST, "SELECT id, weight, text FROM autobroadcast WHERE realmid = ? OR realmid = -1", CONNECTION_SYNCH); + PrepareStatement(LOGIN_GET_EMAIL_BY_ID, "SELECT email FROM account WHERE id = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_ACCOUNT_ACCESS_BY_ID, "SELECT gmlevel, RealmID FROM account_access WHERE id = ? and (RealmID = ? OR RealmID = -1) ORDER BY gmlevel desc", CONNECTION_SYNCH); diff --git a/src/server/shared/Database/Implementation/LoginDatabase.h b/src/server/shared/Database/Implementation/LoginDatabase.h index 97cf91fc178..47fa48c6ada 100644 --- a/src/server/shared/Database/Implementation/LoginDatabase.h +++ b/src/server/shared/Database/Implementation/LoginDatabase.h @@ -84,6 +84,8 @@ enum LoginDatabaseStatements LOGIN_INS_LOG, LOGIN_UPD_USERNAME, LOGIN_UPD_PASSWORD, + LOGIN_UPD_EMAIL, + LOGIN_UPD_REG_EMAIL, LOGIN_UPD_MUTE_TIME, LOGIN_UPD_MUTE_TIME_LOGIN, LOGIN_UPD_LAST_IP, @@ -112,6 +114,7 @@ enum LoginDatabaseStatements LOGIN_DEL_ACCOUNT, LOGIN_SEL_IP2NATION_COUNTRY, LOGIN_SEL_AUTOBROADCAST, + LOGIN_GET_EMAIL_BY_ID, LOGIN_SEL_ACCOUNT_ACCESS_BY_ID, LOGIN_SEL_RBAC_ACCOUNT_GROUPS, diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index a98a4e61448..018cd309805 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -1132,6 +1132,15 @@ AccountInstancesPerHour = 5 RBAC.DefaultGroups = "" # +# Account.PasswordChangeSecurity +# Description: Controls how secure you want the password changes to be. +# Default: 2 - RBAC (RBAC en- or disables Email confirmation per group) +# 1 - Email (Email confirmation necessary) +# 0 - None (Just Old and new password comparision. Default.) + +Account.PasswordChangeSecurity = 0 + +# ################################################################################################### ################################################################################################### @@ -2829,4 +2838,4 @@ PacketSpoof.BanMode = 0 PacketSpoof.BanDuration = 86400 # -###################################################################################################
\ No newline at end of file +################################################################################################### |