commit 9a79df540e2e66730fcd996d7a83bb800c049f80 Author: Trent Hensler Date: Wed Feb 6 18:46:08 2013 -0600 Abrakadabra! diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ebd21a --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Rr]elease/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.vspscc +.builds +*.dotCover + +## TODO: If you have NuGet Package Restore enabled, uncomment this +#packages/ + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp + +# ReSharper is a .NET coding add-in +_ReSharper* + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Others +[Bb]in +[Oo]bj +sql +TestResults +*.Cache +ClientBin +stylecop.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML + + + +############ +## Windows +############ + +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +# Mac crap +.DS_Store diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..3feb460 --- /dev/null +++ b/config.yml @@ -0,0 +1,6 @@ +# PlayerVaults by drsthock +check-update: true +debug-mode: false +disabled-worlds: + - YourAwesomeWorld + - Toonville \ No newline at end of file diff --git a/lang.yml b/lang.yml new file mode 100644 index 0000000..c2a17db --- /dev/null +++ b/lang.yml @@ -0,0 +1,10 @@ +# Use & for color codes. +# %p is where the player name will get inserted. +# %v is where the vault number will get inserted. +# Made with love :) +title-name: "&4[&fPlayerVaults&4]" +open-vault: "&fOpening vault &a%v" +open-other-vault: "&fOpening vault &a%v &fof &a%p" +invalid-args: "&fInvalid args!" +delete-other-vault: "&fDeleted vault &a%v &fof &a%p" +delete-vault: "&fDeleted vault &a%v" \ No newline at end of file diff --git a/plugin.yml b/plugin.yml new file mode 100644 index 0000000..a0a1dc6 --- /dev/null +++ b/plugin.yml @@ -0,0 +1,12 @@ +name: PlayerVaults +main: me.shock.playervaults.Main +authors: [drtshock] +version: 1.1.4 +website: www.dev.bukkit.org/server-mods/PlayerVaults +commands: + pv: + description: Open a vault with /vault + aliases: [vault, playervault, playervaults] +permissions: + playervaults.amount.9: + default: op \ No newline at end of file diff --git a/src/me/shock/playervaults/Commands.java b/src/me/shock/playervaults/Commands.java new file mode 100644 index 0000000..6073515 --- /dev/null +++ b/src/me/shock/playervaults/Commands.java @@ -0,0 +1,197 @@ +package me.shock.playervaults; + +import java.io.IOException; +import java.util.HashMap; + +import me.shock.playervaults.util.Config; +import me.shock.playervaults.util.VaultManager; + +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class Commands implements CommandExecutor +{ + + Main plugin; + Config config = new Config(); + private VaultManager vm = new VaultManager(); + String pv = ChatColor.DARK_RED + "[" + ChatColor.WHITE + "PlayerVaults" + + ChatColor.DARK_RED + "]" + ChatColor.WHITE + ": "; + + public HashMap inVault = new HashMap(); + + + public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) + { + if (cmd.getName().equalsIgnoreCase("vault")) + { + if (!(sender instanceof Player)) + { + sender.sendMessage("[PlayerVaults] Sorry but the console can't have a vault :("); + return true; + } + + if (args.length == 1) + { + if (args[0].matches("[1-9]")) + { + int number = Integer.parseInt(args[0]); + if ((number <= 9) && (sender.hasPermission("playervaults.amount.9"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number <= 8) && (sender.hasPermission("playervaults.amount.8"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number <= 7) && (sender.hasPermission("playervaults.amount.7"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number <= 6) && (sender.hasPermission("playervaults.amount.6"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number <= 5) && (sender.hasPermission("playervaults.amount.5"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number <= 4) && (sender.hasPermission("playervaults.amount.4"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number <= 3) && (sender.hasPermission("playervaults.amount.3"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number <= 2) && (sender.hasPermission("playervaults.amount.2"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + if ((number == 1) && (sender.hasPermission("playervaults.amount.1"))) + { + vm.loadVault(sender, sender.getName(), number); + sender.sendMessage(pv + "Opening vault " + ChatColor.GREEN + args[0]); + return true; + } + + sender.sendMessage(pv + "You don't have permission for that many vaults!"); + return true; + } + + showHelp(sender); + return true; + } + + if (args.length == 2) + { + if (args[0].equals("delete")) + { + if (sender.hasPermission("playervaults.delete")) + { + if (args[1].matches("[1-9]")) + { + int number = Integer.parseInt(args[1]); + try { + vm.deleteVault(sender, sender.getName(), number); + } catch (IOException e) { + e.printStackTrace(); + } + sender.sendMessage(pv + "Deleted vault " + ChatColor.GREEN + args[1]); + return true; + } + } + else + { + sender.sendMessage(pv + "You don't have permission for "); + return true; + } + + } + + else + { + if (!sender.hasPermission("playervaults.admin")) + { + sender.sendMessage(pv + "You don't have permission for "); + return true; + } + if (args[1].matches("[1-9]")) + { + int number = Integer.parseInt(args[2]); + vm.loadVault(sender, args[1].toLowerCase(), number); + sender.sendMessage(pv + "Opened vault " + ChatColor.GREEN + args[1] + ChatColor.WHITE + " for " + + ChatColor.GREEN + args[0]); + return true; + } + + sender.sendMessage(pv + "Chest number must be 1-9."); + return true; + } + + sender.sendMessage(pv + "We have no record of that vault."); + return true; + } + + if (args.length > 1) + { + if (args[0].equalsIgnoreCase("delete")) + { + if (sender.hasPermission("playervaults.admin")) + { + if (args[2].matches("[1-9]")) + { + Integer number = Integer.parseInt(args[2]); + try { + vm.deleteVault(sender, sender.getName(), number); + } catch (IOException e) { + e.printStackTrace(); + } + sender.sendMessage(pv + "Deleted vault " + ChatColor.RED + args[2] + ChatColor.WHITE + + " for " + ChatColor.RED + args[1]); + return true; + } + } + else + { + sender.sendMessage(pv + "You don't have permission for "); + return true; + } + } + } + else + { + showHelp(sender); + return true; + } + + } + + return true; + } + + public void showHelp(CommandSender sender) + { + sender.sendMessage(pv + "/vault "); + sender.sendMessage(pv + "/vault delete "); + } +} \ No newline at end of file diff --git a/src/me/shock/playervaults/Listeners.java b/src/me/shock/playervaults/Listeners.java new file mode 100644 index 0000000..be87ba4 --- /dev/null +++ b/src/me/shock/playervaults/Listeners.java @@ -0,0 +1,177 @@ +package me.shock.playervaults; + +import java.io.IOException; + +import me.shock.playervaults.util.Config; +import me.shock.playervaults.util.VaultManager; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.Inventory; + +public class Listeners implements Listener +{ + + public Main plugin; + public Listeners(Main instance) + { + this.plugin = instance; + } + VaultManager vm = new VaultManager(); + Config config = new Config(); + Commands commands = new Commands(); + + + + @EventHandler + public void onQuit(PlayerQuitEvent event) + { + if(commands.inVault.containsKey(event.getPlayer().getName())) { + Player player = event.getPlayer(); + Inventory inv = player.getOpenInventory().getTopInventory(); + int number = Integer.parseInt(commands.inVault.get(player.getName())); + try { + vm.saveVault(inv, player, number); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + @EventHandler + public void onJoin(PlayerJoinEvent event) + { + Player player = event.getPlayer(); + vm.checkFile(player); + if(player.isOp() && Main.update) + { + player.sendMessage(ChatColor.GREEN + "Version " + Main.name + " of PlayerVaults is up for download!"); + player.sendMessage(ChatColor.GREEN + "http://dev.bukkit.org/server-mods/playervaults to view the changelog and download!"); + } + } + @EventHandler + public void onDeath(PlayerDeathEvent event) { + if(commands.inVault.containsKey(event.getEntity().getName())) { + Player player = event.getEntity(); + Inventory inv = player.getOpenInventory().getTopInventory(); + int number = Integer.parseInt(commands.inVault.get(player.getName())); + try { + vm.saveVault(inv, player, number); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @EventHandler + public void onTP(PlayerTeleportEvent event) { + if(commands.inVault.containsKey(event.getPlayer().getName())) { + Player player = event.getPlayer(); + Inventory inv = player.getOpenInventory().getTopInventory(); + int number = Integer.parseInt(commands.inVault.get(player.getName())); + try { + vm.saveVault(inv, player, number); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @EventHandler + public void onWorldChange(PlayerChangedWorldEvent event) { + if(commands.inVault.containsKey(event.getPlayer().getName())) { + Player player = event.getPlayer(); + Inventory inv = player.getOpenInventory().getTopInventory(); + int number = Integer.parseInt(commands.inVault.get(player.getName())); + try { + vm.saveVault(inv, player, number); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @EventHandler + public void onClose(InventoryCloseEvent event) { + if(commands.inVault.containsKey(event.getPlayer().getName())) { + HumanEntity he = event.getPlayer(); + if(he instanceof Player) + { + Player player = (Player) he; + Inventory inv = player.getOpenInventory().getTopInventory(); + int number = Integer.parseInt(commands.inVault.get(player.getName())); + try { + vm.saveVault(inv, player, number); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Check if a player is trying to do something while + * in a vault. + * Don't let them open up another chest. + * @param event + */ + @EventHandler + public void onInteract(PlayerInteractEvent event) + { + Player player = event.getPlayer(); + if(commands.inVault.containsKey(player.getName()) && event.getAction() == Action.RIGHT_CLICK_BLOCK) + { + Block block = event.getClickedBlock(); + + /** + * Different inventories that + * we don't want the player to open. + */ + if(block.getType() == Material.CHEST + || block.getType() == Material.ENDER_CHEST + || block.getType() == Material.FURNACE + /** + * Storage_minecart and Powered minecart aren't blocks ;)- added to EntityInteractEvent + */ + || block.getType() == Material.BURNING_FURNACE + //|| block.getType() == Material.STORAGE_MINECART + //|| block.getType() == Material.MINECART + //|| block.getType() == Material.POWERED_MINECART + || block.getType() == Material.BREWING_STAND + || block.getType() == Material.BEACON) + { + event.setCancelled(true); + } + } + } + + /** + * Don't let a player open a trading inventory OR a minecart + * while he has his vault open. + * @param event + */ + @EventHandler + public void onInteractEntity(PlayerInteractEntityEvent event) + { + Player player = event.getPlayer(); + EntityType type = event.getRightClicked().getType(); + if((type == EntityType.VILLAGER||type==EntityType.MINECART) && commands.inVault.containsKey(player.getName())) + { + event.setCancelled(true); + } + } +} diff --git a/src/me/shock/playervaults/Main.java b/src/me/shock/playervaults/Main.java new file mode 100644 index 0000000..6ae07f4 --- /dev/null +++ b/src/me/shock/playervaults/Main.java @@ -0,0 +1,61 @@ +package me.shock.playervaults; + + +import java.io.IOException; +import java.util.logging.Logger; + +import me.shock.playervaults.Listeners; +import me.shock.playervaults.util.Config; +import me.shock.playervaults.util.Metrics; +import me.shock.playervaults.util.Updater; + +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; + + +public class Main extends JavaPlugin { + + public Main plugin; + public Logger log; + Config config = new Config(); + public static boolean update = false; + public static String name = ""; + + public void onEnable() + { + log = getServer().getLogger(); + PluginManager pm = getServer().getPluginManager(); + pm.registerEvents(new Listeners(this), this); + getCommand("pv").setExecutor(new Commands()); + config.loadConfig(); + config.loadLang(); + startMetrics(); + + if(config.updateCheck()) + { + Updater updater = new Updater(this, "playervaults", this.getFile(), Updater.UpdateType.NO_DOWNLOAD, false); + update = updater.getResult() == Updater.UpdateResult.UPDATE_AVAILABLE; + name = updater.getLatestVersionString(); + } + } + + public void onDisable() + { + //saveData(); + } + + + public void startMetrics() + { + try + { + Metrics metrics = new Metrics(this); + metrics.start(); + } + catch (IOException localIOException) + { + localIOException.printStackTrace(); + } + } + +} diff --git a/src/me/shock/playervaults/util/Config.java b/src/me/shock/playervaults/util/Config.java new file mode 100644 index 0000000..9fb5a60 --- /dev/null +++ b/src/me/shock/playervaults/util/Config.java @@ -0,0 +1,191 @@ +package me.shock.playervaults.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.configuration.file.YamlConfiguration; + +import me.shock.playervaults.Main; + +public class Config +{ + + private Main plugin; + + public void loadConfig() + { + Logger log = plugin.getServer().getLogger(); + + /** + * Check to see if there's a config. + * If not then create a new one. + */ + File config = new File(plugin.getDataFolder() + "/config.yml"); + if(!config.exists()) + { + try{ + plugin.getDataFolder().mkdir(); + config.createNewFile(); + } catch (IOException e) { + log.log(Level.SEVERE, "[PlayerVaults] Couldn't create config"); + } + /** + * Write the config file here. + * New, genius way to write it :) + */ + try { + FileOutputStream fos = new FileOutputStream(new File(plugin.getDataFolder() + File.separator + "config.yml")); + InputStream is = plugin.getResource("config.yml"); + byte[] linebuffer = new byte[4096]; + int lineLength = 0; + while((lineLength = is.read(linebuffer)) > 0) + { + fos.write(linebuffer, 0, lineLength); + } + fos.close(); + + log.log(Level.INFO, "[PlayerVaults] Wrote new config"); + + } catch (IOException e) { + log.log(Level.SEVERE, "[PlayerVaults] Couldn't write config: " + e); + } + } + else + { + log.log(Level.INFO, "[PlayerVaults] Config found."); + } + } + + public void loadLang() + { + Logger log = plugin.getServer().getLogger(); + + /** + * Check to see if there's a config. + * If not then create a new one. + */ + File config = new File(plugin.getDataFolder() + "/lang.yml"); + if(!config.exists()) + { + try{ + plugin.getDataFolder().mkdir(); + config.createNewFile(); + } catch (IOException e) { + log.log(Level.SEVERE, "[PlayerVaults] Couldn't create language file."); + } + /** + * Write the config file here. + * New, genius way to write it :) + */ + try { + FileOutputStream fos = new FileOutputStream(new File(plugin.getDataFolder() + File.separator + "config.yml")); + InputStream is = plugin.getResource("lang.yml"); + byte[] linebuffer = new byte[4096]; + int lineLength = 0; + while((lineLength = is.read(linebuffer)) > 0) + { + fos.write(linebuffer, 0, lineLength); + } + fos.close(); + + log.log(Level.INFO, "[PlayerVaults] Wrote new language file"); + + } catch (IOException e) { + log.log(Level.SEVERE, "[PlayerVaults] Couldn't write Language file: " + e); + } + } + else + { + log.log(Level.INFO, "[PlayerVaults] Language file found."); + } + } + + private YamlConfiguration lang() { + File file = new File(plugin.getDataFolder() + "lang.yml"); + YamlConfiguration lang = YamlConfiguration.loadConfiguration(file); + return lang; + } + + /** + * Methods to get values from the config. + * public so any class / plugin can get them. + */ + + /** + * + * @return updateCheck + */ + public boolean updateCheck() { + return plugin.getConfig().getBoolean("check-update"); + } + + public boolean debugMode() { + return plugin.getConfig().getBoolean("debug-mode"); + } + + /** + * + * @return disabled worlds. + */ + public List disabledWorlds() { + return plugin.getConfig().getList("disabled-worlds"); + } + + /** + * Values for the lang.yml + */ + + /** + * + * @return title used in all messages. + */ + public String title() { + return lang().getString("title-name"); + } + + /** + * + * @return string for opening vault. + */ + public String openVault() { + return lang().getString("open-vault"); + } + + /** + * + * @return string for opening someone else's vault. + */ + public String openOtherVault() { + return lang().getString("open-other-vault"); + } + + /** + * + * @return string for invalid args. + */ + public String invalidArgs() { + return lang().getString("invalid-args"); + } + + /** + * + * @return string for deleting a vault. + */ + public String deleteVault() { + return lang().getString("delete-vault"); + } + + /** + * + * @return string for deleting someone else's vault. + */ + public String deleteOtherVault() { + return lang().getString("delete-other-vault"); + } + +} \ No newline at end of file diff --git a/src/me/shock/playervaults/util/Metrics.java b/src/me/shock/playervaults/util/Metrics.java new file mode 100644 index 0000000..fa83065 --- /dev/null +++ b/src/me/shock/playervaults/util/Metrics.java @@ -0,0 +1,634 @@ +package me.shock.playervaults.util; + +/* + * Copyright 2011 Tyler Blair. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and contributors and should not be interpreted as representing official policies, + * either expressed or implied, of anybody else. + */ + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; + +/** + *

+ * The metrics class obtains data about a plugin and submits statistics about it to the metrics backend. + *

+ *

+ * Public methods provided by this class: + *

+ * + * Graph createGraph(String name);
+ * void addCustomData(Metrics.Plotter plotter);
+ * void start();
+ *
+ */ +public class Metrics { + + /** + * The current revision number + */ + private final static int REVISION = 5; + + /** + * The base url of the metrics domain + */ + private static final String BASE_URL = "http://mcstats.org"; + + /** + * The url used to report a server's status + */ + private static final String REPORT_URL = "/report/%s"; + + /** + * The separator to use for custom data. This MUST NOT change unless you are hosting your own + * version of metrics and want to change it. + */ + private static final String CUSTOM_DATA_SEPARATOR = "~~"; + + /** + * Interval of time to ping (in minutes) + */ + private static final int PING_INTERVAL = 10; + + /** + * The plugin this metrics submits for + */ + private final Plugin plugin; + + /** + * All of the custom graphs to submit to metrics + */ + private final Set graphs = Collections.synchronizedSet(new HashSet()); + + /** + * The default graph, used for addCustomData when you don't want a specific graph + */ + private final Graph defaultGraph = new Graph("Default"); + + /** + * The plugin configuration file + */ + private final YamlConfiguration configuration; + + /** + * The plugin configuration file + */ + private final File configurationFile; + + /** + * Unique server id + */ + private final String guid; + + /** + * Lock for synchronization + */ + private final Object optOutLock = new Object(); + + /** + * Id of the scheduled task + */ + private volatile int taskId = -1; + + public Metrics(final Plugin plugin) throws IOException { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null"); + } + + this.plugin = plugin; + + // load the config + configurationFile = getConfigFile(); + configuration = YamlConfiguration.loadConfiguration(configurationFile); + + // add some defaults + configuration.addDefault("opt-out", false); + configuration.addDefault("guid", UUID.randomUUID().toString()); + + // Do we need to create the file? + if (configuration.get("guid", null) == null) { + configuration.options().header("http://mcstats.org").copyDefaults(true); + configuration.save(configurationFile); + } + + // Load the guid then + guid = configuration.getString("guid"); + } + + /** + * Construct and create a Graph that can be used to separate specific plotters to their own graphs + * on the metrics website. Plotters can be added to the graph object returned. + * + * @param name The name of the graph + * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given + */ + public Graph createGraph(final String name) { + if (name == null) { + throw new IllegalArgumentException("Graph name cannot be null"); + } + + // Construct the graph object + final Graph graph = new Graph(name); + + // Now we can add our graph + graphs.add(graph); + + // and return back + return graph; + } + + /** + * Add a Graph object to Metrics that represents data for the plugin that should be sent to the backend + * + * @param graph The name of the graph + */ + public void addGraph(final Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + + graphs.add(graph); + } + + /** + * Adds a custom data plotter to the default graph + * + * @param plotter The plotter to use to plot custom data + */ + public void addCustomData(final Plotter plotter) { + if (plotter == null) { + throw new IllegalArgumentException("Plotter cannot be null"); + } + + // Add the plotter to the graph o/ + defaultGraph.addPlotter(plotter); + + // Ensure the default graph is included in the submitted graphs + graphs.add(defaultGraph); + } + + /** + * Start measuring statistics. This will immediately create an async repeating task as the plugin and send + * the initial data to the metrics backend, and then after that it will post in increments of + * PING_INTERVAL * 1200 ticks. + * + * @return True if statistics measuring is running, otherwise false. + */ + @SuppressWarnings("deprecation") + public boolean start() { + synchronized (optOutLock) { + // Did we opt out? + if (isOptOut()) { + return false; + } + + // Is metrics already running? + if (taskId >= 0) { + return true; + } + + // Begin hitting the server with glorious data + taskId = plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(plugin, new Runnable() { + + private boolean firstPost = true; + + public void run() { + try { + // This has to be synchronized or it can collide with the disable method. + synchronized (optOutLock) { + // Disable Task, if it is running and the server owner decided to opt-out + if (isOptOut() && taskId > 0) { + plugin.getServer().getScheduler().cancelTask(taskId); + taskId = -1; + // Tell all plotters to stop gathering information. + for (Graph graph : graphs){ + graph.onOptOut(); + } + } + } + + // We use the inverse of firstPost because if it is the first time we are posting, + // it is not a interval ping, so it evaluates to FALSE + // Each time thereafter it will evaluate to TRUE, i.e PING! + postPlugin(!firstPost); + + // After the first post we set firstPost to false + // Each post thereafter will be a ping + firstPost = false; + } catch (IOException e) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); + } + } + }, 0, PING_INTERVAL * 1200); + + return true; + } + } + + /** + * Has the server owner denied plugin metrics? + * + * @return true if metrics should be opted out of it + */ + public boolean isOptOut() { + synchronized(optOutLock) { + try { + // Reload the metrics file + configuration.load(getConfigFile()); + } catch (IOException ex) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + return true; + } catch (InvalidConfigurationException ex) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + return true; + } + return configuration.getBoolean("opt-out", false); + } + } + + /** + * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. + * + * @throws IOException + */ + public void enable() throws IOException { + // This has to be synchronized or it can collide with the check in the task. + synchronized (optOutLock) { + // Check if the server owner has already set opt-out, if not, set it. + if (isOptOut()) { + configuration.set("opt-out", false); + configuration.save(configurationFile); + } + + // Enable Task, if it is not running + if (taskId < 0) { + start(); + } + } + } + + /** + * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task. + * + * @throws IOException + */ + public void disable() throws IOException { + // This has to be synchronized or it can collide with the check in the task. + synchronized (optOutLock) { + // Check if the server owner has already set opt-out, if not, set it. + if (!isOptOut()) { + configuration.set("opt-out", true); + configuration.save(configurationFile); + } + + // Disable Task, if it is running + if (taskId > 0) { + this.plugin.getServer().getScheduler().cancelTask(taskId); + taskId = -1; + } + } + } + + /** + * Gets the File object of the config file that should be used to store data such as the GUID and opt-out status + * + * @return the File object for the config file + */ + public File getConfigFile() { + // I believe the easiest way to get the base folder (e.g craftbukkit set via -P) for plugins to use + // is to abuse the plugin object we already have + // plugin.getDataFolder() => base/plugins/PluginA/ + // pluginsFolder => base/plugins/ + // The base is not necessarily relative to the startup directory. + File pluginsFolder = plugin.getDataFolder().getParentFile(); + + // return => base/plugins/PluginMetrics/config.yml + return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml"); + } + + /** + * Generic method that posts a plugin to the metrics website + */ + private void postPlugin(final boolean isPing) throws IOException { + // The plugin's description file containg all of the plugin data such as name, version, author, etc + final PluginDescriptionFile description = plugin.getDescription(); + + // Construct the post data + final StringBuilder data = new StringBuilder(); + data.append(encode("guid")).append('=').append(encode(guid)); + encodeDataPair(data, "version", description.getVersion()); + encodeDataPair(data, "server", Bukkit.getVersion()); + encodeDataPair(data, "players", Integer.toString(Bukkit.getServer().getOnlinePlayers().length)); + encodeDataPair(data, "revision", String.valueOf(REVISION)); + + // If we're pinging, append it + if (isPing) { + encodeDataPair(data, "ping", "true"); + } + + // Acquire a lock on the graphs, which lets us make the assumption we also lock everything + // inside of the graph (e.g plotters) + synchronized (graphs) { + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + final Graph graph = iter.next(); + + for (Plotter plotter : graph.getPlotters()) { + // The key name to send to the metrics server + // The format is C-GRAPHNAME-PLOTTERNAME where separator - is defined at the top + // Legacy (R4) submitters use the format Custom%s, or CustomPLOTTERNAME + final String key = String.format("C%s%s%s%s", CUSTOM_DATA_SEPARATOR, graph.getName(), CUSTOM_DATA_SEPARATOR, plotter.getColumnName()); + + // The value to send, which for the foreseeable future is just the string + // value of plotter.getValue() + final String value = Integer.toString(plotter.getValue()); + + // Add it to the http post data :) + encodeDataPair(data, key, value); + } + } + } + + // Create the url + URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(plugin.getDescription().getName()))); + + // Connect to the website + URLConnection connection; + + // Mineshafter creates a socks proxy, so we can safely bypass it + // It does not reroute POST requests so we need to go around it + if (isMineshafterPresent()) { + connection = url.openConnection(Proxy.NO_PROXY); + } else { + connection = url.openConnection(); + } + + connection.setDoOutput(true); + + // Write the data + final OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); + writer.write(data.toString()); + writer.flush(); + + // Now read the response + final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + final String response = reader.readLine(); + + // close resources + writer.close(); + reader.close(); + + if (response == null || response.startsWith("ERR")) { + throw new IOException(response); //Throw the exception + } else { + // Is this the first update this hour? + if (response.contains("OK This is your first update this hour")) { + synchronized (graphs) { + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + final Graph graph = iter.next(); + + for (Plotter plotter : graph.getPlotters()) { + plotter.reset(); + } + } + } + } + } + } + + /** + * Check if mineshafter is present. If it is, we need to bypass it to send POST requests + * + * @return true if mineshafter is installed on the server + */ + private boolean isMineshafterPresent() { + try { + Class.forName("mineshafter.MineServer"); + return true; + } catch (Exception e) { + return false; + } + } + + /** + *

Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first + * key/value pair MUST be included manually, e.g:

+ * + * StringBuffer data = new StringBuffer(); + * data.append(encode("guid")).append('=').append(encode(guid)); + * encodeDataPair(data, "version", description.getVersion()); + * + * + * @param buffer the stringbuilder to append the data pair onto + * @param key the key value + * @param value the value + */ + private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException { + buffer.append('&').append(encode(key)).append('=').append(encode(value)); + } + + /** + * Encode text as UTF-8 + * + * @param text the text to encode + * @return the encoded text, as UTF-8 + */ + private static String encode(final String text) throws UnsupportedEncodingException { + return URLEncoder.encode(text, "UTF-8"); + } + + /** + * Represents a custom graph on the website + */ + public static class Graph { + + /** + * The graph's name, alphanumeric and spaces only :) + * If it does not comply to the above when submitted, it is rejected + */ + private final String name; + + /** + * The set of plotters that are contained within this graph + */ + private final Set plotters = new LinkedHashSet(); + + private Graph(final String name) { + this.name = name; + } + + /** + * Gets the graph's name + * + * @return the Graph's name + */ + public String getName() { + return name; + } + + /** + * Add a plotter to the graph, which will be used to plot entries + * + * @param plotter the plotter to add to the graph + */ + public void addPlotter(final Plotter plotter) { + plotters.add(plotter); + } + + /** + * Remove a plotter from the graph + * + * @param plotter the plotter to remove from the graph + */ + public void removePlotter(final Plotter plotter) { + plotters.remove(plotter); + } + + /** + * Gets an unmodifiable set of the plotter objects in the graph + * + * @return an unmodifiable {@link Set} of the plotter objects + */ + public Set getPlotters() { + return Collections.unmodifiableSet(plotters); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Graph)) { + return false; + } + + final Graph graph = (Graph) object; + return graph.name.equals(name); + } + + /** + * Called when the server owner decides to opt-out of Metrics while the server is running. + */ + protected void onOptOut() { + } + + } + + /** + * Interface used to collect custom data for a plugin + */ + public static abstract class Plotter { + + /** + * The plot's name + */ + private final String name; + + /** + * Construct a plotter with the default plot name + */ + public Plotter() { + this("Default"); + } + + /** + * Construct a plotter with a specific plot name + * + * @param name the name of the plotter to use, which will show up on the website + */ + public Plotter(final String name) { + this.name = name; + } + + /** + * Get the current value for the plotted point. Since this function defers to an external function + * it may or may not return immediately thus cannot be guaranteed to be thread friendly or safe. + * This function can be called from any thread so care should be taken when accessing resources + * that need to be synchronized. + * + * @return the current value for the point to be plotted. + */ + public abstract int getValue(); + + /** + * Get the column name for the plotted point + * + * @return the plotted point's column name + */ + public String getColumnName() { + return name; + } + + /** + * Called after the website graphs have been updated + */ + public void reset() { + } + + @Override + public int hashCode() { + return getColumnName().hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Plotter)) { + return false; + } + + final Plotter plotter = (Plotter) object; + return plotter.name.equals(name) && plotter.getValue() == getValue(); + } + + } + +} + diff --git a/src/me/shock/playervaults/util/Serialization.java b/src/me/shock/playervaults/util/Serialization.java new file mode 100644 index 0000000..a044486 --- /dev/null +++ b/src/me/shock/playervaults/util/Serialization.java @@ -0,0 +1,77 @@ +package me.shock.playervaults.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; + +import net.minecraft.server.v1_4_R1.NBTBase; +import net.minecraft.server.v1_4_R1.NBTTagCompound; +import net.minecraft.server.v1_4_R1.NBTTagList; + +import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftInventoryCustom; +import org.bukkit.craftbukkit.v1_4_R1.inventory.CraftItemStack; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class Serialization +{ + + public static String toBase64(Inventory inventory) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataOutputStream dataOutput = new DataOutputStream(outputStream); + NBTTagList itemList = new NBTTagList(); + + // Save every element in the list + for (int i = 0; i < inventory.getSize(); i++) + { + NBTTagCompound outputObject = new NBTTagCompound(); + CraftItemStack craft = getCraftVersion(inventory.getItem(i)); + + // Convert the item stack to a NBT compound + if (craft != null) + CraftItemStack.asNMSCopy(craft).save(outputObject); + itemList.add(outputObject); + } + + // Now save the list + NBTBase.a(itemList, dataOutput); + + // Serialize that array + return Base64Coder.encodeLines(outputStream.toByteArray()); + } + + public static Inventory fromBase64(String data) + { + ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data)); + NBTTagList itemList = (NBTTagList) NBTBase.b(new DataInputStream(inputStream)); + Inventory inventory = new CraftInventoryCustom(null, itemList.size()); + + for (int i = 0; i < itemList.size(); i++) + { + NBTTagCompound inputObject = (NBTTagCompound) itemList.get(i); + + if (!inputObject.isEmpty()) + { + inventory.setItem(i, CraftItemStack.asCraftMirror(net.minecraft.server.v1_4_R1.ItemStack.createStack(inputObject))); + } + } + + // Serialize that array + return inventory; + } + + private static CraftItemStack getCraftVersion(ItemStack stack) + { + if (stack instanceof CraftItemStack) + + return (CraftItemStack) stack; + else if (stack != null) + return CraftItemStack.asCraftCopy(stack); + else + return null; + } +} \ No newline at end of file diff --git a/src/me/shock/playervaults/util/Updater.java b/src/me/shock/playervaults/util/Updater.java new file mode 100644 index 0000000..b5ccfa6 --- /dev/null +++ b/src/me/shock/playervaults/util/Updater.java @@ -0,0 +1,615 @@ +package me.shock.playervaults.util; + +/* + * Updater for Bukkit. + * + * This class provides the means to safetly and easily update a plugin, or check to see if it is updated using dev.bukkit.org + */ + +import java.io.*; +import java.lang.Runnable; +import java.lang.Thread; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +/** + * Check dev.bukkit.org to find updates for a given plugin, and download the updates if needed. + *

+ * VERY, VERY IMPORTANT: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating. + *
+ * It is a BUKKIT POLICY that you include a boolean value in your config that prevents the auto-updater from running AT ALL. + *
+ * If you fail to include this option in your config, your plugin will be REJECTED when you attempt to submit it to dev.bukkit.org. + *

+ * An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-updater. + *
+ * If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l + * + * @author H31IX + */ + +public class Updater +{ + private Plugin plugin; + private UpdateType type; + private String versionTitle; + private String versionLink; + private long totalSize; // Holds the total size of the file + //private double downloadedSize; TODO: Holds the number of bytes downloaded + private int sizeLine; // Used for detecting file size + private int multiplier; // Used for determining when to broadcast download updates + private boolean announce; // Whether to announce file downloads + private URL url; // Connecting to RSS + private File file; // The plugin's file + private Thread thread; // Updater thread + private static final String DBOUrl = "http://dev.bukkit.org/server-mods/"; // Slugs will be appended to this to get to the project's RSS feed + private String [] noUpdateTag = {"-DEV","-PRE","-SNAPSHOT"}; // If the version number contains one of these, don't update. + private static final int BYTE_SIZE = 1024; // Used for downloading files + private String updateFolder = YamlConfiguration.loadConfiguration(new File("bukkit.yml")).getString("settings.update-folder"); // The folder that downloads will be placed in + private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; // Used for determining the outcome of the update process + // Strings for reading RSS + private static final String TITLE = "title"; + private static final String LINK = "link"; + private static final String ITEM = "item"; + + /** + * Gives the dev the result of the update process. Can be obtained by called getResult(). + */ + public enum UpdateResult + { + /** + * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads. + */ + SUCCESS, + /** + * The updater did not find an update, and nothing was downloaded. + */ + NO_UPDATE, + /** + * The updater found an update, but was unable to download it. + */ + FAIL_DOWNLOAD, + /** + * For some reason, the updater was unable to contact dev.bukkit.org to download the file. + */ + FAIL_DBO, + /** + * When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'. + */ + FAIL_NOVERSION, + /** + * The slug provided by the plugin running the updater was invalid and doesn't exist on DBO. + */ + FAIL_BADSLUG, + /** + * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded. + */ + UPDATE_AVAILABLE + } + + /** + * Allows the dev to specify the type of update that will be run. + */ + public enum UpdateType + { + /** + * Run a version check, and then if the file is out of date, download the newest version. + */ + DEFAULT, + /** + * Don't run a version check, just find the latest update and download it. + */ + NO_VERSION_CHECK, + /** + * Get information about the version and the download size, but don't actually download anything. + */ + NO_DOWNLOAD + } + + /** + * Initialize the updater + * + * @param plugin + * The plugin that is checking for an update. + * @param slug + * The dev.bukkit.org slug of the project (http://dev.bukkit.org/server-mods/SLUG_IS_HERE) + * @param file + * The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type + * Specify the type of update this will be. See {@link UpdateType} + * @param announce + * True if the program should announce the progress of new updates in console + */ + public Updater(Plugin plugin, String slug, File file, UpdateType type, boolean announce) + { + this.plugin = plugin; + this.type = type; + this.announce = announce; + this.file = file; + try + { + // Obtain the results of the project's file feed + url = new URL(DBOUrl + slug + "/files.rss"); + } + catch (MalformedURLException ex) + { + // Invalid slug + plugin.getLogger().warning("The author of this plugin (" + plugin.getDescription().getAuthors().get(0) + ") has misconfigured their Auto Update system"); + plugin.getLogger().warning("The project slug given ('" + slug + "') is invalid. Please nag the author about this."); + result = Updater.UpdateResult.FAIL_BADSLUG; // Bad slug! Bad! + } + thread = new Thread(new UpdateRunnable()); + thread.start(); + } + + /** + * Get the result of the update process. + */ + public Updater.UpdateResult getResult() + { + waitForThread(); + return result; + } + + /** + * Get the total bytes of the file (can only be used after running a version check or a normal run). + */ + public long getFileSize() + { + waitForThread(); + return totalSize; + } + + /** + * Get the version string latest file avaliable online. + */ + public String getLatestVersionString() + { + waitForThread(); + return versionTitle; + } + + /** + * As the result of Updater output depends on the thread's completion, it is necessary to wait for the thread to finish + * before alloowing anyone to check the result. + */ + public void waitForThread() { + if(thread.isAlive()) { + try { + thread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Save an update from dev.bukkit.org into the server's update folder. + */ + private void saveFile(File folder, String file, String u) + { + if(!folder.exists()) + { + folder.mkdir(); + } + BufferedInputStream in = null; + FileOutputStream fout = null; + try + { + // Download the file + URL url = new URL(u); + int fileLength = url.openConnection().getContentLength(); + in = new BufferedInputStream(url.openStream()); + fout = new FileOutputStream(folder.getAbsolutePath() + "/" + file); + + byte[] data = new byte[BYTE_SIZE]; + int count; + if(announce) plugin.getLogger().info("About to download a new update: " + versionTitle); + long downloaded = 0; + while ((count = in.read(data, 0, BYTE_SIZE)) != -1) + { + downloaded += count; + fout.write(data, 0, count); + int percent = (int) (downloaded * 100 / fileLength); + if(announce & (percent % 10 == 0)) + { + plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes."); + } + } + //Just a quick check to make sure we didn't leave any files from last time... + for(File xFile : new File("plugins/" + updateFolder).listFiles()) + { + if(xFile.getName().endsWith(".zip")) + { + xFile.delete(); + } + } + // Check to see if it's a zip file, if it is, unzip it. + File dFile = new File(folder.getAbsolutePath() + "/" + file); + if(dFile.getName().endsWith(".zip")) + { + // Unzip + unzip(dFile.getCanonicalPath()); + } + if(announce) plugin.getLogger().info("Finished updating."); + } + catch (Exception ex) + { + plugin.getLogger().warning("The auto-updater tried to download a new update, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DOWNLOAD; + } + finally + { + try + { + if (in != null) + { + in.close(); + } + if (fout != null) + { + fout.close(); + } + } + catch (Exception ex) + { + } + } + } + + /** + * Part of Zip-File-Extractor, modified by H31IX for use with Bukkit + */ + private void unzip(String file) + { + try + { + File fSourceZip = new File(file); + String zipPath = file.substring(0, file.length()-4); + ZipFile zipFile = new ZipFile(fSourceZip); + Enumeration e = zipFile.entries(); + while(e.hasMoreElements()) + { + ZipEntry entry = (ZipEntry)e.nextElement(); + File destinationFilePath = new File(zipPath,entry.getName()); + destinationFilePath.getParentFile().mkdirs(); + if(entry.isDirectory()) + { + continue; + } + else + { + BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); + int b; + byte buffer[] = new byte[BYTE_SIZE]; + FileOutputStream fos = new FileOutputStream(destinationFilePath); + BufferedOutputStream bos = new BufferedOutputStream(fos, BYTE_SIZE); + while((b = bis.read(buffer, 0, BYTE_SIZE)) != -1) + { + bos.write(buffer, 0, b); + } + bos.flush(); + bos.close(); + bis.close(); + String name = destinationFilePath.getName(); + if(name.endsWith(".jar") && pluginFile(name)) + { + destinationFilePath.renameTo(new File("plugins/" + updateFolder + "/" + name)); + } + } + entry = null; + destinationFilePath = null; + } + e = null; + zipFile.close(); + zipFile = null; + // Move any plugin data folders that were included to the right place, Bukkit won't do this for us. + for(File dFile : new File(zipPath).listFiles()) + { + if(dFile.isDirectory()) + { + if(pluginFile(dFile.getName())) + { + File oFile = new File("plugins/" + dFile.getName()); // Get current dir + File [] contents = oFile.listFiles(); // List of existing files in the current dir + for(File cFile : dFile.listFiles()) // Loop through all the files in the new dir + { + boolean found = false; + for(File xFile : contents) // Loop through contents to see if it exists + { + if(xFile.getName().equals(cFile.getName())) + { + found = true; + break; + } + } + if(!found) + { + // Move the new file into the current dir + cFile.renameTo(new File(oFile.getCanonicalFile() + "/" + cFile.getName())); + } + else + { + // This file already exists, so we don't need it anymore. + cFile.delete(); + } + } + } + } + dFile.delete(); + } + new File(zipPath).delete(); + fSourceZip.delete(); + } + catch(IOException ex) + { + ex.printStackTrace(); + plugin.getLogger().warning("The auto-updater tried to unzip a new update file, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DOWNLOAD; + } + new File(file).delete(); + } + + /** + * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip. + */ + public boolean pluginFile(String name) + { + for(File file : new File("plugins").listFiles()) + { + if(file.getName().equals(name)) + { + return true; + } + } + return false; + } + + /** + * Obtain the direct download file url from the file's page. + */ + private String getFile(String link) + { + String download = null; + try + { + // Open a connection to the page + URL url = new URL(link); + URLConnection urlConn = url.openConnection(); + InputStreamReader inStream = new InputStreamReader(urlConn.getInputStream()); + BufferedReader buff = new BufferedReader(inStream); + + int counter = 0; + String line; + while((line = buff.readLine()) != null) + { + counter++; + // Search for the download link + if(line.contains("

  • ")) + { + // Get the raw link + download = line.split("Download")[0]; + } + // Search for size + else if (line.contains("
    Size
    ")) + { + sizeLine = counter+1; + } + else if(counter == sizeLine) + { + String size = line.replaceAll("
    ", "").replaceAll("
    ", ""); + multiplier = size.contains("MiB") ? 1048576 : 1024; + size = size.replace(" KiB", "").replace(" MiB", ""); + totalSize = (long)(Double.parseDouble(size)*multiplier); + } + } + urlConn = null; + inStream = null; + buff.close(); + buff = null; + } + catch (Exception ex) + { + ex.printStackTrace(); + plugin.getLogger().warning("The auto-updater tried to contact dev.bukkit.org, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DBO; + return null; + } + return download; + } + + /** + * Check to see if the program should continue by evaluation whether the plugin is already updated, or shouldn't be updated + */ + private boolean versionCheck(String title) + { + if(type != UpdateType.NO_VERSION_CHECK) + { + String version = plugin.getDescription().getVersion(); + if(title.split(" v").length == 2) + { + String remoteVersion = title.split(" v")[1].split(" ")[0]; // Get the newest file's version number + int remVer = -1,curVer=0; + try + { + remVer = calVer(remoteVersion); + curVer = calVer(version); + } + catch(NumberFormatException nfe) + { + remVer=-1; + } + if(hasTag(version)||version.equalsIgnoreCase(remoteVersion)||curVer>=remVer) + { + // We already have the latest version, or this build is tagged for no-update + result = Updater.UpdateResult.NO_UPDATE; + return false; + } + } + else + { + // The file's name did not contain the string 'vVersion' + plugin.getLogger().warning("The author of this plugin (" + plugin.getDescription().getAuthors().get(0) + ") has misconfigured their Auto Update system"); + plugin.getLogger().warning("Files uploaded to BukkitDev should contain the version number, seperated from the name by a 'v', such as PluginName v1.0"); + plugin.getLogger().warning("Please notify the author of this error."); + result = Updater.UpdateResult.FAIL_NOVERSION; + return false; + } + } + return true; + } + /** + * Used to calculate the version string as an Integer + */ + private Integer calVer(String s) throws NumberFormatException + { + if(s.contains(".")) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i