/*
* Universal Media Server, for streaming any media to DLNA
* compatible renderers based on the http://www.ps3mediaserver.org.
* Copyright (C) 2012 UMS developers.
*
* This program is a free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.configuration;
import com.sun.jna.Platform;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Map;
import java.util.Scanner;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.swing.JLabel;
import net.pms.Messages;
import net.pms.PMS;
import net.pms.external.ExternalFactory;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DownloadPlugins {
private final static String PLUGIN_LIST_URL = "http://www.universalmediaserver.com/plugins/list.php";
private final static String PLUGIN_TEST_FILE = "plugin_inst.tst";
private static final Logger LOGGER = LoggerFactory.getLogger(DownloadPlugins.class);
private static final PmsConfiguration configuration = PMS.getConfiguration();
private String id;
private String name;
private String rating;
private String desc;
private String url;
private String author;
private String version;
private ArrayList<URL> jars;
private JLabel updateLabel;
private String[] props;
private boolean test;
private boolean old;
public static ArrayList<DownloadPlugins> downloadList() {
ArrayList<DownloadPlugins> res = new ArrayList<>();
if (!configuration.getExternalNetwork()) {
// Do not try to get the plugin list if there is no
// external network.
return res;
}
try {
URL u = new URL(PLUGIN_LIST_URL);
URLConnection connection = u.openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
parse_list(res, in, false);
File test = new File(configuration.getPluginDirectory() + File.separator + PLUGIN_TEST_FILE);
if (test.exists()) {
in = new BufferedReader(new InputStreamReader(new FileInputStream(test)));
parse_list(res, in, true);
}
} catch (IOException e) {
}
return res;
}
private static int getInt(String val) {
try {
return Integer.parseInt(val);
} catch (Exception e) {
return 0;
}
}
private static void parse_list(ArrayList<DownloadPlugins> res, BufferedReader in, boolean test) throws IOException {
String str;
DownloadPlugins plugin = new DownloadPlugins(test);
while ((str = in.readLine()) != null) {
str = str.trim();
if (StringUtils.isEmpty(str)) {
if (plugin.isOk() || plugin.isTest()) {
if (test) {
plugin.setRating("TEST");
}
res.add(plugin);
} else {
LOGGER.trace("An invalid plugin was ignored (1)");
}
plugin = new DownloadPlugins(test);
continue;
}
String[] keyval = str.split("=", 2);
if (keyval.length < 2) {
continue;
}
if (keyval[0].equalsIgnoreCase("id")) {
plugin.id = keyval[1];
}
if (keyval[0].equalsIgnoreCase("name")) {
plugin.name = keyval[1];
}
if (keyval[0].equalsIgnoreCase("rating")) {
// Rating is temporarily switched off
// plugin.rating = keyval[1];
}
if (keyval[0].equalsIgnoreCase("desc")) {
plugin.desc = keyval[1];
}
if (keyval[0].equalsIgnoreCase("url")) {
plugin.url = keyval[1];
}
if (keyval[0].equalsIgnoreCase("author")) {
plugin.author = keyval[1];
}
if (keyval[0].equalsIgnoreCase("version")) {
plugin.version = keyval[1];
}
if (keyval[0].equalsIgnoreCase("type")) {
// Deprecated
}
if (keyval[0].equalsIgnoreCase("require")) {
plugin.old = false;
String[] minVer = keyval[1].split("\\.");
String[] myVer = PMS.getVersion().split("\\.");
int max = Math.max(myVer.length, minVer.length);
for (int i = 0; i < max; i++) {
int my,min;
// If the versions are of different length
// say that the part of "wrong" length is 0
my = getInt(((i > myVer.length) ? "0" : myVer[i]));
min = getInt(((i > minVer.length) ? "0" : minVer[i]));
if (min == my) {
// This is equal take the next part of the
// version string
continue;
}
if (min > my) {
// Old otherwise it is new
plugin.old = true;
}
// anyway stop here
break;
}
}
if (keyval[0].equalsIgnoreCase("prop")) {
plugin.props = keyval[1].split(",");
}
}
if (plugin.isOk() || plugin.isTest()) { // Add the last one
if (test) {
plugin.setRating("TEST");
}
res.add(plugin);
} else {
LOGGER.trace("An invalid plugin was ignored (2)");
}
in.close();
}
public DownloadPlugins() {
this(false);
}
public DownloadPlugins(boolean test) {
rating = "--";
jars = null;
old = false;
this.test = test;
}
public String getName() {
return name;
}
public String getRating() {
return rating;
}
public String getAuthor() {
return author;
}
public String getDescription() {
return desc;
}
public String getVersion() {
return version;
}
public void setRating(String str) {
rating = str;
}
public boolean isOk() {
// We must have a name and ID
return (!StringUtils.isEmpty(name)) && (!StringUtils.isEmpty(id));
}
public boolean isTest() {
return test;
}
public boolean isOld() {
return old;
}
private String splitString(String string) {
StringBuilder buf = new StringBuilder();
String tempString = string;
if (string != null) {
while (tempString.length() > 60) {
String block = tempString.substring(0, 60);
int index = block.lastIndexOf(' ');
if (index < 0) {
index = tempString.indexOf(' ');
}
if (index >= 0) {
buf.append(tempString.substring(0, index)).append("<BR>");
}
tempString = tempString.substring(index + 1);
}
} else {
tempString = " ";
}
buf.append(tempString);
return buf.toString();
}
private String header(String hdr) {
return "<br><b>" + hdr + ": </b>";
}
public String htmlString() {
String res = "<html>";
res += "<b>Name: </b>" + getName();
if (!StringUtils.isEmpty(getRating())) {
res += header("Rating") + getRating();
}
if (!StringUtils.isEmpty(getAuthor())) {
res += header("Author") + getAuthor();
}
if (!StringUtils.isEmpty(getDescription())) {
res += header("Description") + splitString(getDescription());
}
return res;
}
private String extractFileName(String str, String name) {
if (!StringUtils.isEmpty(name)) {
return name;
}
int pos = str.lastIndexOf('/');
if (pos == -1) {
return name;
}
return str.substring(pos + 1);
}
private void ensureCreated(String p) {
File f = new File(p);
f.mkdirs();
}
private void unzip(File f, String dir) {
// Zip file with loads of goodies
// Unzip it
ZipInputStream zis;
try {
zis = new ZipInputStream(new FileInputStream(f));
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
File dst = new File(dir + File.separator + entry.getName());
if (entry.isDirectory()) {
dst.mkdirs();
continue;
}
int count;
byte data[] = new byte[4096];
FileOutputStream fos = new FileOutputStream(dst);
try (BufferedOutputStream dest = new BufferedOutputStream(fos, 4096)) {
while ((count = zis.read(data, 0, 4096)) != -1) {
dest.write(data, 0, count);
}
dest.flush();
}
if (dst.getAbsolutePath().endsWith(".jar")) {
jars.add(dst.toURI().toURL());
}
}
zis.close();
} catch (Exception e) {
LOGGER.info("unzip error " + e);
}
f.delete();
}
private boolean downloadFile(String url, String dir, String name) throws Exception {
URL u = new URL(url);
ensureCreated(dir);
String fName = extractFileName(url, name);
LOGGER.debug("fetch file " + url);
if (updateLabel != null) {
updateLabel.setText(Messages.getString("NetworkTab.47") + ": " + fName);
}
File f = new File(dir + File.separator + fName);
if (f.exists()) {
// no need to download it twice
return true;
}
URLConnection connection = u.openConnection();
connection.setDoInput(true);
connection.setDoOutput(true);
try (InputStream in = connection.getInputStream(); FileOutputStream out = new FileOutputStream(f)) {
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
out.flush();
} catch (Exception e) {
LOGGER.warn("Plugin download error: " + e);
}
if (fName.endsWith(".zip")) {
for (String prop : props) {
if (prop.equalsIgnoreCase("unzip")) {
unzip(f, dir);
}
}
}
// If we got down here add the jar to the list (if it is a jar)
if (f.getAbsolutePath().endsWith(".jar")) {
jars.add(f.toURI().toURL());
}
return true;
}
private void doExec(String args) throws IOException, InterruptedException, ConfigurationException {
int pos = args.indexOf(',');
if (pos == -1) { // weird stuff
return;
}
// Everything after the "," is what we're supposed to run
// First make note of jars we got
File[] oldJar = new File(configuration.getPluginDirectory()).listFiles();
// Before we start external installers better save the config
configuration.save();
ProcessBuilder pb = new ProcessBuilder(args.substring(pos + 1));
pb.redirectErrorStream(true);
Map<String, String> env = pb.environment();
env.put("PROFILE_PATH", configuration.getProfilePath());
env.put("UMS_VERSION", PMS.getVersion());
LOGGER.debug("running '" + args + "'");
Process pid = pb.start();
// Consume and log any output
Scanner output = new Scanner(pid.getInputStream());
while (output.hasNextLine()) {
LOGGER.debug("[" + args + "] " + output.nextLine());
}
configuration.reload();
pid.waitFor();
File[] newJar = new File(configuration.getPluginDirectory()).listFiles();
for (File f : newJar) {
if (!f.getAbsolutePath().endsWith(".jar")) {
// skip non jar files
continue;
}
for (File oldJar1 : oldJar) {
if (f.getAbsolutePath().equals(oldJar1.getAbsolutePath())) {
// old jar file break out, and set f to null to skip adding it
f = null;
break;
}
}
// if f is null this is an jar that is old
if (f != null) {
jars.add(f.toURI().toURL());
}
}
}
private boolean command(String cmd, String args) throws ConfigurationException {
if (cmd.equalsIgnoreCase("move")) {
// arg1 is src and arg2 is dst
String[] tmp = args.split(",");
try {
FileUtils.moveFile(new File(tmp[1]), new File(tmp[2]));
} catch (IOException e) {
// Ignore errors, just log it
LOGGER.debug("couldn't move file " + tmp[1] + " to " + tmp[2]);
}
return true;
}
if (cmd.equalsIgnoreCase("touch")) {
// arg1 is file to touch
String[] tmp = args.split(",");
try {
FileUtils.touch(new File(tmp[1]));
} catch (IOException e) {
// Ignore errors, just log it
LOGGER.debug("couldn't touch file " + tmp[1]);
}
return true;
}
if (cmd.equalsIgnoreCase("conf")) {
String[] tmp = args.split(",", 2);
tmp = tmp[1].split("=");
configuration.setCustomProperty(tmp[1], tmp[2]);
configuration.save();
return true;
}
if (cmd.equalsIgnoreCase("exec")) {
try {
doExec(args);
} catch (ConfigurationException | IOException | InterruptedException e) {
}
return true;
}
return false;
}
private boolean downloadList(String url) throws Exception {
if ("".equals(url)) {
url = PLUGIN_LIST_URL + "?id=" + id;
}
String platform = Platform.isWindows() ? "windows" : Platform.isLinux() ? "linux" : Platform.isMac() ? "osx" : "";
URL u = new URL(url);
URLConnection connection = u.openConnection();
boolean res, skip = false;
try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String str;
res = true;
while ((str = in.readLine()) != null) {
str = str.trim();
if (StringUtils.isEmpty(str)) {
continue;
}
if (str.toLowerCase().matches("windows|linux|osx")) {
skip = !str.toLowerCase().equals(platform);
continue;
}
if (skip) {
// This line is specific to another platform, ignore it
continue;
}
String[] tmp = str.split(",", 3);
String dir = configuration.getPluginDirectory();
String filename = "";
if (command(tmp[0], str)) {
// a command take the next line
continue;
}
if (tmp.length > 1) {
String rootDir = new File("").getAbsolutePath();
if (tmp[1].equalsIgnoreCase("root")) {
dir = rootDir;
} else {
dir = rootDir + File.separator + tmp[1];
}
if (tmp.length > 2) {
filename = tmp[2];
}
}
res &= downloadFile(tmp[0], dir, filename);
}
}
return res;
}
private boolean download() throws Exception {
if (isTest()) {
// we can't use the id based stuff
// when testing
return downloadList(url);
}
return downloadList("");
}
public boolean install(JLabel update) throws Exception {
LOGGER.debug("Installing plugin " + name);
updateLabel = update;
// Init the jar file list
jars = new ArrayList<>();
// Download the list
if (!download()) { // Download failed, bail out
LOGGER.debug("Download failed");
return false;
}
// Load the jars (if any)
if (jars.isEmpty()) {
return true;
}
URL[] jarURLs = new URL[jars.size()];
jars.toArray(jarURLs);
if (updateLabel != null) {
updateLabel.setText("Loading JARs");
}
LOGGER.debug("Loading jars");
ExternalFactory.loadJARs(jarURLs, true);
// Create the instances of the plugins
ExternalFactory.instantiateDownloaded(update);
updateLabel = null;
return true;
}
}