Second+third review pass (2026-07-10/11): security and completeness fixes, public-release polish
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
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());
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# ============================================
|
||||
|
||||
# Language setting (available: en_US, pt_PT)
|
||||
language: en_US
|
||||
language: pt_PT
|
||||
|
||||
# Database Configuration
|
||||
database:
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user