aboutsummaryrefslogtreecommitdiff
path: root/subsonic-backend
diff options
context:
space:
mode:
Diffstat (limited to 'subsonic-backend')
-rw-r--r--subsonic-backend/pom.xml144
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/Util.java62
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/IPNController.java152
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/MultiController.java256
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionController.java155
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionManagementController.java273
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/AbstractDao.java57
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/DaoHelper.java84
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/PaymentDao.java125
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/RedirectionDao.java96
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema.java66
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema10.java100
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema20.java90
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Payment.java200
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Redirection.java155
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/EmailSession.java108
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/LicenseGenerator.java170
-rw-r--r--subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/WhitelistGenerator.java53
-rw-r--r--subsonic-backend/src/main/resources/log4j.properties19
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/applicationContext-backend.xml25
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/db.jsp51
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/payment.jsp23
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/requestLicense.jsp42
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/jsp/head.jsp3
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/jsp/include.jsp7
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/subsonic-backend-servlet.xml54
-rw-r--r--subsonic-backend/src/main/webapp/WEB-INF/web.xml36
27 files changed, 2606 insertions, 0 deletions
diff --git a/subsonic-backend/pom.xml b/subsonic-backend/pom.xml
new file mode 100644
index 00000000..15d052b0
--- /dev/null
+++ b/subsonic-backend/pom.xml
@@ -0,0 +1,144 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>net.sourceforge.subsonic</groupId>
+ <artifactId>subsonic-backend</artifactId>
+ <packaging>war</packaging>
+ <name>Subsonic Backend</name>
+
+ <parent>
+ <groupId>net.sourceforge.subsonic</groupId>
+ <artifactId>subsonic</artifactId>
+ <version>4.7.beta2</version>
+ </parent>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring</artifactId>
+ <version>2.5.6</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-webmvc</artifactId>
+ <version>2.5.6</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-beans</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context-support</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-web</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.3</version>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ <version>1.4</version>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>1.3.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ <version>4.0.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ <version>2.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.0.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>hsqldb</groupId>
+ <artifactId>hsqldb</artifactId>
+ <version>1.8.0.7</version>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>1.2.16</version>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <version>2.4</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>jsp-api</artifactId>
+ <version>2.0</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>jstl</artifactId>
+ <version>1.1.2</version>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>taglibs</groupId>
+ <artifactId>standard</artifactId>
+ <version>1.1.2</version>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>taglibs</groupId>
+ <artifactId>string</artifactId>
+ <version>1.1.0</version>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.1</version>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/Util.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/Util.java
new file mode 100644
index 00000000..31a1be71
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/Util.java
@@ -0,0 +1,62 @@
+package net.sourceforge.subsonic.backend;
+
+import org.apache.log4j.Logger;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.Reader;
+import java.io.FileReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+
+/**
+ * @author Sindre Mehus
+ */
+public class Util {
+
+ private static final File BACKEND_HOME = new File("/var/subsonic-backend");
+ private static final Logger LOG = Logger.getLogger(Util.class);
+
+ private Util() {
+ }
+
+ public static synchronized File getBackendHome() {
+ if (!BACKEND_HOME.exists() || !BACKEND_HOME.isDirectory()) {
+ boolean success = BACKEND_HOME.mkdirs();
+ if (!success) {
+ String message = "The directory " + BACKEND_HOME + " does not exist. Please create it and make it writable.";
+ LOG.error(message);
+ throw new RuntimeException(message);
+ }
+ }
+ return BACKEND_HOME;
+ }
+
+ public static String getPassword(String filename) throws IOException {
+ File pwdFile = new File(getBackendHome(), filename);
+ Reader reader = new FileReader(pwdFile);
+ try {
+ return StringUtils.trimToNull(IOUtils.toString(reader));
+ } finally {
+ IOUtils.closeQuietly(reader);
+ }
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/IPNController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/IPNController.java
new file mode 100644
index 00000000..f6ced54f
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/IPNController.java
@@ -0,0 +1,152 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.controller;
+
+import org.apache.log4j.Logger;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.params.HttpConnectionParams;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URLEncoder;
+import java.util.Enumeration;
+import java.util.Date;
+import java.io.UnsupportedEncodingException;
+
+import net.sourceforge.subsonic.backend.domain.Payment;
+import net.sourceforge.subsonic.backend.dao.PaymentDao;
+
+/**
+ * Processes IPNs (Instant Payment Notifications) from PayPal.
+ *
+ * @author Sindre Mehus
+ */
+public class IPNController implements Controller {
+
+ private static final Logger LOG = Logger.getLogger(IPNController.class);
+
+ private static final String PAYPAL_URL = "https://www.paypal.com/cgi-bin/webscr";
+
+ private PaymentDao paymentDao;
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ try {
+
+ LOG.info("Incoming IPN from " + request.getRemoteAddr());
+
+ String url = createValidationURL(request);
+ if (validate(url)) {
+ LOG.info("Verified payment. " + url);
+ } else {
+ LOG.warn("Failed to verify payment. " + url);
+ }
+ createOrUpdatePayment(request);
+
+ return null;
+ } catch (Exception x) {
+ LOG.error("Failed to process IPN.", x);
+ throw x;
+ }
+ }
+
+ private String createValidationURL(HttpServletRequest request) throws UnsupportedEncodingException {
+ Enumeration<?> en = request.getParameterNames();
+ StringBuilder url = new StringBuilder(PAYPAL_URL).append("?cmd=_notify-validate");
+ String encoding = request.getParameter("charset");
+ if (encoding == null) {
+ encoding = "ISO-8859-1";
+ }
+
+ while (en.hasMoreElements()) {
+ String paramName = (String) en.nextElement();
+ String paramValue = request.getParameter(paramName);
+ url.append("&").append(paramName).append("=").append(URLEncoder.encode(paramValue, encoding));
+ }
+
+ return url.toString();
+ }
+
+ private void createOrUpdatePayment(HttpServletRequest request) {
+ String item = request.getParameter("item_number");
+ if (item == null) {
+ item = request.getParameter("item_number1");
+ }
+ String paymentStatus = request.getParameter("payment_status");
+ String paymentType = request.getParameter("payment_type");
+ int paymentAmount = Math.round(new Float(request.getParameter("mc_gross")));
+ String paymentCurrency = request.getParameter("mc_currency");
+ String txnId = request.getParameter("txn_id");
+ String txnType = request.getParameter("txn_type");
+ String payerEmail = request.getParameter("payer_email");
+ String payerFirstName = request.getParameter("first_name");
+ String payerLastName = request.getParameter("last_name");
+ String payerCountry = request.getParameter("address_country");
+
+ Payment payment = paymentDao.getPaymentByTransactionId(txnId);
+ if (payment == null) {
+ payment = new Payment(null, txnId, txnType, item, paymentType, paymentStatus,
+ paymentAmount, paymentCurrency, payerEmail, payerFirstName, payerLastName,
+ payerCountry, Payment.ProcessingStatus.NEW, new Date(), new Date());
+ paymentDao.createPayment(payment);
+ } else {
+ payment.setTransactionType(txnType);
+ payment.setItem(item);
+ payment.setPaymentType(paymentType);
+ payment.setPaymentStatus(paymentStatus);
+ payment.setPaymentAmount(paymentAmount);
+ payment.setPaymentCurrency(paymentCurrency);
+ payment.setPayerEmail(payerEmail);
+ payment.setPayerFirstName(payerFirstName);
+ payment.setPayerLastName(payerLastName);
+ payment.setPayerCountry(payerCountry);
+ payment.setLastUpdated(new Date());
+ paymentDao.updatePayment(payment);
+ }
+
+ LOG.info("Received " + payment);
+ }
+
+ private boolean validate(String url) throws Exception {
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 60000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 60000);
+ HttpGet method = new HttpGet(url);
+ String content;
+ try {
+ ResponseHandler<String> responseHandler = new BasicResponseHandler();
+ content = client.execute(method, responseHandler);
+
+ LOG.info("Validation result: " + content);
+ return "VERIFIED".equals(content);
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+
+ public void setPaymentDao(PaymentDao paymentDao) {
+ this.paymentDao = paymentDao;
+ }
+
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/MultiController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/MultiController.java
new file mode 100644
index 00000000..cdc1674a
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/MultiController.java
@@ -0,0 +1,256 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.controller;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.subsonic.backend.dao.PaymentDao;
+import net.sourceforge.subsonic.backend.service.LicenseGenerator;
+import net.sourceforge.subsonic.backend.service.WhitelistGenerator;
+import org.apache.log4j.Logger;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.jdbc.core.ColumnMapRowMapper;
+import org.springframework.dao.DataAccessException;
+import net.sourceforge.subsonic.backend.dao.DaoHelper;
+import net.sourceforge.subsonic.backend.Util;
+import net.sourceforge.subsonic.backend.service.EmailSession;
+
+/**
+ * Multi-controller used for simple pages.
+ *
+ * @author Sindre Mehus
+ */
+public class MultiController extends MultiActionController {
+
+ private static final Logger LOG = Logger.getLogger(RedirectionController.class);
+
+ private static final String SUBSONIC_VERSION = "4.6";
+ private static final String SUBSONIC_BETA_VERSION = "4.7.beta2";
+
+ private static final Date LICENSE_DATE_THRESHOLD;
+
+ private DaoHelper daoHelper;
+
+ private PaymentDao paymentDao;
+ private WhitelistGenerator whitelistGenerator;
+ private LicenseGenerator licenseGenerator;
+
+ static {
+ Calendar calendar = Calendar.getInstance();
+ calendar.clear();
+ calendar.set(Calendar.YEAR, 2010);
+ calendar.set(Calendar.MONTH, Calendar.JUNE);
+ calendar.set(Calendar.DAY_OF_MONTH, 19);
+ LICENSE_DATE_THRESHOLD = calendar.getTime();
+ }
+
+ public ModelAndView version(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ String localVersion = request.getParameter("v");
+ LOG.info(request.getRemoteAddr() + " asked for latest version. Local version: " + localVersion);
+
+ PrintWriter writer = response.getWriter();
+
+ writer.println("SUBSONIC_VERSION_BEGIN" + SUBSONIC_VERSION + "SUBSONIC_VERSION_END");
+ writer.println("SUBSONIC_FULL_VERSION_BEGIN" + SUBSONIC_VERSION + "SUBSONIC_FULL_VERSION_END");
+ writer.println("SUBSONIC_BETA_VERSION_BEGIN" + SUBSONIC_BETA_VERSION + "SUBSONIC_BETA_VERSION_END");
+
+ return null;
+ }
+
+ public ModelAndView validateLicense(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ String email = request.getParameter("email");
+ Long date = ServletRequestUtils.getLongParameter(request, "date");
+
+ boolean valid = isLicenseValid(email, date);
+ LOG.info(request.getRemoteAddr() + " asked to validate license for " + email + ". Result: " + valid);
+
+ PrintWriter writer = response.getWriter();
+ writer.println(valid);
+
+ return null;
+ }
+
+ public ModelAndView sendMail(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String from = request.getParameter("from");
+ String to = request.getParameter("to");
+ String subject = request.getParameter("subject");
+ String text = request.getParameter("text");
+
+ EmailSession session = new EmailSession();
+ session.sendMessage(from, Arrays.asList(to), null, null, null, subject, text);
+
+ LOG.info("Sent email on behalf of " + request.getRemoteAddr() + " to " + to + " with subject '" + subject + "'");
+
+ return null;
+ }
+
+ public ModelAndView db(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ if (!authenticate(request, response)) {
+ return null;
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ map.put("p", request.getParameter("p"));
+ String query = request.getParameter("query");
+ if (query != null) {
+ map.put("query", query);
+
+ try {
+ List<?> result = daoHelper.getJdbcTemplate().query(query, new ColumnMapRowMapper());
+ map.put("result", result);
+ } catch (DataAccessException x) {
+ map.put("error", ExceptionUtils.getRootCause(x).getMessage());
+ }
+ }
+
+ return new ModelAndView("backend/db", "model", map);
+ }
+
+ public ModelAndView payment(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ if (!authenticate(request, response)) {
+ return null;
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ Calendar startOfToday = Calendar.getInstance();
+ startOfToday.set(Calendar.HOUR_OF_DAY, 0);
+ startOfToday.set(Calendar.MINUTE, 0);
+ startOfToday.set(Calendar.SECOND, 0);
+ startOfToday.set(Calendar.MILLISECOND, 0);
+
+ Calendar endOfToday = Calendar.getInstance();
+ endOfToday.setTime(startOfToday.getTime());
+ endOfToday.add(Calendar.DATE, 1);
+
+ Calendar startOfYesterday = Calendar.getInstance();
+ startOfYesterday.setTime(startOfToday.getTime());
+ startOfYesterday.add(Calendar.DATE, -1);
+
+ Calendar startOfMonth = Calendar.getInstance();
+ startOfMonth.setTime(startOfToday.getTime());
+ startOfMonth.set(Calendar.DATE, 1);
+
+ int sumToday = paymentDao.getPaymentAmount(startOfToday.getTime(), endOfToday.getTime());
+ int sumYesterday = paymentDao.getPaymentAmount(startOfYesterday.getTime(), startOfToday.getTime());
+ int sumMonth = paymentDao.getPaymentAmount(startOfMonth.getTime(), endOfToday.getTime());
+ int dayAverageThisMonth = sumMonth / startOfToday.get(Calendar.DATE);
+
+ map.put("sumToday", sumToday);
+ map.put("sumYesterday", sumYesterday);
+ map.put("sumMonth", sumMonth);
+ map.put("dayAverageThisMonth", dayAverageThisMonth);
+
+ return new ModelAndView("backend/payment", "model", map);
+ }
+
+ public ModelAndView requestLicense(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ String email = request.getParameter("email");
+ boolean valid = email != null && isLicenseValid(email, System.currentTimeMillis());
+ if (valid) {
+ EmailSession session = new EmailSession();
+ licenseGenerator.sendLicenseTo(email, session);
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ map.put("email", email);
+ map.put("valid", valid);
+
+ return new ModelAndView("backend/requestLicense", "model", map);
+ }
+
+ public ModelAndView whitelist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ if (!authenticate(request, response)) {
+ return null;
+ }
+
+ Date newerThan = MultiController.LICENSE_DATE_THRESHOLD;
+ Integer days = ServletRequestUtils.getIntParameter(request, "days");
+ if (days != null) {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DATE, -days);
+ newerThan = cal.getTime();
+ }
+ whitelistGenerator.generate(newerThan);
+ return null;
+ }
+
+ private boolean authenticate(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException, IOException {
+ String password = ServletRequestUtils.getRequiredStringParameter(request, "p");
+ if (!password.equals(Util.getPassword("backendpwd.txt"))) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isLicenseValid(String email, Long date) {
+ if (email == null || date == null) {
+ return false;
+ }
+
+ if (paymentDao.isBlacklisted(email)) {
+ return false;
+ }
+
+ // Always accept licenses that are older than 2010-06-19.
+ if (date < LICENSE_DATE_THRESHOLD.getTime()) {
+ return true;
+ }
+
+ return paymentDao.getPaymentByEmail(email) != null || paymentDao.isWhitelisted(email);
+ }
+
+ public void setDaoHelper(DaoHelper daoHelper) {
+ this.daoHelper = daoHelper;
+ }
+
+ public void setPaymentDao(PaymentDao paymentDao) {
+ this.paymentDao = paymentDao;
+ }
+
+ public void setWhitelistGenerator(WhitelistGenerator whitelistGenerator) {
+ this.whitelistGenerator = whitelistGenerator;
+ }
+
+ public void setLicenseGenerator(LicenseGenerator licenseGenerator) {
+ this.licenseGenerator = licenseGenerator;
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionController.java
new file mode 100644
index 00000000..3f36ec6d
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionController.java
@@ -0,0 +1,155 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.controller;
+
+import net.sourceforge.subsonic.backend.dao.RedirectionDao;
+import net.sourceforge.subsonic.backend.domain.Redirection;
+import static net.sourceforge.subsonic.backend.controller.RedirectionManagementController.RESERVED_REDIRECTS;
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.Date;
+import java.util.Enumeration;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Redirects vanity URLs (such as http://sindre.subsonic.org).
+ *
+ * @author Sindre Mehus
+ */
+public class RedirectionController implements Controller {
+
+ private static final Logger LOG = Logger.getLogger(RedirectionController.class);
+ private RedirectionDao redirectionDao;
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ String redirectFrom = getRedirectFrom(request);
+ if (RESERVED_REDIRECTS.containsKey(redirectFrom)) {
+ LOG.info("Reserved redirection: " + redirectFrom);
+ return new ModelAndView(new RedirectView(RESERVED_REDIRECTS.get(redirectFrom)));
+ }
+
+ Redirection redirection = redirectFrom == null ? null : redirectionDao.getRedirection(redirectFrom);
+
+ if (redirection == null) {
+ LOG.info("No redirection found: " + redirectFrom);
+ return new ModelAndView(new RedirectView("http://subsonic.org/pages"));
+ }
+
+ redirection.setLastRead(new Date());
+ redirection.setReadCount(redirection.getReadCount() + 1);
+ redirectionDao.updateRedirection(redirection);
+
+ // Check for trial expiration (unless called from REST client for which the Subsonic server manages trial expiry).
+ if (isTrialExpired(redirection) && !isREST(request)) {
+ LOG.info("Expired redirection: " + redirectFrom);
+ return new ModelAndView(new RedirectView("http://subsonic.org/pages/redirect-expired.jsp?redirectFrom=" +
+ redirectFrom + "&expired=" + redirection.getTrialExpires().getTime()));
+ }
+
+ String requestUrl = getFullRequestURL(request);
+ String to = StringUtils.removeEnd(getRedirectTo(request, redirection), "/");
+ String redirectTo = requestUrl.replaceFirst("http://" + redirectFrom + "\\.subsonic\\.org", to);
+ LOG.info("Redirecting from " + requestUrl + " to " + redirectTo);
+
+ return new ModelAndView(new RedirectView(redirectTo));
+ }
+
+ private String getRedirectTo(HttpServletRequest request, Redirection redirection) {
+
+ // If the request comes from within the same LAN as the destination Subsonic
+ // server, redirect using the local IP address of the server.
+
+ String localRedirectTo = redirection.getLocalRedirectTo();
+ if (localRedirectTo != null) {
+ try {
+ URL url = new URL(redirection.getRedirectTo());
+ if (url.getHost().equals(request.getRemoteAddr())) {
+ return localRedirectTo;
+ }
+ } catch (Throwable x) {
+ LOG.error("Malformed local redirect URL.", x);
+ }
+ }
+
+ return redirection.getRedirectTo();
+ }
+
+ private boolean isTrialExpired(Redirection redirection) {
+ return redirection.isTrial() && redirection.getTrialExpires() != null && redirection.getTrialExpires().before(new Date());
+ }
+
+ private boolean isREST(HttpServletRequest request) {
+ return request.getParameter("c") != null;
+ }
+
+ private String getFullRequestURL(HttpServletRequest request) throws UnsupportedEncodingException {
+ StringBuilder builder = new StringBuilder(request.getRequestURL());
+
+ // For backwards compatibility; return query parameters in exact same sequence.
+ if ("GET".equalsIgnoreCase(request.getMethod())) {
+ if (request.getQueryString() != null) {
+ builder.append("?").append(request.getQueryString());
+ }
+ return builder.toString();
+ }
+
+ builder.append("?");
+
+ Enumeration<?> paramNames = request.getParameterNames();
+ while (paramNames.hasMoreElements()) {
+ String paramName = (String) paramNames.nextElement();
+ String[] paramValues = request.getParameterValues(paramName);
+ for (String paramValue : paramValues) {
+ String p = URLEncoder.encode(paramValue, "UTF-8");
+ builder.append(paramName).append("=").append(p).append("&");
+ }
+ }
+
+ return builder.toString();
+ }
+
+ private String getRedirectFrom(HttpServletRequest request) throws MalformedURLException {
+ URL url = new URL(request.getRequestURL().toString());
+ String host = url.getHost();
+
+ String redirectFrom;
+ if (host.contains(".")) {
+ redirectFrom = StringUtils.substringBefore(host, ".");
+ } else {
+ // For testing.
+ redirectFrom = request.getParameter("redirectFrom");
+ }
+
+ return StringUtils.lowerCase(redirectFrom);
+ }
+
+ public void setRedirectionDao(RedirectionDao redirectionDao) {
+ this.redirectionDao = redirectionDao;
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionManagementController.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionManagementController.java
new file mode 100644
index 00000000..01ff90fa
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/controller/RedirectionManagementController.java
@@ -0,0 +1,273 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.controller;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.HttpStatus;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.params.HttpConnectionParams;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
+
+import net.sourceforge.subsonic.backend.dao.RedirectionDao;
+import net.sourceforge.subsonic.backend.domain.Redirection;
+
+/**
+ * @author Sindre Mehus
+ */
+public class RedirectionManagementController extends MultiActionController {
+
+ private static final Logger LOG = Logger.getLogger(RedirectionManagementController.class);
+
+ public static final Map<String,String> RESERVED_REDIRECTS = new HashMap<String, String>();
+
+ static {
+ RESERVED_REDIRECTS.put("forum", "http://www.activeobjects.no/subsonic/forum/index.php");
+ RESERVED_REDIRECTS.put("www", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("web", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("ftp", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("mail", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("s", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("subsonic", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("gosubsonic", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("android", "http://www.subsonic.org/pages/android.jsp");
+ RESERVED_REDIRECTS.put("iphone", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("subair", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("m", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("link", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("share", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("mobile", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("mobil", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("phone", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("wap", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("db", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("shop", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("wiki", "http://www.subsonic.org/pages/index.jsp");
+ RESERVED_REDIRECTS.put("test", "http://www.subsonic.org/pages/index.jsp");
+ }
+
+ private RedirectionDao redirectionDao;
+
+ public void register(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ String redirectFrom = StringUtils.lowerCase(ServletRequestUtils.getRequiredStringParameter(request, "redirectFrom"));
+ String licenseHolder = ServletRequestUtils.getStringParameter(request, "licenseHolder");
+ String serverId = ServletRequestUtils.getRequiredStringParameter(request, "serverId");
+ int port = ServletRequestUtils.getRequiredIntParameter(request, "port");
+ Integer localPort = ServletRequestUtils.getIntParameter(request, "localPort");
+ String localIp = ServletRequestUtils.getStringParameter(request, "localIp");
+ String contextPath = ServletRequestUtils.getRequiredStringParameter(request, "contextPath");
+ boolean trial = ServletRequestUtils.getBooleanParameter(request, "trial", false);
+
+ Date now = new Date();
+ Date trialExpires = null;
+ if (trial) {
+ trialExpires = new Date(ServletRequestUtils.getRequiredLongParameter(request, "trialExpires"));
+ }
+
+ if (RESERVED_REDIRECTS.containsKey(redirectFrom)) {
+ sendError(response, "\"" + redirectFrom + "\" is a reserved address. Please select another.");
+ return;
+ }
+
+ if (!redirectFrom.matches("(\\w|\\-)+")) {
+ sendError(response, "Illegal characters present in \"" + redirectFrom + "\". Please select another.");
+ return;
+ }
+
+ String host = request.getRemoteAddr();
+ URL url = new URL("http", host, port, "/" + contextPath);
+ String redirectTo = url.toExternalForm();
+
+ String localRedirectTo = null;
+ if (localIp != null && localPort != null) {
+ URL localUrl = new URL("http", localIp, localPort, "/" + contextPath);
+ localRedirectTo = localUrl.toExternalForm();
+ }
+
+ Redirection redirection = redirectionDao.getRedirection(redirectFrom);
+ if (redirection == null) {
+
+ // Delete other redirects for same server ID.
+ redirectionDao.deleteRedirectionsByServerId(serverId);
+
+ redirection = new Redirection(0, licenseHolder, serverId, redirectFrom, redirectTo, localRedirectTo, trial, trialExpires, now, null, 0);
+ redirectionDao.createRedirection(redirection);
+ LOG.info("Created " + redirection);
+
+ } else {
+
+ boolean sameServerId = serverId.equals(redirection.getServerId());
+ boolean sameLicenseHolder = licenseHolder != null && licenseHolder.equals(redirection.getLicenseHolder());
+
+ // Note: A licensed user can take over any expired trial domain.
+ boolean existingTrialExpired = redirection.getTrialExpires() != null && redirection.getTrialExpires().before(now);
+
+ if (sameServerId || sameLicenseHolder || (existingTrialExpired && !trial)) {
+ redirection.setLicenseHolder(licenseHolder);
+ redirection.setServerId(serverId);
+ redirection.setRedirectFrom(redirectFrom);
+ redirection.setRedirectTo(redirectTo);
+ redirection.setLocalRedirectTo(localRedirectTo);
+ redirection.setTrial(trial);
+ redirection.setTrialExpires(trialExpires);
+ redirection.setLastUpdated(now);
+ redirectionDao.updateRedirection(redirection);
+ LOG.info("Updated " + redirection);
+ } else {
+ sendError(response, "The web address \"" + redirectFrom + "\" is already in use. Please select another.");
+ }
+ }
+ }
+
+ public void unregister(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String serverId = ServletRequestUtils.getStringParameter(request, "serverId");
+ if (!StringUtils.isEmpty(serverId)) {
+ redirectionDao.deleteRedirectionsByServerId(serverId);
+ }
+ }
+
+ public void get(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String redirectFrom = StringUtils.lowerCase(ServletRequestUtils.getRequiredStringParameter(request, "redirectFrom"));
+
+ Redirection redirection = redirectionDao.getRedirection(redirectFrom);
+ if (redirection == null) {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Web address " + redirectFrom + ".subsonic.org not registered.");
+ return;
+ }
+
+ PrintWriter writer = response.getWriter();
+ String url = redirection.getRedirectTo();
+ if (!url.endsWith("/")) {
+ url += "/";
+ }
+ writer.println(url);
+
+ url = redirection.getLocalRedirectTo();
+ if (!url.endsWith("/")) {
+ url += "/";
+ }
+ writer.println(url);
+ }
+
+ public void test(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String redirectFrom = StringUtils.lowerCase(ServletRequestUtils.getRequiredStringParameter(request, "redirectFrom"));
+ PrintWriter writer = response.getWriter();
+
+ Redirection redirection = redirectionDao.getRedirection(redirectFrom);
+ String webAddress = redirectFrom + ".subsonic.org";
+ if (redirection == null) {
+ writer.print("Web address " + webAddress + " not registered.");
+ return;
+ }
+
+ if (redirection.getTrialExpires() != null && redirection.getTrialExpires().before(new Date())) {
+ writer.print("Trial period expired. Please donate to activate web address.");
+ return;
+ }
+
+ String url = redirection.getRedirectTo();
+ if (!url.endsWith("/")) {
+ url += "/";
+ }
+ url += "icons/favicon.ico";
+
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 15000);
+ HttpGet method = new HttpGet(url);
+
+ try {
+ HttpResponse resp = client.execute(method);
+ StatusLine status = resp.getStatusLine();
+
+ if (status.getStatusCode() == HttpStatus.SC_OK) {
+ String msg = webAddress + " responded successfully.";
+ writer.print(msg);
+ LOG.info(msg);
+ } else {
+ String msg = webAddress + " returned HTTP error code " + status.getStatusCode() + " " + status.getReasonPhrase();
+ writer.print(msg);
+ LOG.info(msg);
+ }
+ } catch (SSLPeerUnverifiedException x) {
+ String msg = webAddress + " responded successfully, but could not authenticate it.";
+ writer.print(msg);
+ LOG.info(msg);
+
+ } catch (Throwable x) {
+ String msg = webAddress + " is registered, but could not connect to it. (" + x.getClass().getSimpleName() + ")";
+ writer.print(msg);
+ LOG.info(msg);
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+
+ private void sendError(HttpServletResponse response, String message) throws IOException {
+ response.getWriter().print(message);
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ }
+
+ public void dump(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ File file = File.createTempFile("redirections", ".txt");
+ PrintWriter writer = new PrintWriter(file, "UTF-8");
+ try {
+ int offset = 0;
+ int count = 100;
+ while (true) {
+ List<Redirection> redirections = redirectionDao.getAllRedirections(offset, count);
+ if (redirections.isEmpty()) {
+ break;
+ }
+ offset += redirections.size();
+ for (Redirection redirection : redirections) {
+ writer.println(redirection);
+ }
+ }
+ LOG.info("Dumped redirections to " + file.getAbsolutePath());
+ } finally {
+ IOUtils.closeQuietly(writer);
+ }
+ }
+
+ public void setRedirectionDao(RedirectionDao redirectionDao) {
+ this.redirectionDao = redirectionDao;
+ }
+} \ No newline at end of file
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/AbstractDao.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/AbstractDao.java
new file mode 100644
index 00000000..86830b7c
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/AbstractDao.java
@@ -0,0 +1,57 @@
+package net.sourceforge.subsonic.backend.dao;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.util.List;
+
+/**
+ * Abstract superclass for all DAO's.
+ *
+ * @author Sindre Mehus
+ */
+public class AbstractDao {
+ private DaoHelper daoHelper;
+
+ /**
+ * Returns a JDBC template for performing database operations.
+ * @return A JDBC template.
+ */
+ public JdbcTemplate getJdbcTemplate() {
+ return daoHelper.getJdbcTemplate();
+ }
+
+ protected String questionMarks(String columns) {
+ int count = columns.split(", ").length;
+ StringBuffer buf = new StringBuffer();
+ for (int i = 0; i < count; i++) {
+ buf.append('?');
+ if (i < count - 1) {
+ buf.append(", ");
+ }
+ }
+ return buf.toString();
+ }
+
+ protected int update(String sql, Object... args) {
+ return getJdbcTemplate().update(sql, args);
+ }
+
+ protected <T> List<T> query(String sql, RowMapper rowMapper, Object... args) {
+ return getJdbcTemplate().query(sql, args, rowMapper);
+ }
+
+ protected <T> T queryOne(String sql, RowMapper rowMapper, Object... args) {
+ List<T> result = query(sql, rowMapper, args);
+ return result.isEmpty() ? null : result.get(0);
+ }
+
+ protected Integer queryForInt(String sql, Integer defaultValue, Object... args) {
+ List<Integer> result = getJdbcTemplate().queryForList(sql, args, Integer.class);
+ return result.isEmpty() ? defaultValue : result.get(0) == null ? defaultValue : result.get(0);
+ }
+
+ public void setDaoHelper(DaoHelper daoHelper) {
+ this.daoHelper = daoHelper;
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/DaoHelper.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/DaoHelper.java
new file mode 100644
index 00000000..2f5911f9
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/DaoHelper.java
@@ -0,0 +1,84 @@
+package net.sourceforge.subsonic.backend.dao;
+
+import net.sourceforge.subsonic.backend.dao.schema.Schema;
+import net.sourceforge.subsonic.backend.dao.schema.Schema10;
+import net.sourceforge.subsonic.backend.dao.schema.Schema20;
+import net.sourceforge.subsonic.backend.Util;
+import org.apache.log4j.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+import javax.sql.DataSource;
+import java.io.File;
+
+/**
+ * DAO helper class which creates the data source, and updates the database schema.
+ *
+ * @author Sindre Mehus
+ */
+public class DaoHelper {
+
+ private static final Logger LOG = Logger.getLogger(DaoHelper.class);
+
+ private Schema[] schemas = {new Schema10(), new Schema20()};
+ private DataSource dataSource;
+ private static boolean shutdownHookAdded;
+
+ public DaoHelper() {
+ dataSource = createDataSource();
+ checkDatabase();
+ addShutdownHook();
+ }
+
+ private void addShutdownHook() {
+ if (shutdownHookAdded) {
+ return;
+ }
+ shutdownHookAdded = true;
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ System.err.println("Shutting down database.");
+ try {
+ getJdbcTemplate().execute("shutdown");
+ System.err.println("Done.");
+ } catch (Throwable x) {
+ System.err.println("Failed to shut down database.");
+ x.printStackTrace();
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns a JDBC template for performing database operations.
+ *
+ * @return A JDBC template.
+ */
+ public JdbcTemplate getJdbcTemplate() {
+ return new JdbcTemplate(dataSource);
+ }
+
+ private DataSource createDataSource() {
+ File home = Util.getBackendHome();
+ DriverManagerDataSource ds = new DriverManagerDataSource();
+ ds.setDriverClassName("org.hsqldb.jdbcDriver");
+ ds.setUrl("jdbc:hsqldb:file:" + home.getPath() + "/db/subsonic-backend");
+ ds.setUsername("sa");
+ ds.setPassword("");
+
+ return ds;
+ }
+
+ private void checkDatabase() {
+ LOG.info("Checking database schema.");
+ try {
+ for (Schema schema : schemas) {
+ schema.execute(getJdbcTemplate());
+ }
+ LOG.info("Done checking database schema.");
+ } catch (Exception x) {
+ LOG.error("Failed to initialize database.", x);
+ }
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/PaymentDao.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/PaymentDao.java
new file mode 100644
index 00000000..e9f2eb21
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/PaymentDao.java
@@ -0,0 +1,125 @@
+package net.sourceforge.subsonic.backend.dao;
+
+import net.sourceforge.subsonic.backend.domain.Payment;
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+import org.springframework.jdbc.core.simple.ParameterizedSingleColumnRowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Provides database services for PayPal payments.
+ *
+ * @author Sindre Mehus
+ */
+public class PaymentDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(PaymentDao.class);
+ private static final String COLUMNS = "id, transaction_id, transaction_type, item, " +
+ "payment_type, payment_status, payment_amount, payment_currency, " +
+ "payer_email, payer_email_lower, payer_first_name, payer_last_name, payer_country, " +
+ "processing_status, created, last_updated";
+
+ private RowMapper paymentRowMapper = new PaymentRowMapper();
+ private RowMapper listRowMapper = new ParameterizedSingleColumnRowMapper<Integer>();
+
+ /**
+ * Returns the payment with the given transaction ID.
+ *
+ * @param transactionId The transaction ID.
+ * @return The payment or <code>null</code> if not found.
+ */
+ public Payment getPaymentByTransactionId(String transactionId) {
+ String sql = "select " + COLUMNS + " from payment where transaction_id=?";
+ return queryOne(sql, paymentRowMapper, transactionId);
+ }
+
+ /**
+ * Returns the payment with the given payer email.
+ *
+ * @param email The payer email.
+ * @return The payment or <code>null</code> if not found.
+ */
+ public Payment getPaymentByEmail(String email) {
+ if (email == null) {
+ return null;
+ }
+ String sql = "select " + COLUMNS + " from payment where payer_email_lower=?";
+ return queryOne(sql, paymentRowMapper, email.toLowerCase());
+ }
+
+ /**
+ * Returns all payments with the given processing status.
+ *
+ * @param status The status.
+ * @return List of payments.
+ */
+ public List<Payment> getPaymentsByProcessingStatus(Payment.ProcessingStatus status) {
+ return query("select " + COLUMNS + " from payment where processing_status=?", paymentRowMapper, status.name());
+ }
+
+ /**
+ * Creates a new payment.
+ *
+ * @param payment The payment to create.
+ */
+ public void createPayment(Payment payment) {
+ String sql = "insert into payment (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")";
+ update(sql, null, payment.getTransactionId(), payment.getTransactionType(), payment.getItem(),
+ payment.getPaymentType(), payment.getPaymentStatus(), payment.getPaymentAmount(),
+ payment.getPaymentCurrency(), payment.getPayerEmail(), StringUtils.lowerCase(payment.getPayerEmail()),
+ payment.getPayerFirstName(), payment.getPayerLastName(), payment.getPayerCountry(),
+ payment.getProcessingStatus().name(), payment.getCreated(), payment.getLastUpdated());
+ LOG.info("Created " + payment);
+ }
+
+ /**
+ * Updates the given payment.
+ *
+ * @param payment The payment to update.
+ */
+ public void updatePayment(Payment payment) {
+ String sql = "update payment set transaction_type=?, item=?, payment_type=?, payment_status=?, " +
+ "payment_amount=?, payment_currency=?, payer_email=?, payer_email_lower=?, payer_first_name=?, payer_last_name=?, " +
+ "payer_country=?, processing_status=?, created=?, last_updated=? where id=?";
+ update(sql, payment.getTransactionType(), payment.getItem(), payment.getPaymentType(), payment.getPaymentStatus(),
+ payment.getPaymentAmount(), payment.getPaymentCurrency(), payment.getPayerEmail(), StringUtils.lowerCase(payment.getPayerEmail()),
+ payment.getPayerFirstName(), payment.getPayerLastName(), payment.getPayerCountry(), payment.getProcessingStatus().name(),
+ payment.getCreated(), payment.getLastUpdated(), payment.getId());
+ LOG.info("Updated " + payment);
+ }
+
+ public int getPaymentAmount(Date from, Date to) {
+ String sql = "select sum(payment_amount) from payment where created between ? and ?";
+ return queryForInt(sql, 0, from, to);
+ }
+
+ public boolean isBlacklisted(String email) {
+ String sql = "select 1 from blacklist where email=?";
+ return queryOne(sql, listRowMapper, StringUtils.lowerCase(email)) != null;
+ }
+
+ public boolean isWhitelisted(String email) {
+ String sql = "select 1 from whitelist where email=?";
+ return queryOne(sql, listRowMapper, StringUtils.lowerCase(email)) != null;
+ }
+
+ public void whitelist(String email) {
+ update("insert into whitelist(email) values (?)", StringUtils.lowerCase(email));
+ }
+
+ private static class PaymentRowMapper implements ParameterizedRowMapper<Payment> {
+
+ public Payment mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Payment(rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5),
+ rs.getString(6), rs.getInt(7), rs.getString(8), rs.getString(9), rs.getString(11),
+ rs.getString(12), rs.getString(13), Payment.ProcessingStatus.valueOf(rs.getString(14)),
+ rs.getTimestamp(15), rs.getTimestamp(16));
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/RedirectionDao.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/RedirectionDao.java
new file mode 100644
index 00000000..edd73222
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/RedirectionDao.java
@@ -0,0 +1,96 @@
+package net.sourceforge.subsonic.backend.dao;
+
+import net.sourceforge.subsonic.backend.domain.Redirection;
+import org.apache.log4j.Logger;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides database services for xxx.subsonic.org redirections.
+ *
+ * @author Sindre Mehus
+ */
+public class RedirectionDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(RedirectionDao.class);
+ private static final String COLUMNS = "id, license_holder, server_id, redirect_from, redirect_to, local_redirect_to, trial, trial_expires, last_updated, last_read, read_count";
+
+ private RedirectionRowMapper rowMapper = new RedirectionRowMapper();
+
+ /**
+ * Returns the redirection with the given "redirect from".
+ *
+ * @param redirectFrom The "redirect from" string.
+ * @return The redirection or <code>null</code> if not found.
+ */
+ public Redirection getRedirection(String redirectFrom) {
+ String sql = "select " + COLUMNS + " from redirection where redirect_from=?";
+ return queryOne(sql, rowMapper, redirectFrom);
+ }
+
+ /**
+ * Returns all redirections with respect to the given row offset and count.
+ *
+ * @param offset Number of rows to skip.
+ * @param count Maximum number of rows to return.
+ * @return Redirections with respect to the given row offset and count.
+ */
+ public List<Redirection> getAllRedirections(int offset, int count) {
+ if (count < 1) {
+ return new ArrayList<Redirection>();
+ }
+ String sql = "select " + COLUMNS + " from redirection " +
+ "order by id " +
+ "limit " + count + " offset " + offset;
+ return query(sql, rowMapper);
+ }
+
+ /**
+ * Creates a new redirection.
+ *
+ * @param redirection The redirection to create.
+ */
+ public void createRedirection(Redirection redirection) {
+ String sql = "insert into redirection (" + COLUMNS + ") values (null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ update(sql, redirection.getLicenseHolder(), redirection.getServerId(), redirection.getRedirectFrom(),
+ redirection.getRedirectTo(), redirection.getLocalRedirectTo(), redirection.isTrial(),
+ redirection.getTrialExpires(), redirection.getLastUpdated(),
+ redirection.getLastRead(), redirection.getReadCount());
+ LOG.info("Created redirection " + redirection.getRedirectFrom() + " -> " + redirection.getRedirectTo());
+ }
+
+ /**
+ * Updates the given redirection.
+ *
+ * @param redirection The redirection to update.
+ */
+ public void updateRedirection(Redirection redirection) {
+ String sql = "update redirection set license_holder=?, server_id=?, redirect_from=?, redirect_to=?, " +
+ "local_redirect_to=?, trial=?, trial_expires=?, last_updated=?, last_read=?, read_count=? where id=?";
+ update(sql, redirection.getLicenseHolder(), redirection.getServerId(), redirection.getRedirectFrom(),
+ redirection.getRedirectTo(), redirection.getLocalRedirectTo(), redirection.isTrial(), redirection.getTrialExpires(),
+ redirection.getLastUpdated(), redirection.getLastRead(), redirection.getReadCount(), redirection.getId());
+ }
+
+ /**
+ * Deletes all redirections with the given server ID.
+ *
+ * @param serverId The server ID.
+ */
+ public void deleteRedirectionsByServerId(String serverId) {
+ update("delete from redirection where server_id=?", serverId);
+ LOG.info("Deleted redirections for server ID " + serverId);
+ }
+
+ private static class RedirectionRowMapper implements ParameterizedRowMapper<Redirection> {
+ public Redirection mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Redirection(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5),
+ rs.getString(6), rs.getBoolean(7), rs.getTimestamp(8), rs.getTimestamp(9), rs.getTimestamp(10),
+ rs.getInt(11));
+ }
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema.java
new file mode 100644
index 00000000..850b82da
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema.java
@@ -0,0 +1,66 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.dao.schema;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ *
+ * @author Sindre Mehus
+ */
+public abstract class Schema {
+
+ /**
+ * Executes this schema.
+ * @param template The JDBC template to use.
+ */
+ public abstract void execute(JdbcTemplate template);
+
+ /**
+ * Returns whether the given table exists.
+ * @param template The JDBC template to use.
+ * @param table The table in question.
+ * @return Whether the table exists.
+ */
+ protected boolean tableExists(JdbcTemplate template, String table) {
+ try {
+ template.execute("select 1 from " + table);
+ } catch (Exception x) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether the given column in the given table exists.
+ * @param template The JDBC template to use.
+ * @param column The column in question.
+ * @param table The table in question.
+ * @return Whether the column exists.
+ */
+ protected boolean columnExists(JdbcTemplate template, String column, String table) {
+ try {
+ template.execute("select " + column + " from " + table + " where 1 = 0");
+ } catch (Exception x) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema10.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema10.java
new file mode 100644
index 00000000..29c4c492
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema10.java
@@ -0,0 +1,100 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.dao.schema;
+
+import org.apache.log4j.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic Backend version 1.0.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema10 extends Schema {
+ private static final Logger LOG = Logger.getLogger(Schema10.class);
+
+ public void execute(JdbcTemplate template) {
+
+ /*
+ Example row 1:
+
+ id: 123
+ license_holder: sindre@activeobjects.no
+ server_id: 972342834928656
+ redirect_from: sindre
+ redirect_to: http://23.45.123.56:8080/subsonic
+ local_redirect_to: http://192.168.0.7:80/subsonic
+ trial: false
+ trial_expires: null
+
+ Example row 2:
+
+ id: 124
+ license_holder: null
+ server_id: 72121983567129
+ redirect_from: joe
+ redirect_to: http://232.21.18.14/subsonic
+ local_redirect_to: http://192.168.0.7:80/subsonic
+ trial: true
+ trial_expires: 2010-01-13 05:34:17
+ */
+
+ if (!tableExists(template, "redirection")) {
+ LOG.info("Database table 'redirection' not found. Creating it.");
+ template.execute("create cached table redirection (" +
+ "id identity," +
+ "license_holder varchar," +
+ "server_id varchar not null," +
+ "redirect_from varchar not null," +
+ "redirect_to varchar not null," +
+ "trial boolean not null," +
+ "trial_expires datetime," +
+ "last_updated datetime," +
+ "last_read datetime," +
+ "unique(redirect_from))");
+ template.execute("create index idx_redirection_redirect_from on redirection(redirect_from)");
+ template.execute("create index idx_redirection_server_id on redirection(server_id)");
+
+ createRedirection(template, "demo", "http://subsonic.org/demo");
+
+ LOG.info("Database table 'redirection' was created successfully.");
+ }
+
+ if (!columnExists(template, "local_redirect_to", "redirection")) {
+ LOG.info("Database column 'redirection.local_redirect_to' not found. Creating it.");
+ template.execute("alter table redirection " +
+ "add local_redirect_to varchar");
+ LOG.info("Database column 'redirection.local_redirect_to' was added successfully.");
+ }
+
+ if (!columnExists(template, "read_count", "redirection")) {
+ LOG.info("Database column 'redirection.read_count' not found. Creating it.");
+ template.execute("alter table redirection " +
+ "add read_count int default 0 not null");
+ LOG.info("Database column 'redirection.read_count' was added successfully.");
+ }
+ }
+
+ private void createRedirection(JdbcTemplate template, String redirectFrom, String redirectTo) {
+ template.update("insert into redirection values (null, 'sindre@activeobjects.no', '-1', ?, ?, false, null, null, null)",
+ new Object[] {redirectFrom, redirectTo});
+ LOG.info("Creating redirection from " + redirectFrom + " to " + redirectTo);
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema20.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema20.java
new file mode 100644
index 00000000..99ea679e
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/dao/schema/Schema20.java
@@ -0,0 +1,90 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.dao.schema;
+
+import org.apache.log4j.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic Backend version 2.0.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema20 extends Schema {
+ private static final Logger LOG = Logger.getLogger(Schema20.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (!tableExists(template, "payment")) {
+ LOG.info("Database table 'payment' not found. Creating it.");
+ template.execute("create cached table payment (" +
+ "id identity," +
+ "transaction_id varchar not null," +
+ "transaction_type varchar," + // cart, web_accept
+ "item varchar," +
+ "payment_type varchar," + // echeck, instant
+ "payment_status varchar," + // Completed, Pending, Denied, Failed, ...
+ "payment_amount int," +
+ "payment_currency varchar," +
+ "payer_email varchar," +
+ "payer_first_name varchar," +
+ "payer_last_name varchar," +
+ "payer_country varchar," +
+ "processing_status varchar not null," +
+ "created datetime," +
+ "last_updated datetime," +
+ "unique(transaction_id))");
+ template.execute("create index idx_payment_transaction_id on payment(transaction_id)");
+ template.execute("create index idx_payment_created on payment(created)");
+ template.execute("create index idx_payment_payer_email on payment(payer_email)");
+
+ LOG.info("Database table 'payment' was created successfully.");
+ }
+
+ if (!columnExists(template, "payer_email_lower", "payment")) {
+ LOG.info("Database column 'payment.payer_email_lower' not found. Creating it.");
+ template.execute("alter table payment " +
+ "add payer_email_lower varchar");
+ template.execute("update payment set payer_email_lower=lcase(payer_email)");
+ template.execute("create index idx_payment_payer_email_lower on payment(payer_email_lower)");
+ LOG.info("Database column 'payment.payer_email_lower' was added successfully.");
+ }
+
+ if (!tableExists(template, "whitelist")) {
+ LOG.info("Database table 'whitelist' not found. Creating it.");
+ template.execute("create cached table whitelist (" +
+ "id identity," +
+ "email varchar not null)");
+ template.execute("create index idx_whitelist_email on whitelist(email)");
+
+ LOG.info("Database table 'whitelist' was created successfully.");
+ }
+
+ if (!tableExists(template, "blacklist")) {
+ LOG.info("Database table 'blacklist' not found. Creating it.");
+ template.execute("create cached table blacklist (" +
+ "id identity," +
+ "email varchar not null)");
+ template.execute("create index idx_blacklist_email on blacklist(email)");
+
+ LOG.info("Database table 'blacklist' was created successfully.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Payment.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Payment.java
new file mode 100644
index 00000000..9e32b2cd
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Payment.java
@@ -0,0 +1,200 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.domain;
+
+import java.util.Date;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Payment {
+
+ private String id;
+ private String transactionId;
+ private String transactionType;
+ private String item;
+ private String paymentType;
+ private String paymentStatus;
+ private int paymentAmount;
+ private String paymentCurrency;
+ private String payerEmail;
+ private String payerFirstName;
+ private String payerLastName;
+ private String payerCountry;
+ private ProcessingStatus processingStatus;
+ private Date created;
+ private Date lastUpdated;
+
+ public Payment(String id, String transactionId, String transactionType, String item, String paymentType,
+ String paymentStatus, int paymentAmount, String paymentCurrency, String payerEmail,
+ String payerFirstName, String payerLastName, String payerCountry, ProcessingStatus processingStatus,
+ Date created, Date lastUpdated) {
+ this.id = id;
+ this.transactionId = transactionId;
+ this.transactionType = transactionType;
+ this.item = item;
+ this.paymentType = paymentType;
+ this.paymentStatus = paymentStatus;
+ this.paymentAmount = paymentAmount;
+ this.paymentCurrency = paymentCurrency;
+ this.payerEmail = payerEmail;
+ this.payerFirstName = payerFirstName;
+ this.payerLastName = payerLastName;
+ this.payerCountry = payerCountry;
+ this.processingStatus = processingStatus;
+ this.created = created;
+ this.lastUpdated = lastUpdated;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public void setTransactionId(String transactionId) {
+ this.transactionId = transactionId;
+ }
+
+ public String getTransactionType() {
+ return transactionType;
+ }
+
+ public void setTransactionType(String transactionType) {
+ this.transactionType = transactionType;
+ }
+
+ public String getItem() {
+ return item;
+ }
+
+ public void setItem(String item) {
+ this.item = item;
+ }
+
+ public String getPaymentType() {
+ return paymentType;
+ }
+
+ public void setPaymentType(String paymentType) {
+ this.paymentType = paymentType;
+ }
+
+ public String getPaymentStatus() {
+ return paymentStatus;
+ }
+
+ public void setPaymentStatus(String paymentStatus) {
+ this.paymentStatus = paymentStatus;
+ }
+
+ public int getPaymentAmount() {
+ return paymentAmount;
+ }
+
+ public void setPaymentAmount(int paymentAmount) {
+ this.paymentAmount = paymentAmount;
+ }
+
+ public String getPaymentCurrency() {
+ return paymentCurrency;
+ }
+
+ public void setPaymentCurrency(String paymentCurrency) {
+ this.paymentCurrency = paymentCurrency;
+ }
+
+ public String getPayerEmail() {
+ return payerEmail;
+ }
+
+ public void setPayerEmail(String payerEmail) {
+ this.payerEmail = payerEmail;
+ }
+
+ public String getPayerFirstName() {
+ return payerFirstName;
+ }
+
+ public void setPayerFirstName(String payerFirstName) {
+ this.payerFirstName = payerFirstName;
+ }
+
+ public String getPayerLastName() {
+ return payerLastName;
+ }
+
+ public void setPayerLastName(String payerLastName) {
+ this.payerLastName = payerLastName;
+ }
+
+ public String getPayerCountry() {
+ return payerCountry;
+ }
+
+ public void setPayerCountry(String payerCountry) {
+ this.payerCountry = payerCountry;
+ }
+
+ public ProcessingStatus getProcessingStatus() {
+ return processingStatus;
+ }
+
+ public void setProcessingStatus(ProcessingStatus processingStatus) {
+ this.processingStatus = processingStatus;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getLastUpdated() {
+ return lastUpdated;
+ }
+
+ public void setLastUpdated(Date lastUpdated) {
+ this.lastUpdated = lastUpdated;
+ }
+
+ @Override
+ public String toString() {
+ return "Payment{" +
+ "tx='" + transactionId + '\'' +
+ ", type='" + paymentType + '\'' +
+ ", status='" + paymentStatus + '\'' +
+ ", amount=" + paymentAmount +
+ ", email='" + payerEmail + '\'' +
+ '}';
+ }
+
+ public enum ProcessingStatus {
+ NEW,
+ COMPLETED
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Redirection.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Redirection.java
new file mode 100644
index 00000000..aa62bd75
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/domain/Redirection.java
@@ -0,0 +1,155 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.domain;
+
+import java.util.Date;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Redirection {
+
+ private int id;
+ private String licenseHolder;
+ private String serverId;
+ private String redirectFrom;
+ private String redirectTo;
+ private String localRedirectTo;
+ private boolean trial;
+ private Date trialExpires;
+ private Date lastUpdated;
+ private Date lastRead;
+ private int readCount;
+
+ public Redirection(int id, String licenseHolder, String serverId, String redirectFrom, String redirectTo,
+ String localRedirectTo, boolean trial, Date trialExpires, Date lastUpdated, Date lastRead, int readCount) {
+ this.id = id;
+ this.licenseHolder = licenseHolder;
+ this.serverId = serverId;
+ this.redirectFrom = redirectFrom;
+ this.redirectTo = redirectTo;
+ this.localRedirectTo = localRedirectTo;
+ this.trial = trial;
+ this.trialExpires = trialExpires;
+ this.lastUpdated = lastUpdated;
+ this.lastRead = lastRead;
+ this.readCount = readCount;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getLicenseHolder() {
+ return licenseHolder;
+ }
+
+ public void setLicenseHolder(String licenseHolder) {
+ this.licenseHolder = licenseHolder;
+ }
+
+ public String getServerId() {
+ return serverId;
+ }
+
+ public void setServerId(String serverId) {
+ this.serverId = serverId;
+ }
+
+ public String getRedirectFrom() {
+ return redirectFrom;
+ }
+
+ public void setRedirectFrom(String redirectFrom) {
+ this.redirectFrom = redirectFrom;
+ }
+
+ public String getRedirectTo() {
+ return redirectTo;
+ }
+
+ public void setRedirectTo(String redirectTo) {
+ this.redirectTo = redirectTo;
+ }
+
+ public String getLocalRedirectTo() {
+ return localRedirectTo;
+ }
+
+ public void setLocalRedirectTo(String localRedirectTo) {
+ this.localRedirectTo = localRedirectTo;
+ }
+
+ public boolean isTrial() {
+ return trial;
+ }
+
+ public void setTrial(boolean trial) {
+ this.trial = trial;
+ }
+
+ public Date getTrialExpires() {
+ return trialExpires;
+ }
+
+ public void setTrialExpires(Date trialExpires) {
+ this.trialExpires = trialExpires;
+ }
+
+ public Date getLastUpdated() {
+ return lastUpdated;
+ }
+
+ public void setLastUpdated(Date lastUpdated) {
+ this.lastUpdated = lastUpdated;
+ }
+
+ public Date getLastRead() {
+ return lastRead;
+ }
+
+ public void setLastRead(Date lastRead) {
+ this.lastRead = lastRead;
+ }
+
+ public int getReadCount() {
+ return readCount;
+ }
+
+ public void setReadCount(int readCount) {
+ this.readCount = readCount;
+ }
+
+ @Override
+ public String toString() {
+ return "Redirection{" +
+ "id=" + id +
+ ", licenseHolder='" + licenseHolder + '\'' +
+ ", serverId='" + serverId + '\'' +
+ ", redirectFrom='" + redirectFrom + '\'' +
+ ", redirectTo='" + redirectTo + '\'' +
+ ", localRedirectTo='" + localRedirectTo + '\'' +
+ ", trial=" + trial +
+ ", trialExpires=" + trialExpires +
+ ", lastUpdated=" + lastUpdated +
+ ", lastRead=" + lastRead +
+ ", readCount=" + readCount +
+ '}';
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/EmailSession.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/EmailSession.java
new file mode 100644
index 00000000..cfd96651
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/EmailSession.java
@@ -0,0 +1,108 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.backend.service;
+
+import net.sourceforge.subsonic.backend.Util;
+
+import javax.mail.Folder;
+import javax.mail.Session;
+import javax.mail.Message;
+import javax.mail.Address;
+import javax.mail.MessagingException;
+import javax.mail.Store;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.AddressException;
+import java.util.Properties;
+import java.util.List;
+
+/**
+ * @author Sindre Mehus
+ */
+public class EmailSession {
+
+ private static final String SMTP_MAIL_SERVER = "smtp.gmail.com";
+ private static final String POP_MAIL_SERVER = "pop.gmail.com";
+ private static final String IMAP_MAIL_SERVER = "imap.gmail.com";
+ private static final String USER = "subsonic@activeobjects.no";
+
+ private Session session;
+ private String password;
+
+ public EmailSession() throws Exception {
+ Properties props = new Properties();
+// props.setProperty("mail.debug", "true");
+ props.setProperty("mail.store.protocol", "pop3s");
+ props.setProperty("mail.smtps.host", SMTP_MAIL_SERVER);
+ props.setProperty("mail.smtps.auth", "true");
+ props.setProperty("mail.smtps.timeout", "10000");
+ props.setProperty("mail.smtps.connectiontimeout", "10000");
+ props.setProperty("mail.pop3s.timeout", "10000");
+ props.setProperty("mail.pop3s.connectiontimeout", "10000");
+
+ session = Session.getDefaultInstance(props, null);
+ password = Util.getPassword("gmailpwd.txt");
+ }
+
+ public void sendMessage(String from, List<String> to, List<String> cc, List<String> bcc, List<String> replyTo,
+ String subject, String text) throws MessagingException {
+ Message message = new MimeMessage(session);
+
+ message.setFrom(new InternetAddress(from));
+ message.setReplyTo(new Address[]{new InternetAddress(from)});
+ message.setRecipients(Message.RecipientType.TO, convertAddress(to));
+ message.setRecipients(Message.RecipientType.CC, convertAddress(cc));
+ message.setRecipients(Message.RecipientType.BCC, convertAddress(bcc));
+ message.setReplyTo(convertAddress(replyTo));
+ message.setSubject(subject);
+ message.setText(text);
+
+ // Send the message
+ Transport transport = null;
+ try {
+ transport = session.getTransport("smtps");
+ transport.connect(USER, password);
+ transport.sendMessage(message, message.getAllRecipients());
+ } finally {
+ if (transport != null) {
+ transport.close();
+ }
+ }
+ }
+
+ public Folder getFolder(String name) throws Exception {
+ Store store = session.getStore("imaps");
+ store.connect(IMAP_MAIL_SERVER, USER, password);
+ Folder folder = store.getFolder(name);
+ folder.open(Folder.READ_ONLY);
+ return folder;
+ }
+
+ private Address[] convertAddress(List<String> addresses) throws AddressException {
+ if (addresses == null) {
+ return null;
+ }
+ Address[] result = new Address[addresses.size()];
+ for (int i = 0; i < addresses.size(); i++) {
+ result[i] = new InternetAddress(addresses.get(i));
+ }
+ return result;
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/LicenseGenerator.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/LicenseGenerator.java
new file mode 100644
index 00000000..77376a8e
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/LicenseGenerator.java
@@ -0,0 +1,170 @@
+package net.sourceforge.subsonic.backend.service;
+
+import net.sourceforge.subsonic.backend.dao.PaymentDao;
+import net.sourceforge.subsonic.backend.domain.Payment;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.log4j.Logger;
+
+import javax.mail.MessagingException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Runs a task at regular intervals, checking for incoming donations and sending
+ * out license keys by email.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class LicenseGenerator {
+
+ private static final Logger LOG = Logger.getLogger(LicenseGenerator.class);
+ private static final long DELAY = 60; // One minute.
+
+ private PaymentDao paymentDao;
+ private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+
+ public void init() {
+ Runnable task = new Runnable() {
+ public void run() {
+ try {
+ LOG.info("Starting license generator.");
+ processPayments();
+ LOG.info("Completed license generator.");
+ } catch (Throwable x) {
+ LOG.error("Failed to process license emails.", x);
+ }
+ }
+ };
+ executor.scheduleWithFixedDelay(task, DELAY, DELAY, TimeUnit.SECONDS);
+ LOG.info("Scheduled license generator to run every " + DELAY + " seconds.");
+ }
+
+ private void processPayments() throws Exception {
+ List<Payment> payments = paymentDao.getPaymentsByProcessingStatus(Payment.ProcessingStatus.NEW);
+ LOG.info(payments.size() + " new payment(s).");
+ if (payments.isEmpty()) {
+ return;
+ }
+
+ EmailSession emailSession = new EmailSession();
+ for (Payment payment : payments) {
+ processPayment(payment, emailSession);
+ }
+ }
+
+ private void processPayment(Payment payment, EmailSession emailSession) {
+ try {
+ LOG.info("Processing " + payment);
+ String email = payment.getPayerEmail();
+ if (email == null) {
+ throw new Exception("Missing email address.");
+ }
+
+ boolean eligible = isEligible(payment);
+ boolean ignorable = isIgnorable(payment);
+ if (eligible) {
+ sendLicenseTo(email, emailSession);
+ LOG.info("Sent license key for " + payment);
+ } else {
+ LOG.info("Payment not eligible for " + payment);
+ }
+
+ if (eligible || ignorable) {
+ payment.setProcessingStatus(Payment.ProcessingStatus.COMPLETED);
+ payment.setLastUpdated(new Date());
+ paymentDao.updatePayment(payment);
+ }
+
+ } catch (Throwable x) {
+ LOG.error("Failed to process " + payment, x);
+ }
+ }
+
+ private boolean isEligible(Payment payment) {
+ String status = payment.getPaymentStatus();
+ if ("echeck".equalsIgnoreCase(payment.getPaymentType())) {
+ return "Pending".equalsIgnoreCase(status) || "Completed".equalsIgnoreCase(status);
+ }
+ return "Completed".equalsIgnoreCase(status);
+ }
+
+ private boolean isIgnorable(Payment payment) {
+ String status = payment.getPaymentStatus();
+ return "Denied".equalsIgnoreCase(status) ||
+ "Reversed".equalsIgnoreCase(status) ||
+ "Refunded".equalsIgnoreCase(status);
+ }
+
+ public void sendLicenseTo(String to, EmailSession emailSession) throws MessagingException {
+ emailSession.sendMessage("subsonic_donation@activeobjects.no",
+ Arrays.asList(to),
+ null,
+ Arrays.asList("subsonic_donation@activeobjects.no", "sindre@activeobjects.no"),
+ Arrays.asList("subsonic_donation@activeobjects.no"),
+ "Subsonic License",
+ createLicenseContent(to));
+ LOG.info("Sent license to " + to);
+ }
+
+ private String createLicenseContent(String to) {
+ String license = md5Hex(to.toLowerCase());
+
+ return "Dear Subsonic donor,\n" +
+ "\n" +
+ "Many thanks for your kind donation to Subsonic!\n" +
+ "Please find your license key below.\n" +
+ "\n" +
+ "Email: " + to + "\n" +
+ "License: " + license + " \n" +
+ "\n" +
+ "To install the license key, click the \"Donate\" link in the top right corner of the Subsonic web interface.\n" +
+ "\n" +
+ "More info here: http://subsonic.org/pages/getting-started.jsp#3\n" +
+ "\n" +
+ "This license is valid for personal, non-commercial of Subsonic. For commercial use, please contact us for licensing options.\n" +
+ "\n" +
+ "Thanks again for supporting the project!\n" +
+ "\n" +
+ "Best regards,\n" +
+ "The Subsonic team";
+ }
+
+ /**
+ * 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.
+ */
+ private String md5Hex(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return new String(Hex.encodeHex(md5.digest(s.getBytes("UTF-8"))));
+ } catch (Exception x) {
+ throw new RuntimeException(x.getMessage(), x);
+ }
+ }
+
+ public void setPaymentDao(PaymentDao paymentDao) {
+ this.paymentDao = paymentDao;
+ }
+
+ public static void main(String[] args) throws Exception {
+ String address = args[0];
+// String license = md5Hex(address.toLowerCase());
+// System.out.println("Email: " + address);
+// System.out.println("License: " + license);
+
+ LicenseGenerator generator = new LicenseGenerator();
+ generator.sendLicenseTo(address, new EmailSession());
+ }
+}
diff --git a/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/WhitelistGenerator.java b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/WhitelistGenerator.java
new file mode 100644
index 00000000..eef29611
--- /dev/null
+++ b/subsonic-backend/src/main/java/net/sourceforge/subsonic/backend/service/WhitelistGenerator.java
@@ -0,0 +1,53 @@
+package net.sourceforge.subsonic.backend.service;
+
+import net.sourceforge.subsonic.backend.dao.PaymentDao;
+import org.apache.log4j.Logger;
+
+import javax.mail.Address;
+import javax.mail.Folder;
+import javax.mail.Message;
+import javax.mail.internet.InternetAddress;
+import java.util.Date;
+
+/**
+ * Creates a license whitelist.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class WhitelistGenerator {
+
+ private static final Logger LOG = Logger.getLogger(WhitelistGenerator.class);
+
+ private PaymentDao paymentDao;
+
+ public void generate(Date newerThan) throws Exception {
+ LOG.info("Starting whitelist update for emails newer than " + newerThan);
+
+ EmailSession session = new EmailSession();
+ Folder folder = session.getFolder("[Gmail]/Sent Mail");
+ int n = folder.getMessageCount();
+
+ for (int i = n; i >= 0; i--) {
+ Message message = folder.getMessage(i);
+ Date date = message.getSentDate();
+ InternetAddress address = (InternetAddress) message.getRecipients(Message.RecipientType.TO)[0];
+ String recipient = address.getAddress();
+ if (date.before(newerThan)) {
+ break;
+ }
+ LOG.info(date + " " + recipient);
+
+ if (paymentDao.getPaymentByEmail(recipient) == null && !paymentDao.isWhitelisted(recipient)) {
+ paymentDao.whitelist(recipient);
+ LOG.info("WHITELISTED " + recipient);
+ }
+ }
+ folder.close(false);
+ LOG.info("Completed whitelist update.");
+ }
+
+ public void setPaymentDao(PaymentDao paymentDao) {
+ this.paymentDao = paymentDao;
+ }
+}
diff --git a/subsonic-backend/src/main/resources/log4j.properties b/subsonic-backend/src/main/resources/log4j.properties
new file mode 100644
index 00000000..a49f34e5
--- /dev/null
+++ b/subsonic-backend/src/main/resources/log4j.properties
@@ -0,0 +1,19 @@
+log4j.rootLogger=INFO, file
+
+log4j.appender.file=org.apache.log4j.RollingFileAppender
+log4j.appender.file.file=/var/subsonic-backend/subsonic-backend.log
+log4j.appender.file.maxBackupIndex=9
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=[%d{ISO8601}] %-5p %c - %m%n
+
+log4j.appender.ipn=org.apache.log4j.RollingFileAppender
+log4j.appender.ipn.file=/var/subsonic-backend/ipn.log
+log4j.appender.ipn.maxBackupIndex=9
+log4j.appender.ipn.layout=org.apache.log4j.PatternLayout
+log4j.appender.ipn.layout.ConversionPattern=[%d{ISO8601}] %-5p %c - %m%n
+
+log4j.logger.net.sourceforge.subsonic.backend.controller.IPNController=INFO, ipn
+log4j.logger.net.sourceforge.subsonic.backend.service.LicenseGenerator=INFO, ipn
+log4j.logger.net.sourceforge.subsonic.backend.dao.PaymentDao=INFO, ipn
+
+log4j.logger.org.springframework=INFO \ No newline at end of file
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/applicationContext-backend.xml b/subsonic-backend/src/main/webapp/WEB-INF/applicationContext-backend.xml
new file mode 100644
index 00000000..f1008a29
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/applicationContext-backend.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="redirectionDao" class="net.sourceforge.subsonic.backend.dao.RedirectionDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="paymentDao" class="net.sourceforge.subsonic.backend.dao.PaymentDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="daoHelper" class="net.sourceforge.subsonic.backend.dao.DaoHelper"/>
+
+ <bean id="licenseGenerator" class="net.sourceforge.subsonic.backend.service.LicenseGenerator" init-method="init">
+ <property name="paymentDao" ref="paymentDao"/>
+ </bean>
+
+ <bean id="whitelistGenerator" class="net.sourceforge.subsonic.backend.service.WhitelistGenerator">
+ <property name="paymentDao" ref="paymentDao"/>
+ </bean>
+
+</beans> \ No newline at end of file
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/db.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/db.jsp
new file mode 100644
index 00000000..c3e1eb39
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/db.jsp
@@ -0,0 +1,51 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="../head.jsp" %>
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/reset/reset.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/grid/grid.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/base/base.css">
+</head><body>
+
+<h1>Database query</h1>
+
+<form method="post" action="db.view">
+ <textarea rows="10" cols="80" name="query" style="margin-top:1em">${model.query}</textarea>
+ <input type="submit" value="OK">
+ <input type="hidden" name="p" value="${model.p}">
+</form>
+
+
+<c:if test="${not empty model.result}">
+ <h1 style="margin-top:2em">Result</h1>
+
+ <table>
+ <c:forEach items="${model.result}" var="row" varStatus="loopStatus">
+
+ <c:if test="${loopStatus.count == 1}">
+ <tr>
+ <c:forEach items="${row}" var="entry">
+ <td>${entry.key}</td>
+ </c:forEach>
+ </tr>
+ </c:if>
+ <tr>
+ <c:forEach items="${row}" var="entry">
+ <td>${entry.value}</td>
+ </c:forEach>
+ </tr>
+ </c:forEach>
+
+ </table>
+</c:if>
+
+<c:if test="${not empty model.error}">
+ <h1 style="margin-top:2em">Error</h1>
+
+ <p>
+ ${model.error}
+ </p>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/payment.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/payment.jsp
new file mode 100644
index 00000000..c72f798d
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/payment.jsp
@@ -0,0 +1,23 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="../head.jsp" %>
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/reset/reset.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/grid/grid.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/base/base.css">
+ <meta http-equiv="refresh" content="300">
+</head>
+<body>
+
+<div style="margin-left: auto; margin-right: auto;width:10em">
+ <h1 style="text-align: center;">&euro;${model.sumToday}</h1>
+
+ <div style="white-space: nowrap; text-align:center;">
+ <span title="Sum yesterday">Y <b>&euro;${model.sumYesterday}</b></span> &nbsp;&nbsp;
+ <span title="Daily average this month">M <b>&euro;${model.dayAverageThisMonth}</b></span>
+ </div>
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/requestLicense.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/requestLicense.jsp
new file mode 100644
index 00000000..c8654663
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/backend/requestLicense.jsp
@@ -0,0 +1,42 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="../head.jsp" %>
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/reset/reset.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/grid/grid.css">
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.8.0r4/build/base/base.css">
+</head>
+<body>
+
+<h1>Resend Subsonic license key</h1>
+
+<c:if test="${empty model.email}">
+ <p>Have you purchased a Subsonic license but lost the license key?</p>
+
+ <p>Enter your email address below to have it resent to you.</p>
+</c:if>
+
+<c:if test="${not empty model.email and not model.valid}">
+ <p>Sorry, no license key is associated to ${model.email}. Did you use a different email address when
+ creating the payment on PayPal?</p>
+</c:if>
+
+<c:choose>
+ <c:when test="${model.valid}">
+ <p>Your license key has been sent to ${model.email}. Didn't get it? Please remember to check your spam
+ folder.</p>
+ </c:when>
+ <c:otherwise>
+ <form method="post" action="requestLicense.view">
+ <label>Email address
+ <input type="text" size="30" name="email">
+ </label>
+ <input type="submit" value="Send license key">
+ </form>
+ </c:otherwise>
+</c:choose>
+
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/head.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/head.jsp
new file mode 100644
index 00000000..a8b170f4
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/head.jsp
@@ -0,0 +1,3 @@
+<%@ include file="include.jsp" %>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Subsonic</title>
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/jsp/include.jsp b/subsonic-backend/src/main/webapp/WEB-INF/jsp/include.jsp
new file mode 100644
index 00000000..3758aa0a
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/jsp/include.jsp
@@ -0,0 +1,7 @@
+<%@ page session="false"%>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+<%@ taglib prefix="str" uri="http://jakarta.apache.org/taglibs/string-1.1" %>
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/subsonic-backend-servlet.xml b/subsonic-backend/src/main/webapp/WEB-INF/subsonic-backend-servlet.xml
new file mode 100644
index 00000000..d166d271
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/subsonic-backend-servlet.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="multiController" class="net.sourceforge.subsonic.backend.controller.MultiController">
+ <property name="daoHelper" ref="daoHelper"/>
+ <property name="paymentDao" ref="paymentDao"/>
+ <property name="whitelistGenerator" ref="whitelistGenerator"/>
+ <property name="licenseGenerator" ref="licenseGenerator"/>
+ </bean>
+
+ <bean id="redirectionController" class="net.sourceforge.subsonic.backend.controller.RedirectionController">
+ <property name="redirectionDao" ref="redirectionDao"/>
+ </bean>
+
+ <bean id="ipnController" class="net.sourceforge.subsonic.backend.controller.IPNController">
+ <property name="paymentDao" ref="paymentDao"/>
+ </bean>
+
+ <bean id="redirectionManagementController" class="net.sourceforge.subsonic.backend.controller.RedirectionManagementController">
+ <property name="redirectionDao" ref="redirectionDao"/>
+ </bean>
+
+ <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
+ <property name="alwaysUseFullPath" value="true"/>
+ <property name="mappings">
+ <props>
+ <prop key="/backend/version.view">multiController</prop>
+ <prop key="/backend/db.view">multiController</prop>
+ <prop key="/backend/payment.view">multiController</prop>
+ <prop key="/backend/sendMail.view">multiController</prop>
+ <prop key="/backend/requestLicense.view">multiController</prop>
+ <prop key="/backend/validateLicense.view">multiController</prop>
+ <prop key="/backend/whitelist.view">multiController</prop>
+ <prop key="/backend/ipn.view">ipnController</prop>
+ <prop key="/backend/redirect/register.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/unregister.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/get.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/test.view">redirectionManagementController</prop>
+ <prop key="/backend/redirect/dump.view">redirectionManagementController</prop>
+ <prop key="/**">redirectionController</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
+ <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
+ <property name="prefix" value="/WEB-INF/jsp/"/>
+ <property name="suffix" value=".jsp"/>
+ </bean>
+
+</beans> \ No newline at end of file
diff --git a/subsonic-backend/src/main/webapp/WEB-INF/web.xml b/subsonic-backend/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000..3e69f937
--- /dev/null
+++ b/subsonic-backend/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<web-app id="subsonic-backend" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
+
+ <display-name>Subsonic Backend</display-name>
+
+ <!-- Location of application context. Used by ContextLoaderListener. -->
+ <context-param>
+ <param-name>contextConfigLocation</param-name>
+ <param-value>
+ /WEB-INF/applicationContext-backend.xml
+ </param-value>
+ </context-param>
+
+ <listener>
+ <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
+ </listener>
+
+ <servlet>
+ <servlet-name>subsonic-backend</servlet-name>
+ <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
+ <load-on-startup>1</load-on-startup>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>subsonic-backend</servlet-name>
+ <url-pattern>/</url-pattern>
+ </servlet-mapping>
+
+ <welcome-file-list>
+ <welcome-file>index.html</welcome-file>
+ <welcome-file>index.jsp</welcome-file>
+ </welcome-file-list>
+
+</web-app> \ No newline at end of file