Second+third review pass (2026-07-10/11): security and completeness fixes, public-release polish

This commit is contained in:
2026-07-11 13:59:33 +01:00
parent fe8dc78d0f
commit bc6e2296c6
34 changed files with 226 additions and 203 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
@@ -124,8 +124,11 @@ public class ConfigManager {
// Economy settings
currencyFormat = config.getString("economy.currency-format", "$#,##0.00");
currencySymbol = config.getString("economy.currency-symbol", "$");
marketTax = config.getDouble("economy.taxes.market-tax", 5.0);
auctionTax = config.getDouble("economy.taxes.auction-tax", 7.5);
// Clamp taxes to [0, 100]. A tax > 100 would make seller earnings negative
// (price * (1 - tax/100)) which the deposit path would then mint/refuse - a config
// mistake must never be able to create or destroy money.
marketTax = clampPercent(config.getDouble("economy.taxes.market-tax", 5.0));
auctionTax = clampPercent(config.getDouble("economy.taxes.auction-tax", 7.5));
// Market settings
maxListingsPerPlayer = config.getInt("market.max-listings-per-player", 20);
@@ -272,6 +275,23 @@ public class ConfigManager {
return blacklistedMaterials.contains(material);
}
/**
* Clamps a percentage value to the valid [0, 100] range.
* Non-finite values (NaN/Infinity from a malformed config) fall back to 0.
*/
private double clampPercent(double value) {
if (Double.isNaN(value) || Double.isInfinite(value)) {
plugin.getLogger().warning("Invalid tax percentage in config; defaulting to 0.");
return 0.0;
}
if (value < 0.0) return 0.0;
if (value > 100.0) {
plugin.getLogger().warning("Tax percentage " + value + " exceeds 100; clamping to 100.");
return 100.0;
}
return value;
}
/**
* Checks if text contains blacklisted keywords
*/
@@ -36,8 +36,8 @@ public class MessageManager {
public void reload() {
messageCache.clear();
// Get language from config
currentLanguage = plugin.getConfig().getString("language", "en_US");
// Get language from config (default pt_PT; en_US remains bundled as a fallback)
currentLanguage = plugin.getConfig().getString("language", "pt_PT");
// Save default language files
saveDefaultLanguageFiles();
@@ -349,25 +349,45 @@ public class CreateAuctionGui implements MarketGui {
// Create the auction
ItemStack auctionItem = selectedItem.clone();
int amount = auctionItem.getAmount();
// From here an async creation is in flight; block further confirms until it finishes.
processing = true;
// Reserve the items NOW, atomically on the main thread, BEFORE the async DB write.
// Removing after the async round-trip is a dupe window: during the round-trip the
// player could drop/stash/hand off the items (the auction is built from a clone and
// does not need the live items), leaving them with both the items AND an auction that
// delivers the item to the winner. Remove-first + refund-on-failure closes that window.
if (!InventoryUtil.removeItem(player, auctionItem, amount)) {
processing = false;
player.sendMessage(msgManager.getPrefixed("messages.item-no-longer-available"));
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.AUCTION).open(player);
return;
}
plugin.getTransactionService().createAuctionTransaction(
player, auctionItem, startPrice, buyoutPrice, durationHours
).thenAccept(result -> {
Bukkit.getScheduler().runTask(plugin, () -> {
processing = false;
if (result.isSuccess()) {
// Remove items from inventory AFTER successful creation
InventoryUtil.removeItem(player, auctionItem, auctionItem.getAmount());
player.sendMessage(msgManager.getPrefixed("messages.auction-created",
"id", String.valueOf(result.getId())));
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
player.closeInventory();
guiManager.openMainMenu(player);
} else {
// Creation failed AFTER we removed the items - give them back so nothing is lost.
ItemStack refund = auctionItem.clone();
refund.setAmount(amount);
ItemStack leftover = InventoryUtil.giveItem(player, refund);
if (leftover != null) {
plugin.getClaimService().addClaimItem(player.getUniqueId(), leftover,
pt.henrique.communityMarket.model.ClaimItem.ClaimReason.ADMIN_RETURN,
"Auction creation failed");
}
player.sendMessage(msgManager.getPrefixed("messages." + result.getErrorKey()));
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
}
@@ -298,21 +298,41 @@ public class CreateListingGui implements MarketGui {
// From here an async creation is in flight; block further confirms until it finishes.
processing = true;
// Reserve the items NOW, atomically on the main thread, BEFORE the async DB write.
// Removing after the async round-trip is a dupe window: during the round-trip the
// player could drop/stash/hand off the items (the listing is built from a clone and
// does not need the live items), leaving them with both the items AND a listing that
// pays out. Remove-first + refund-on-failure closes that window.
if (!InventoryUtil.removeItem(player, listItem, amount)) {
// Couldn't take the full amount (item moved between the check and here) - abort.
processing = false;
player.sendMessage(msgManager.getPrefixed("messages.item-no-longer-available"));
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.LISTING).open(player);
return;
}
plugin.getTransactionService().createListingTransaction(
player, listItem, amount, price, durationHours
).thenAccept(result -> {
Bukkit.getScheduler().runTask(plugin, () -> {
processing = false;
if (result.isSuccess()) {
// Remove items from inventory AFTER successful creation
InventoryUtil.removeItem(player, listItem, amount);
player.sendMessage(msgManager.getPrefixed("messages.listing-created",
"id", String.valueOf(result.getId())));
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
player.closeInventory();
guiManager.openMainMenu(player);
} else {
// Creation failed AFTER we removed the items - give them back so nothing is lost.
ItemStack refund = listItem.clone();
refund.setAmount(amount);
ItemStack leftover = InventoryUtil.giveItem(player, refund);
if (leftover != null) {
plugin.getClaimService().addClaimItem(player.getUniqueId(), leftover,
pt.henrique.communityMarket.model.ClaimItem.ClaimReason.ADMIN_RETURN,
"Listing creation failed");
}
player.sendMessage(msgManager.getPrefixed("messages." + result.getErrorKey()));
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
}
@@ -48,111 +48,24 @@ public class HelpGui implements MarketGui {
inventory.setItem(i, filler);
}
// Help content
// Help content (localized). Rendered as the book's lore so the Help screen
// respects the configured language instead of showing hardcoded English.
List<String> helpContent = msgManager.getList("help.content");
// Main help book
inventory.setItem(4, new ItemBuilder(Material.WRITTEN_BOOK)
// Main help book with the localized explanation of every feature
inventory.setItem(22, new ItemBuilder(Material.WRITTEN_BOOK)
.name(msgManager.getRaw("help.title"))
.lore(helpContent)
.build());
// Feature explanations
inventory.setItem(19, new ItemBuilder(Material.CHEST)
.name("&aBrowse Market")
.lore(
"&7View all fixed-price listings",
"&7from other players.",
"",
"&7Click on items to purchase them."
)
.build());
inventory.setItem(20, new ItemBuilder(Material.GOLD_INGOT)
.name("&6Browse Auctions")
.lore(
"&7View all active auctions.",
"",
"&eLeft-click &7to place a bid",
"&eRight-click &7to buyout (if available)"
)
.build());
inventory.setItem(21, new ItemBuilder(Material.WRITABLE_BOOK)
.name("&eCreate Listing")
.lore(
"&7Sell items at a fixed price.",
"",
"&71. Place your item in the slot",
"&72. Set the price",
"&73. Choose duration",
"&74. Click confirm"
)
.build());
inventory.setItem(22, new ItemBuilder(Material.GOLDEN_HELMET)
.name("&eCreate Auction")
.lore(
"&7Auction items to the highest bidder.",
"",
"&71. Place your item in the slot",
"&72. Set starting price",
"&73. Optionally set buyout",
"&74. Choose duration",
"&75. Click confirm"
)
.build());
inventory.setItem(23, new ItemBuilder(Material.BOOK)
.name("&bMy Listings")
.lore(
"&7View your active listings.",
"",
"&7Click on a listing to cancel it.",
"&7Cancelled items go to claim storage."
)
.build());
inventory.setItem(24, new ItemBuilder(Material.CLOCK)
.name("&bMy Auctions")
.lore(
"&7View your active auctions.",
"",
"&7You can only cancel auctions",
"&7that have no bids yet."
)
.build());
inventory.setItem(25, new ItemBuilder(Material.ENDER_CHEST)
.name("&dClaim Items")
.lore(
"&7Collect items waiting for you:",
"",
"&7- Expired listings",
"&7- Cancelled listings",
"&7- Won auctions",
"&7- Auction refunds"
)
.build());
inventory.setItem(31, new ItemBuilder(Material.EMERALD)
.name("&aEarnings")
.lore(
"&7Withdraw money from sales.",
"",
"&7When you sell something, the money",
"&7goes to pending earnings first.",
"&7Withdraw it here."
)
.build());
// Tax info
// Tax info (values are dynamic; labels come from the language file)
inventory.setItem(40, new ItemBuilder(Material.GOLD_NUGGET)
.name("&6Tax Information")
.name(msgManager.getRaw("help.tax-title"))
.lore(
"&7Market Tax: &f" + plugin.getConfigManager().getMarketTax() + "%",
"&7Auction Tax: &f" + plugin.getConfigManager().getAuctionTax() + "%",
msgManager.getRaw("help.tax-market").replace("{tax}", String.valueOf(plugin.getConfigManager().getMarketTax())),
msgManager.getRaw("help.tax-auction").replace("{tax}", String.valueOf(plugin.getConfigManager().getAuctionTax())),
"",
"&7Taxes are deducted from seller earnings."
msgManager.getRaw("help.tax-note")
)
.build());
+1 -1
View File
@@ -4,7 +4,7 @@
# ============================================
# Language setting (available: en_US, pt_PT)
language: en_US
language: pt_PT
# Database Configuration
database:
+8
View File
@@ -45,12 +45,16 @@ messages:
claim-empty: "&eYou have no items to claim."
claim-inventory-full: "&cYour inventory is full! Please make space."
claim-all-success: "&aClaimed {count} items!"
claim-items: "&eYou have &6{count}&e item(s) waiting to be claimed. Use /market to claim them."
# Earnings Messages
earnings-withdrawn: "&aWithdrew {amount}! New balance: {balance}"
earnings-empty: "&eYou have no pending earnings."
earnings-balance: "&aYour pending earnings: {amount}"
# Generic
failed: "&cSomething went wrong. Please try again."
# Item Validation
invalid-item: "&cPlease select a valid item."
item-no-longer-available: "&cThe selected item is no longer in your inventory."
@@ -445,4 +449,8 @@ help:
- ""
- "&7&oTip: All actions are done through GUIs!"
- "&7&oJust click on buttons to navigate."
tax-title: "&6Tax Information"
tax-market: "&7Market Tax: &f{tax}%"
tax-auction: "&7Auction Tax: &f{tax}%"
tax-note: "&7Taxes are deducted from seller earnings."
+8
View File
@@ -45,12 +45,16 @@ messages:
claim-empty: "&eNão tens itens para reclamar."
claim-inventory-full: "&cO teu inventário está cheio! Por favor liberta espaço."
claim-all-success: "&aReclamaste {count} itens!"
claim-items: "&eTens &6{count}&e item(ns) por reclamar. Usa /market para os reclamar."
# Mensagens de Ganhos
earnings-withdrawn: "&aLevantaste {amount}! Novo saldo: {balance}"
earnings-empty: "&eNão tens ganhos pendentes."
earnings-balance: "&aOs teus ganhos pendentes: {amount}"
# Genérico
failed: "&cAlgo correu mal. Tenta novamente."
# Validação de Itens
invalid-item: "&cPor favor seleciona um item válido."
item-no-longer-available: "&cO item selecionado já não está no teu inventário."
@@ -445,4 +449,8 @@ help:
- ""
- "&7&oDica: Todas as ações são feitas através de GUIs!"
- "&7&oBasta clicar nos botões para navegar."
tax-title: "&6Informação de Taxas"
tax-market: "&7Taxa do Mercado: &f{tax}%"
tax-auction: "&7Taxa dos Leilões: &f{tax}%"
tax-note: "&7As taxas são deduzidas dos ganhos do vendedor."
+2 -18
View File
@@ -4,7 +4,7 @@ main: pt.henrique.communityMarket.CommunityMarket
api-version: '1.21'
description: A GUI-only marketplace plugin for fixed-price listings and auctions
author: Henrique
website: https://github.com/henrique/CommunityMarket
website: https://github.com/henriquescrrrr/carrageis-communitymarket
# Soft dependencies - plugin will detect and use these if available
softdepend:
@@ -63,21 +63,5 @@ permissions:
default: true
communitymarket.admin:
description: Allows access to admin functions
default: op
children:
communitymarket.admin.viewall: true
communitymarket.admin.remove: true
communitymarket.admin.reload: true
communitymarket.admin.viewall:
description: Allows viewing all listings/auctions
default: op
communitymarket.admin.remove:
description: Allows removing any listing or auction
default: op
communitymarket.admin.reload:
description: Allows reloading configuration
description: Allows access to admin functions (view all, remove listings, reload)
default: op