diff --git a/airsonic-main/src/main/java/org/airsonic/player/ajax/CoverArtService.java b/airsonic-main/src/main/java/org/airsonic/player/ajax/CoverArtService.java index 9d8d3407..498936eb 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/ajax/CoverArtService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/ajax/CoverArtService.java @@ -25,8 +25,8 @@ import org.airsonic.player.service.LastFmService; import org.airsonic.player.service.MediaFileService; import org.airsonic.player.service.SecurityService; import org.airsonic.player.util.FileUtil; -import org.airsonic.player.util.StringUtil; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringEscapeUtils; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -109,7 +109,7 @@ public class CoverArtService { // Check permissions. File newCoverFile = new File(path, "cover." + suffix); if (!securityService.isWriteAllowed(newCoverFile)) { - throw new Exception("Permission denied: " + StringUtil.toHtml(newCoverFile.getPath())); + throw new Exception("Permission denied: " + StringEscapeUtils.escapeHtml(newCoverFile.getPath())); } // If file exists, create a backup. diff --git a/airsonic-main/src/main/java/org/airsonic/player/ajax/NowPlayingService.java b/airsonic-main/src/main/java/org/airsonic/player/ajax/NowPlayingService.java index 302b1aa4..63e0a28a 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/ajax/NowPlayingService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/ajax/NowPlayingService.java @@ -22,6 +22,7 @@ package org.airsonic.player.ajax; import org.airsonic.player.domain.*; import org.airsonic.player.service.*; import org.airsonic.player.util.StringUtil; +import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.directwebremoting.WebContext; import org.directwebremoting.WebContextFactory; @@ -129,14 +130,14 @@ public class NowPlayingService { avatarUrl = url + "avatar.view?usernameUtf8Hex=" + StringUtil.utf8HexEncode(username); } - String tooltip = StringUtil.toHtml(artist) + " – " + StringUtil.toHtml(title); + String tooltip = StringEscapeUtils.escapeHtml(artist) + " – " + StringEscapeUtils.escapeHtml(title); if (StringUtils.isNotBlank(player.getName())) { username += "@" + player.getName(); } - artist = StringUtil.toHtml(StringUtils.abbreviate(artist, 25)); - title = StringUtil.toHtml(StringUtils.abbreviate(title, 25)); - username = StringUtil.toHtml(StringUtils.abbreviate(username, 25)); + artist = StringEscapeUtils.escapeHtml(StringUtils.abbreviate(artist, 25)); + title = StringEscapeUtils.escapeHtml(StringUtils.abbreviate(title, 25)); + username = StringEscapeUtils.escapeHtml(StringUtils.abbreviate(username, 25)); long minutesAgo = status.getMinutesAgo(); diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/SetMusicFileInfoController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/SetMusicFileInfoController.java index 92018ecd..8a37d5ee 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/SetMusicFileInfoController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/SetMusicFileInfoController.java @@ -21,7 +21,7 @@ package org.airsonic.player.controller; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.service.MediaFileService; -import org.airsonic.player.util.StringUtil; +import org.apache.commons.lang.StringEscapeUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.ServletRequestUtils; @@ -52,7 +52,7 @@ public class SetMusicFileInfoController { MediaFile mediaFile = mediaFileService.getMediaFile(id); if ("comment".equals(action)) { - mediaFile.setComment(StringUtil.toHtml(request.getParameter("comment"))); + mediaFile.setComment(StringEscapeUtils.escapeHtml(request.getParameter("comment"))); mediaFileService.updateMediaFile(mediaFile); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/UploadController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/UploadController.java index 82acf520..ceae1194 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/UploadController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/UploadController.java @@ -27,10 +27,10 @@ import org.airsonic.player.service.SettingsService; import org.airsonic.player.service.StatusService; import org.airsonic.player.upload.MonitoredDiskFileItemFactory; import org.airsonic.player.upload.UploadListener; -import org.airsonic.player.util.StringUtil; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.lang.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -126,7 +126,7 @@ public class UploadController { File targetFile = new File(dir, new File(fileName).getName()); if (!securityService.isUploadAllowed(targetFile)) { - throw new Exception("Permission denied: " + StringUtil.toHtml(targetFile.getPath())); + throw new Exception("Permission denied: " + StringEscapeUtils.escapeHtml(targetFile.getPath())); } if (!dir.exists()) { @@ -173,13 +173,13 @@ public class UploadController { ZipEntry entry = (ZipEntry) entries.nextElement(); File entryFile = new File(file.getParentFile(), entry.getName()); if (!entryFile.toPath().normalize().startsWith(file.getParentFile().toPath())) { - throw new Exception("Bad zip filename: " + StringUtil.toHtml(entryFile.getPath())); + throw new Exception("Bad zip filename: " + StringEscapeUtils.escapeHtml(entryFile.getPath())); } if (!entry.isDirectory()) { if (!securityService.isUploadAllowed(entryFile)) { - throw new Exception("Permission denied: " + StringUtil.toHtml(entryFile.getPath())); + throw new Exception("Permission denied: " + StringEscapeUtils.escapeHtml(entryFile.getPath())); } entryFile.getParentFile().mkdirs(); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java index a8f000e9..ed7ec770 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java @@ -877,7 +877,7 @@ public class SettingsService { String[] lines = StringUtil.readLines(in); for (String line : lines) { - locales.add(parseLocale(line)); + locales.add(StringUtil.parseLocale(line)); } } catch (IOException x) { @@ -888,21 +888,6 @@ public class SettingsService { return locales.toArray(new Locale[locales.size()]); } - private Locale parseLocale(String line) { - String[] s = line.split("_"); - String language = s[0]; - String country = ""; - String variant = ""; - - if (s.length > 1) { - country = s[1]; - } - if (s.length > 2) { - variant = s[2]; - } - return new Locale(language, country, variant); - } - /** * Returns the "brand" name. Normally, this is just "Airsonic". * diff --git a/airsonic-main/src/main/java/org/airsonic/player/util/StringUtil.java b/airsonic-main/src/main/java/org/airsonic/player/util/StringUtil.java index 6519967c..3da90d1b 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/util/StringUtil.java +++ b/airsonic-main/src/main/java/org/airsonic/player/util/StringUtil.java @@ -19,6 +19,7 @@ */ package org.airsonic.player.util; +import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.apache.commons.lang.StringUtils; @@ -28,11 +29,11 @@ import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; import java.text.*; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; /** * Miscellaneous string utility methods. @@ -42,15 +43,8 @@ import java.util.regex.Pattern; public final class StringUtil { public static final String ENCODING_UTF8 = "UTF-8"; - private static final DateFormat ISO_8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - - private static final String[][] HTML_SUBSTITUTIONS = { - {"&", "&"}, - {"<", "<"}, - {">", ">"}, - {"'", "'"}, - {"\"", """}, - }; + + private static final Pattern SPLIT_PATTERN = Pattern.compile("\"([^\"]*)\"|(\\S+)"); private static final String[][] MIME_TYPES = { {"mp3", "audio/mpeg"}, @@ -98,58 +92,6 @@ public final class StringUtil { private StringUtil() { } - /** - * Returns the specified string converted to a format suitable for - * HTML. All single-quote, double-quote, greater-than, less-than and - * ampersand characters are replaces with their corresponding HTML - * Character Entity code. - * - * @param s the string to convert - * @return the converted string - */ - public static String toHtml(String s) { - if (s == null) { - return null; - } - for (String[] substitution : HTML_SUBSTITUTIONS) { - if (s.contains(substitution[0])) { - s = s.replaceAll(substitution[0], substitution[1]); - } - } - return s; - } - - - /** - * Formats the given date to a ISO-8601 date/time format, and UTC timezone. - *

- * The returned date uses the following format: 2007-12-17T14:57:17 - * - * @param date The date to format - * @return The corresponding ISO-8601 formatted string. - */ - public static String toISO8601(Date date) { - if (date == null) { - return null; - } - - synchronized (ISO_8601_DATE_FORMAT) { - return ISO_8601_DATE_FORMAT.format(date); - } - } - - /** - * Removes the suffix (the substring after the last dot) of the given string. The dot is - * also removed. - * - * @param s The string in question, e.g., "foo.mp3". - * @return The string without the suffix, e.g., "foo". - */ - public static String removeSuffix(String s) { - int index = s.lastIndexOf('.'); - return index == -1 ? s : s.substring(0, index); - } - /** * Returns the proper MIME type for the given suffix. * @@ -269,16 +211,14 @@ public final class StringUtil { return new String[0]; } - Pattern pattern = Pattern.compile("\".*?\"|\\S+"); - Matcher matcher = pattern.matcher(input); - - List result = new ArrayList(); - while (matcher.find()) { - String element = matcher.group(); - if (element.startsWith("\"") && element.endsWith("\"") && element.length() > 1) { - element = element.substring(1, element.length() - 1); + List result = new ArrayList<>(); + Matcher m = SPLIT_PATTERN.matcher(input); + while (m.find()) { + if (m.group(1) != null) { + result.add(m.group(1)); // quoted string + } else { + result.add(m.group(2)); // unquoted string } - result.add(element); } return result.toArray(new String[result.size()]); @@ -320,21 +260,9 @@ public final class StringUtil { return new int[0]; } - String[] strings = StringUtils.split(s); - int[] ints = new int[strings.length]; - for (int i = 0; i < strings.length; i++) { - ints[i] = Integer.parseInt(strings[i]); - } - return ints; - } - - /** - * Determines whether a is equal to b, taking null into account. - * - * @return Whether a and b are equal, or both null. - */ - public static boolean isEqual(Object a, Object b) { - return Objects.equals(a, b); + return Stream.of(StringUtils.split(s)) + .mapToInt(Integer::parseInt) + .toArray(); } /** @@ -348,18 +276,11 @@ public final class StringUtil { return null; } - String[] elements = s.split("_"); - - if (elements.length == 0) { - return new Locale(s, "", ""); - } - if (elements.length == 1) { - return new Locale(elements[0], "", ""); + List elements = new ArrayList<>(Arrays.asList(s.split("_", 3))); + while (elements.size() < 3) { + elements.add(""); } - if (elements.length == 2) { - return new Locale(elements[0], elements[1], ""); - } - return new Locale(elements[0], elements[1], elements[2]); + return new Locale(elements.get(0), elements.get(1), elements.get(2)); } /** @@ -404,34 +325,15 @@ public final class StringUtil { * * @param s The string to decode. * @return The decoded string. - * @throws Exception If an error occurs. + * @throws DecoderException If an error occurs. */ - public static String utf8HexDecode(String s) throws Exception { + public static String utf8HexDecode(String s) throws DecoderException { if (s == null) { return null; } return new String(Hex.decodeHex(s.toCharArray()), StandardCharsets.UTF_8); } - /** - * Calculates the MD5 digest and returns the value as a 32 character hex string. - * - * @param s Data to digest. - * @return MD5 digest as a hex string. - */ - public static String md5Hex(String s) { - if (s == null) { - return null; - } - - try { - MessageDigest md5 = MessageDigest.getInstance("MD5"); - return new String(Hex.encodeHex(md5.digest(s.getBytes(StandardCharsets.UTF_8)))); - } catch (Exception x) { - throw new RuntimeException(x.getMessage(), x); - } - } - /** * Returns the file part of an URL. For instance: *

diff --git a/airsonic-main/src/test/java/org/airsonic/player/util/StringUtilTestCase.java b/airsonic-main/src/test/java/org/airsonic/player/util/StringUtilTestCase.java index 78fccf92..3f0b9789 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/util/StringUtilTestCase.java +++ b/airsonic-main/src/test/java/org/airsonic/player/util/StringUtilTestCase.java @@ -20,6 +20,7 @@ package org.airsonic.player.util; import junit.framework.TestCase; +import org.apache.commons.lang.StringEscapeUtils; import java.util.Arrays; import java.util.Locale; @@ -32,20 +33,11 @@ import java.util.Locale; public class StringUtilTestCase extends TestCase { public void testToHtml() { - assertEquals(null, StringUtil.toHtml(null)); - assertEquals("", StringUtil.toHtml("")); - assertEquals(" ", StringUtil.toHtml(" ")); - assertEquals("q & a", StringUtil.toHtml("q & a")); - assertEquals("q & a <> b", StringUtil.toHtml("q & a <> b")); - } - - public void testRemoveSuffix() { - assertEquals("Error in removeSuffix().", "foo", StringUtil.removeSuffix("foo.mp3")); - assertEquals("Error in removeSuffix().", "", StringUtil.removeSuffix(".mp3")); - assertEquals("Error in removeSuffix().", "foo.bar", StringUtil.removeSuffix("foo.bar.mp3")); - assertEquals("Error in removeSuffix().", "foo.", StringUtil.removeSuffix("foo..mp3")); - assertEquals("Error in removeSuffix().", "foo", StringUtil.removeSuffix("foo")); - assertEquals("Error in removeSuffix().", "", StringUtil.removeSuffix("")); + assertEquals(null, StringEscapeUtils.escapeHtml(null)); + assertEquals("", StringEscapeUtils.escapeHtml("")); + assertEquals(" ", StringEscapeUtils.escapeHtml(" ")); + assertEquals("q & a", StringEscapeUtils.escapeHtml("q & a")); + assertEquals("q & a <> b", StringEscapeUtils.escapeHtml("q & a <> b")); } public void testGetMimeType() { @@ -184,12 +176,6 @@ public class StringUtilTestCase extends TestCase { assertEquals("Error in utf8hex.", s, StringUtil.utf8HexDecode(StringUtil.utf8HexEncode(s))); } - public void testMd5Hex() { - assertNull("Error in md5Hex().", StringUtil.md5Hex(null)); - assertEquals("Error in md5Hex().", "d41d8cd98f00b204e9800998ecf8427e", StringUtil.md5Hex("")); - assertEquals("Error in md5Hex().", "308ed0af23d48f6d2fd4717e77a23e0c", StringUtil.md5Hex("sindre@activeobjects.no")); - } - public void testGetUrlFile() { assertEquals("Error in getUrlFile().", "foo.mp3", StringUtil.getUrlFile("http://www.asdf.com/foo.mp3")); assertEquals("Error in getUrlFile().", "foo.mp3", StringUtil.getUrlFile("http://www.asdf.com/bar/foo.mp3"));