diff --git a/README.md b/README.md
index afa583843..a15897fad 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,8 @@ Key | Description
`noticetype_transifex-issues` | Name of the notice type for Transifex string issues in the website database | `transifex-issues`
`noticetype_comment-added` | Name of the notice type for new comments in the website database | `comment-added`
`noticetype_comment-mention` | Name of the notice type for @mentions in comments in the website database | `comment-mention`
+`website_maps_path` | Base path where website maps are stored | `/var/wlmedia`
+`website_maps_slug` | Slug for the content type of comments on website maps | `wlmaps`
`deploy` | Whether this server is the real thing, not just a test environment | `true` or `false`
### The Database
diff --git a/wl/server/Command.java b/wl/server/Command.java
index 8be0e0dee..8c3ae40e0 100644
--- a/wl/server/Command.java
+++ b/wl/server/Command.java
@@ -118,11 +118,14 @@ public enum Command {
* CMD_LIST [2+: control]
*
*
- * Supported command versions: 1-2 (default: 4/1, 5/2)
+ * Supported command versions: 1-3 (default: 4/1, 5/2)
*
*
* List all available add-on names.
*
+ *
+ * In CV 3+, website maps are also listed.
+ *
*
Parameters:
*
* - CV 1: No arguments.
@@ -139,6 +142,9 @@ public enum Command {
*
*
*
+ * If CMD_INFOs are appended, they use the CV for the CMD_INFO as for this CMD_LIST.
+ *
+ *
* Returns:
*
* - Number N of add-ons,
\n
@@ -152,14 +158,19 @@ public enum Command {
* CMD_INFO name
*
*
- * Supported command versions: 1-2 (default: 4/1, 5/2)
+ * Supported command versions: 1-3 (default: 4/1, 5/2)
+ *
+ *
+ * Returns detailed info about a specific addon or in CV3+ a map.
*
*
- * Returns detailed info about a specific addon.
+ * In CV3+, a map set add-on containing a single map map receive a .map response.
+ * In this case, the category string will be changed accordingly.
+ * For maps, the icon is the map's minimap.
*
*
Parameters:
*
- * - Add-on name
+ *
- Add-on name (.wad) or map slug (pseudo-extension .map; only in CV3+)
*
*
*
@@ -205,6 +216,15 @@ public enum Command {
*
- icon checksum (0 for no icon),
\n
* - icon filesize (0 for no icon),
\n
* - icon file as a byte stream
+ *
- .map only: map file name,
\n
+ * - .map only: unlocalized hint,
\n
+ * - .map only: localized hint,
\n
+ * - .map only: unlocalized uploader comment,
\n
+ * - .map only: localized uploader comment,
\n
+ * - .map only: map width,
\n
+ * - .map only: map height,
\n
+ * - .map only: number of players,
\n
+ * - .map only: world name,
\n
* -
ENDOFSTREAM\n
*
*/
@@ -221,12 +241,14 @@ public enum Command {
*
* Parameters:
*
- * - Add-on name
+ *
- Add-on or map name
*
*
*
* Returns:
*
+ * - For add-ons:
+ *
* - Integer string denoting number D of directories,
\n
* - D Directory names (with full paths), each followed by
\n
* - For each of the D directories:
@@ -242,6 +264,13 @@ public enum Command {
*
* -
ENDOFSTREAM\n
*
+ * For maps:
+ *
+ * - checksum,
\n
+ * - filesize in bytes,
\n
+ * - The content of the file as a byte stream
+ *
+ *
*/
CMD_DOWNLOAD,
diff --git a/wl/server/HandleCommand.java b/wl/server/HandleCommand.java
index 202ac23e0..035426cb8 100644
--- a/wl/server/HandleCommand.java
+++ b/wl/server/HandleCommand.java
@@ -79,6 +79,26 @@ public HandleCommand(PrintStream out,
this.locale = locale;
}
+ /**
+ * Check whether the first command argument refers to an add-on or a map.
+ * If so, strips the extension.
+ * Sanitizes the argument in any case.
+ * @return The command refers to a map.
+ * @throws Exception If the version is out of bounds.
+ */
+ private boolean checkCmd1IsMap() throws Exception {
+ cmd[1] = ServerUtils.sanitizeName(cmd[1], false);
+
+ if (cmd[1].endsWith(".wad")) return false;
+
+ if (cmd[1].endsWith(".map")) {
+ cmd[1] = cmd[1].substring(0, cmd[1].length() - 4);
+ return true;
+ }
+
+ throw new ServerUtils.WLProtocolException("Unrecognizable object type '" + cmd[1] + "'");
+ }
+
/**
* Check that the command version is within the expected bounds.
* @param max Maximum supported command version.
@@ -184,7 +204,7 @@ public void handle(String... c) throws Exception {
*/
private void handleCmdList() throws Exception {
// Args: [2+: control]
- checkCommandVersion(2);
+ checkCommandVersion(3);
ServerUtils.checkNrArgs(cmd, commandVersion < 2 ? 0 : 1);
final boolean versionCheck =
@@ -212,6 +232,26 @@ private void handleCmdList() throws Exception {
compatibleAddOns.add(addon);
}
+ if (commandVersion >= 3) {
+ sql = Utils.sql(Utils.Databases.kWebsite,
+ "select slug,file,wl_version_after from wlmaps_map order by slug");
+ while (sql.next()) {
+ if (!new File(Utils.config("website_maps_path"), sql.getString("file")).isFile()) {
+ continue; // File does not exist
+ }
+
+ if (versionCheck) {
+ String mapRequirement = sql.getString("wl_version_after");
+ if (mapRequirement != null && !ServerUtils.matchesWidelandsVersion(
+ widelandsVersion, mapRequirement, null)) {
+ continue;
+ }
+ }
+
+ compatibleAddOns.add(sql.getString("slug") + ".map");
+ }
+ }
+
MuninStatistics.MUNIN.skipNextCmdInfo(compatibleAddOns.size() - (appendInfo ? 0 : 1));
out.println(compatibleAddOns.size());
for (String name : compatibleAddOns) out.println(name);
@@ -219,7 +259,7 @@ private void handleCmdList() throws Exception {
if (appendInfo) {
for (String name : compatibleAddOns) {
- handle("2:CMD_INFO", name);
+ handle(commandVersion + ":CMD_INFO", name);
}
}
}
@@ -230,9 +270,26 @@ private void handleCmdList() throws Exception {
*/
private void handleCmdInfo() throws Exception {
// Args: name
- checkCommandVersion(2);
+ checkCommandVersion(3);
ServerUtils.checkNrArgs(cmd, 1);
- cmd[1] = ServerUtils.sanitizeName(cmd[1], false);
+
+ if (checkCmd1IsMap()) {
+ if (commandVersion < 3) {
+ throw new ServerUtils.WLProtocolException("Command version " + commandVersion +
+ " not supported for maps");
+ }
+ handleCmdInfoMap();
+ } else {
+ handleCmdInfoAddOn();
+ }
+ }
+
+ /**
+ * Handle a CMD_INFO command for an add-on.
+ * @throws Exception If anything at all goes wrong, throw an Exception.
+ * @todo Treat single-map map set add-ons like maps in CV3+
+ */
+ private void handleCmdInfoAddOn() throws Exception {
ServerUtils.checkAddOnExists(cmd[1]);
ServerUtils.semaphoreRO(cmd[1], () -> {
@@ -318,6 +375,125 @@ private void handleCmdInfo() throws Exception {
});
}
+ /**
+ * Handle a CMD_INFO command for a map.
+ * @throws Exception If anything at all goes wrong, throw an Exception.
+ * @todo Localization for map strings
+ */
+ private void handleCmdInfoMap() throws Exception {
+ ResultSet sqlMain = Utils.sql(
+ Utils.Databases.kWebsite,
+ "select *, UNIX_TIMESTAMP(pub_date) as timestamp from wlmaps_map where slug=?", cmd[1]);
+ if (!sqlMain.next())
+ throw new ServerUtils.WLProtocolException("Map '" + cmd[1] +
+ "' is not in the database");
+
+ final long mapID = sqlMain.getLong("id");
+ final String name = sqlMain.getString("name");
+ final String descr = sqlMain.getString("descr");
+ final String hint = sqlMain.getString("hint");
+ final String uploader_comment = sqlMain.getString("uploader_comment");
+ final String author = sqlMain.getString("author");
+
+ final File minimapFile =
+ new File(Utils.config("website_maps_path"), sqlMain.getString("minimap"));
+ final File mapFile = new File(Utils.config("website_maps_path"), sqlMain.getString("file"));
+
+ if (!mapFile.isFile()) throw new ServerUtils.WLProtocolException("Map file does not exist");
+
+ out.println(name);
+ out.println(name);
+ out.println(descr);
+ out.println(descr);
+ out.println(author);
+ out.println(author);
+ out.println(Utils.getUsername(sqlMain.getLong("uploader_id")));
+
+ out.println(); // add-on version
+ out.println("0"); // i18n version
+ out.println("map"); // category
+ out.println(); // requirements
+
+ // Correct min versions such as "18" or "build 19" to empty for simplicity.
+ // We know the user is using something newer than 1.2 anyway.
+ String mapMinWlVersion = sqlMain.getString("wl_version_after");
+ if (mapMinWlVersion == null || !mapMinWlVersion.matches("^\\d+(\\.\\d+)+$")) {
+ mapMinWlVersion = "";
+ }
+ out.println(mapMinWlVersion);
+
+ out.println(); // max version
+ out.println("true"); // sync safety - we just hope it contains no bad scripting...
+ out.println("0"); // number of screenshots
+
+ out.println(mapFile.length());
+ out.println(sqlMain.getLong("timestamp"));
+ out.println(sqlMain.getLong("nr_downloads"));
+
+ ResultSet sql =
+ Utils.sql(Utils.Databases.kWebsite,
+ "select score from star_ratings_userrating where rating_id=(select id from "
+ + "star_ratings_rating where object_id=?)",
+ mapID);
+ int[] votes = new int[10];
+ for (int i = 0; i < votes.length; ++i) votes[i] = 0;
+ while (sql.next()) votes[sql.getInt("score") - 1]++;
+ for (int v : votes) out.println(v);
+
+ sql = Utils.sql(Utils.Databases.kWebsite,
+ "select id, user_id, UNIX_TIMESTAMP(date_submitted) as timestamp, "
+ + "UNIX_TIMESTAMP(date_modified) as edited, comment "
+ + "from threadedcomments_threadedcomment where "
+ + "content_type_id=(select id from django_content_type where "
+ + "app_label=?) and object_id=? and is_public>0",
+ Utils.config("website_maps_slug"), mapID);
+ ArrayList comments = new ArrayList<>();
+ while (sql.next()) {
+ Long user = sql.getLong("user_id");
+ Long ts1 = sql.getLong("timestamp");
+ Long ts2 = sql.getLong("edited");
+ if (ts2.longValue() <= ts1.longValue()) ts2 = null;
+
+ comments.add(new Utils.AddOnComment(sql.getLong("id"), user, ts1,
+ ts2 == null ? null : user, ts2, "",
+ sql.getString("comment")));
+ }
+ out.println(comments.size());
+ for (Utils.AddOnComment c : comments) {
+ if (commandVersion >= 2) out.println(c.commentID);
+ out.println(Utils.getUsername(c.userID));
+ out.println(c.timestamp);
+ out.println(c.editorID == null ? "" : Utils.getUsername(c.editorID));
+ out.println(c.editTimestamp == null ? 0 : c.editTimestamp);
+ out.println(c.version);
+
+ String[] msg = c.message.split("\n");
+ out.println(msg.length - 1);
+ for (String m : msg) out.println(m);
+ }
+
+ out.println("verified"); // we don't review maps currently
+ out.println("2"); // nor do we assess their quality
+
+ if (minimapFile.isFile()) {
+ ServerUtils.writeOneFile(minimapFile, out);
+ } else {
+ out.println("0\n0"); // minimap should always exist, but not critical if it doesn't
+ }
+
+ out.println(mapFile.getName());
+ out.println(Utils.linebreaksToRichtext(hint));
+ out.println(Utils.linebreaksToRichtext(hint));
+ out.println(Utils.linebreaksToRichtext(uploader_comment));
+ out.println(Utils.linebreaksToRichtext(uploader_comment));
+ out.println(sqlMain.getInt("w"));
+ out.println(sqlMain.getInt("h"));
+ out.println(sqlMain.getInt("nr_players"));
+ out.println(sqlMain.getString("world_name"));
+
+ out.println("ENDOFSTREAM");
+ }
+
/**
* Handle a CMD_DOWNLOAD command.
* @throws Exception If anything at all goes wrong, throw an Exception.
@@ -326,7 +502,26 @@ private void handleCmdDownload() throws Exception {
// Args: name
checkCommandVersion(1);
ServerUtils.checkNrArgs(cmd, 1);
- cmd[1] = ServerUtils.sanitizeName(cmd[1], false);
+
+ if (checkCmd1IsMap()) {
+ ResultSet sql =
+ Utils.sql(Utils.Databases.kWebsite,
+ "select file,nr_downloads from wlmaps_map where slug=?", cmd[1]);
+ if (!sql.next())
+ throw new ServerUtils.WLProtocolException("Map '" + cmd[1] +
+ "' is not in the database");
+
+ ServerUtils.writeOneFile(
+ new File(Utils.config("website_maps_path"), sql.getString("file")), out);
+
+ // TODO enable download counter for maps
+ // Utils.sql(Utils.Databases.kWebsite, "update wlmaps_map set nr_downloads=? where
+ // slug=?", sql.getLong("nr_downloads") + 1, cmd[1]);
+
+ out.println("ENDOFSTREAM");
+ return;
+ }
+
ServerUtils.checkAddOnExists(cmd[1]);
ServerUtils.semaphoreRO(cmd[1], () -> {
ServerUtils.DirInfo dir = new ServerUtils.DirInfo(new File("addons", cmd[1]));
@@ -391,9 +586,11 @@ private void handleCmdVote() throws Exception {
// Args: name vote
checkCommandVersion(1);
ServerUtils.checkNrArgs(cmd, 2);
- if (username.isEmpty())
+ if (username.isEmpty()) {
throw new ServerUtils.WLProtocolException("You need to log in to vote");
- cmd[1] = ServerUtils.sanitizeName(cmd[1], false);
+ }
+
+ // TODO enable for maps
ServerUtils.checkAddOnExists(cmd[1]);
final long addon = Utils.getAddOnID(cmd[1]);
@@ -421,13 +618,23 @@ private void handleCmdGetVote() throws Exception {
out.println("NOT_LOGGED_IN"); // No exception here.
return;
}
- cmd[1] = ServerUtils.sanitizeName(cmd[1], false);
- ServerUtils.checkAddOnExists(cmd[1]);
- ResultSet sql = Utils.sql(Utils.Databases.kAddOns,
- "select vote from uservotes where user=? and addon=?",
- userDatabaseID, Utils.getAddOnID(cmd[1]));
- out.println(sql.next() ? ("" + sql.getLong(1)) : "0");
+ if (checkCmd1IsMap()) {
+ ResultSet sql =
+ Utils.sql(Utils.Databases.kWebsite,
+ "select score from star_ratings_userrating where user_id=? and "
+ + "rating_id=(select id from star_ratings_rating where object_id=?)",
+ userDatabaseID, ServerUtils.getMapID(cmd[1]));
+ out.println(sql.next() ? ("" + sql.getLong(1)) : "0");
+ } else {
+ ServerUtils.checkAddOnExists(cmd[1]);
+
+ ResultSet sql = Utils.sql(Utils.Databases.kAddOns,
+ "select vote from uservotes where user=? and addon=?",
+ userDatabaseID, Utils.getAddOnID(cmd[1]));
+ out.println(sql.next() ? ("" + sql.getLong(1)) : "0");
+ }
+
out.println("ENDOFSTREAM");
}
@@ -440,6 +647,7 @@ private void handleCmdComment() throws Exception {
checkCommandVersion(1);
ServerUtils.checkNrArgs(cmd, 3);
if (username.isEmpty()) throw new ServerUtils.WLProtocolException("Log in to comment");
+ // TODO enable for maps
cmd[1] = ServerUtils.sanitizeName(cmd[1], false);
ServerUtils.checkAddOnExists(cmd[1]);
int nrLines = Integer.valueOf(cmd[3]);
@@ -472,6 +680,7 @@ private void handleCmdEditComment() throws Exception {
ServerUtils.checkNrArgs(cmd, commandVersion < 2 ? 3 : 2);
if (username.isEmpty())
throw new ServerUtils.WLProtocolException("Log in to edit comments");
+ // TODO enable for maps
if (commandVersion < 2) {
cmd[1] = ServerUtils.sanitizeName(cmd[1], false);
ServerUtils.checkAddOnExists(cmd[1]);
diff --git a/wl/server/ServerUtils.java b/wl/server/ServerUtils.java
index 650862774..dffb3ed14 100644
--- a/wl/server/ServerUtils.java
+++ b/wl/server/ServerUtils.java
@@ -323,6 +323,20 @@ public static void checkAddOnExists(String name) throws WLProtocolException {
}
}
+ /**
+ * Retrieve the ID of an map from the database.
+ * @param slug The map's slug.
+ * @return The map's ID.
+ * @throws Exception If anything at all goes wrong or the map does not exist, throw an
+ * Exception.
+ */
+ public static Long getMapID(String slug) throws Exception {
+ ResultSet r =
+ Utils.sql(Utils.Databases.kWebsite, "select id from wlmaps_map where slug=?", slug);
+ if (!r.next()) throw new WLProtocolException("Map '" + slug + "' is not in the database");
+ return r.getLong("id");
+ }
+
/**
* Dump a file and some of its metadata to a stream.
* @param f File to send.
diff --git a/wl/server/SyncThread.java b/wl/server/SyncThread.java
index ccf184620..2da35ff24 100644
--- a/wl/server/SyncThread.java
+++ b/wl/server/SyncThread.java
@@ -80,49 +80,50 @@ public void run() {
Calendar.getInstance().get(Calendar.DAY_OF_WEEK) + "_" + phase + ".sql"});
if (phase == 0) {
- ArrayList unverified = new ArrayList<>();
- ArrayList unassessed = new ArrayList<>();
- ArrayList notx = new ArrayList<>();
- ResultSet sql =
- Utils.sql(Utils.Databases.kAddOns,
- "select name from addons where security=0 order by name");
- while (sql.next()) unverified.add(sql.getString("name"));
- sql = Utils.sql(
- Utils.Databases.kAddOns,
- "select name from addons where security>0 and quality=0 order by name");
- while (sql.next()) unassessed.add(sql.getString("name"));
- sql = Utils.sql(
- Utils.Databases.kAddOns,
- "select name from addons where security>0 and quality>0 order by name");
- while (sql.next()) {
- String name = sql.getString("name");
- if (Utils.bashResult(
- "tx", "status", "-r", ServerUtils.toTransifexResource(name)) != 0)
- notx.add(name);
- }
- if (!unverified.isEmpty() || !unassessed.isEmpty() || !notx.isEmpty()) {
- String message = String.format(
- "There are currently %d unverified add-ons, %d add-on awaiting "
- + "quality review, and %d add-ons without Transifex integration.",
- unverified.size(), unassessed.size(), notx.size());
- if (!unverified.isEmpty()) {
- message += "\n\nUnverified:";
- for (String str : unverified) message += "\n- " + str;
- }
- if (!unassessed.isEmpty()) {
- message += "\n\nUnassessed:";
- for (String str : unassessed) message += "\n- " + str;
+ if (Boolean.parseBoolean(Utils.config("deploy"))) {
+ ArrayList unverified = new ArrayList<>();
+ ArrayList unassessed = new ArrayList<>();
+ ArrayList notx = new ArrayList<>();
+ ResultSet sql =
+ Utils.sql(Utils.Databases.kAddOns,
+ "select name from addons where security=0 order by name");
+ while (sql.next()) unverified.add(sql.getString("name"));
+ sql = Utils.sql(
+ Utils.Databases.kAddOns,
+ "select name from addons where security>0 and quality=0 order by name");
+ while (sql.next()) unassessed.add(sql.getString("name"));
+ sql = Utils.sql(
+ Utils.Databases.kAddOns,
+ "select name from addons where security>0 and quality>0 order by name");
+ while (sql.next()) {
+ String name = sql.getString("name");
+ if (Utils.bashResult("tx", "status", "-r",
+ ServerUtils.toTransifexResource(name)) != 0)
+ notx.add(name);
}
- if (!notx.isEmpty()) {
- message += "\n\nNo Transifex integration:";
- for (String str : notx) message += "\n- " + str;
+ if (!unverified.isEmpty() || !unassessed.isEmpty() || !notx.isEmpty()) {
+ String message = String.format(
+ "There are currently %d unverified add-ons, %d add-on awaiting "
+ +
+ "quality review, and %d add-ons without Transifex integration.",
+ unverified.size(), unassessed.size(), notx.size());
+ if (!unverified.isEmpty()) {
+ message += "\n\nUnverified:";
+ for (String str : unverified) message += "\n- " + str;
+ }
+ if (!unassessed.isEmpty()) {
+ message += "\n\nUnassessed:";
+ for (String str : unassessed) message += "\n- " + str;
+ }
+ if (!notx.isEmpty()) {
+ message += "\n\nNo Transifex integration:";
+ for (String str : notx) message += "\n- " + str;
+ }
+
+ Utils.sendEMailToSubscribedAdmins(
+ Utils.kEMailVerbosityFYI, "Moderation Report", message);
}
- Utils.sendEMailToSubscribedAdmins(
- Utils.kEMailVerbosityFYI, "Moderation Report", message);
- }
-
- if (Boolean.parseBoolean(Utils.config("deploy"))) {
TransifexIntegration.TX.checkIssues();
TransifexIntegration.TX.fullSync();
}
diff --git a/wl/utils/Utils.java b/wl/utils/Utils.java
index ba9559426..5c8c3fffd 100644
--- a/wl/utils/Utils.java
+++ b/wl/utils/Utils.java
@@ -259,6 +259,15 @@ public static String getUploadersString(long addon, boolean onlyFirst) throws Ex
return uploaders;
}
+ /**
+ * Replace regular linebreaks in a string with Widelands richtext newline tags.
+ * @param str Arbitrary text to process.
+ * @return Text with escaped linebreaks.
+ */
+ public static String linebreaksToRichtext(String str) {
+ return str.replaceAll("\r?\n", "
");
+ }
+
/**
* List all files in a directory, sorted alphabetically.
* @param dir Directory to list.