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: *

* *

+ * If CMD_INFOs are appended, they use the CV for the CMD_INFO as for this CMD_LIST. + * + *

* Returns: *

*/ @@ -221,12 +241,14 @@ public enum Command { * *

Parameters: *

    - *
  1. Add-on name + *
  2. Add-on or map name *
* *

* Returns: *

+ *
  • For maps: + * + * */ 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.