Index: binaries/data/mods/mod/gui/modmod/modmod.js =================================================================== --- binaries/data/mods/mod/gui/modmod/modmod.js +++ binaries/data/mods/mod/gui/modmod/modmod.js @@ -78,7 +78,6 @@ loadMods(); loadEnabledMods(); recomputeCompatibility(); - validateMods(); initGUIFilters(); } @@ -101,12 +100,6 @@ g_ModsDisabledFiltered = g_ModsDisabled; } -function validateMods() -{ - for (let folder in g_Mods) - validateMod(folder, g_Mods[folder], true); -} - function initGUIFilters() { Engine.GetGUIObjectByName("negateFilter").checked = false; @@ -162,12 +155,12 @@ let selected = listObject.selected !== -1 ? listObject.list_name[listObject.selected] : null; - listObject.list_name = folders.map(folder => colorMod(folder, g_Mods[folder].name, enabled)); + listObject.list_name = folders.map(folder => colorMod(folder, g_Mods[folder].name || "", enabled)); listObject.list_folder = folders.map(folder => colorMod(folder, folder, enabled)); - listObject.list_label = folders.map(folder => colorMod(folder, g_Mods[folder].label, enabled)); + listObject.list_label = folders.map(folder => colorMod(folder, g_Mods[folder].label || "", enabled)); listObject.list_url = folders.map(folder => colorMod(folder, g_Mods[folder].url || "", enabled)); - listObject.list_version = folders.map(folder => colorMod(folder, g_Mods[folder].version, enabled)); - listObject.list_dependencies = folders.map(folder => colorMod(folder, g_Mods[folder].dependencies.join(" "), enabled)); + listObject.list_version = folders.map(folder => colorMod(folder, g_Mods[folder].version || "", enabled)); + listObject.list_dependencies = folders.map(folder => colorMod(folder,g_Mods[folder].dependencies?.join(" "), enabled)); listObject.list = folders; listObject.selected = selected ? listObject.list_name.indexOf(selected) : -1; @@ -312,6 +305,9 @@ function areDependenciesMet(folder, disabledAction = false) { + if (!g_Mods[folder].dependencies) + return true; + // If we disabled mod it will not change satus of incompatible mods if (disabledAction && !g_ModsCompatibility[folder]) return g_ModsCompatibility[folder]; @@ -328,7 +324,7 @@ function recomputeCompatibility(disabledAction = false) { for (let mod in g_Mods) - g_ModsCompatibility[mod] = areDependenciesMet(mod, disabledAction); + g_ModsCompatibility[mod] = !g_Mods[mod].invalid && areDependenciesMet(mod, disabledAction); } /** @@ -394,6 +390,11 @@ g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled, true); } +function isModInvalid(folder) +{ + return g_Mods[folder].invalid; +} + function selectedMod(listObjectName) { let listObject = Engine.GetGUIObjectByName(listObjectName); @@ -413,16 +414,36 @@ toggleModButton.caption = isPickedDisabledList ? translateWithContext("mod activation", "Enable") : translateWithContext("mod activation", "Disable"); - toggleModButton.enabled = isPickedDisabledList ? isModSelected && g_ModsCompatibility[listObject.list[listObject.selected]] : isModSelected; + toggleModButton.enabled = isPickedDisabledList ? isModSelected && g_ModsCompatibility[listObject.list[listObject.selected]] : isModSelected && !g_Mods[listObject.list[listObject.selected]].invalid; Engine.GetGUIObjectByName("enabledModUp").enabled = isModSelected && listObjectName == "modsEnabledList" && !areFilters(); Engine.GetGUIObjectByName("enabledModDown").enabled = isModSelected && listObjectName == "modsEnabledList" && !areFilters(); Engine.GetGUIObjectByName("globalModDescription").caption = listObject.list[listObject.selected] ? + g_Mods[listObject.list[listObject.selected]].invalid ? + '[color="' + g_ColorDependenciesNotMet + '"]' + sprintf(translate("Invalid mod: %(error)s"), {"error": getErrorReason(g_Mods[listObject.list[listObject.selected]].error)}) : g_Mods[listObject.list[listObject.selected]].description : '[color="' + g_ColorNoModSelected + '"]' + translate("No mod has been selected.") + '[/color]'; } +function getErrorReason(error) +{ + switch(error) + { + case 1: return translate("Missing name"); + case 2: return translate("Missing version"); + case 3: return translate("Missing dependencies"); + case 4: return translate("Dependencies are not object"); + case 5: return translate("Missing label"); + case 6: return translate("Missing description"); + case 7: return translate("Name may only contain alphanumeric characters"); + case 8: return translate("Version may only contain numbers and at most 2 periods"); + case 9: return translate("Label may not be empty!"); + default: return translate(sprintf("unknown error %(error)s", {"error": error})); + } +} + + /** * @returns {string} The url of the currently selected mod. */ Index: binaries/data/mods/mod/gui/modmod/validatemod.js =================================================================== --- binaries/data/mods/mod/gui/modmod/validatemod.js +++ /dev/null @@ -1,153 +0,0 @@ -const g_ModProperties = { - // example: "0ad" - "name": { - "required": true, - "type": "string", - "validate": validateName - }, - // example: "0.0.24" - "version": { - "required": true, - "type": "string", - "validate": validateVersion - }, - // example: ["0ad<=0.0.16", "rote"] - "dependencies": { - "required": true, - "type": "object", - "validate": validateDependencies - }, - // example: "0 A.D. - Empires Ascendant" - "label": { - "require": true, - "type": "string", - "validate": validateLabel - }, - // example: "A free, open-source, historical RTS game." - "description": { - "required": true, - "type": "string" - }, - // example: "https://wildfiregames.com/" - "url": { - "required": false, - "type": "string" - } -}; - -/** - * Tests if the string only contains alphanumeric characters and _ - - */ -const g_RegExpName = /[a-zA-Z0-9\-\_]+/; - -/** - * Tests if the version string consists only of numbers and at most two periods. - */ -const g_RegExpVersion= /[0-9]+(\.[0-9]+){0,2}/; - -/** - * Version checks in mod dependencies can use these operators. - */ -const g_RegExpComparisonOperator = /(<=|>=|<|>|=)/; - -/** - * Tests if a dependency compares a mod version against another, for instance "0ad<=0.0.16". - */ -const g_RegExpComparison = globalRegExp(new RegExp(g_RegExpName.source + g_RegExpComparisonOperator.source + g_RegExpVersion.source)); - -/** - * The label may not be empty. - */ -const g_RegExpLabel = /.*\S.*/; - -function globalRegExp(regexp) -{ - return new RegExp("^" + regexp.source + "$"); -} - -/** - * Returns whether the mod defines all required properties and whether all properties are valid. - * Shows a notification if not. - */ -function validateMod(folder, modData, notify) -{ - let valid = true; - - for (let propertyName in g_ModProperties) - { - let property = g_ModProperties[propertyName]; - - if (modData[propertyName] === undefined) - { - if (!property.required) - continue; - - if (notify) - warn("Mod '" + folder + "' does not define '" + propertyName + "'!"); - - valid = false; - } - - if (typeof modData[propertyName] != property.type) - { - if (notify) - warn(propertyName + " in mod '" + folder + "' is not of the type '" + property.type + "'!"); - - valid = false; - - continue; - } - - if (property.validate && !property.validate(folder, modData, notify)) - valid = false; - } - - return valid; -} - -function validateName(folder, modData, notify) -{ - let valid = modData.name.match(globalRegExp(g_RegExpName)); - - if (!valid && notify) - warn("mod name of " + folder + " may only contain alphanumeric characters, but found '" + modData.name + "'!"); - - return valid; -} - -function validateVersion(folder, modData, notify) -{ - let valid = modData.version.match(globalRegExp(g_RegExpVersion)); - - if (!valid && notify) - warn("mod version of " + folder + " may only contain numbers and at most 2 periods, but found '" + modData.version + "'!"); - - return valid; -} - -function validateDependencies(folder, modData, notify) -{ - let valid = true; - - for (let dependency of modData.dependencies) - { - valid = valid && ( - dependency.match(globalRegExp(g_RegExpName)) || - dependency.match(globalRegExp(g_RegExpComparison))); - - if (!valid && notify) - warn("mod folder " + folder + " requires an invalid dependency '" + dependency + "'!"); - } - - return valid; -} - -function validateLabel(folder, modData, notify) -{ - let valid = modData.label.match(g_RegExpLabel); - - if (!valid && notify) - warn("mod label of " + folder + " may not be empty!"); - - return valid; -} Index: binaries/data/mods/mod/gui/modmod/validatemod_test.js =================================================================== --- binaries/data/mods/mod/gui/modmod/validatemod_test.js +++ /dev/null @@ -1,93 +0,0 @@ -const g_ValidTestMods = { - "public": { - "name": "0ad", - "version": "0.0.23", - "label": "0 A.D. Empires Ascendant", - "url": "https://play0ad.com", - "description": "A free, open-source, historical RTS game.", - "dependencies": [] - }, - "tm": { - "name": "Terra_Magna", - "version": "0.0.22", - "label": "0 A.D. Terra Magna", - "url": "https://forum.wildfiregames.com", - "description": "Adds various civilizations to 0 A.D.", - "dependencies": ["0ad"] - }, - "mil": { - "name": "millenniumad", - "version": "0.0.22", - "label": "0 A.D. Medieval Extension", - "url": "https://forum.wildfiregames.com", - "description": "Adds medieval content like civilizations + maps.", - "dependencies": ["0ad=0.0.23"] - } -}; - -const g_TestModsInvalid = { - "broken1": { - "name": "name may not contain whitespace", - "version": "1", - "label": "1", - "description": "", - "dependencies": [] - }, - "broken2": { - "name": "broken2", - "version": "0.0.2.1", - "label": "2", - "description": "it has too many dots in the version", - "dependencies": [] - }, - "broken3": { - "name": "broken3", - "version": "broken3", - "label": "3", - "description": "version numbers must be numeric", - "dependencies": [] - }, - "broken4": { - "name": "broken4", - "version": "4", - "label": "4", - "description": "dependencies must be mod names or valid comparisons", - "dependencies": ["mod version=3"] - }, - "broken5": { - "name": "broken5", - "version": "5", - "label": "5", - "description": "names in mod dependencies may not contain whitespace either", - "dependencies": ["mod version"] - }, - "broken6": { - "name": "broken6", - "version": "6", - "label": "6", - "description": "should have used =", - "dependencies": ["mod==3"] - }, - "broken7": { - "name": "broken7", - "version": "7", - "label": "", - "description": "label may not be empty", - "dependencies": [] - }, - "broken8": { - "name": "broken8", - "version": "8", - "label": "8", - "description": "a version is an invalid dependency", - "dependencies": ["0.0.23"] - } -}; - -for (let folder in g_ValidTestMods) - if (!validateMod(folder, g_ValidTestMods[folder], false)) - throw new Error("Valid mod '" + folder + "' should have passed the test."); - -for (let folder in g_TestModsInvalid) - if (validateMod(folder, g_TestModsInvalid[folder], false)) - throw new Error("Invalid mod '" + folder + "' should not have passed the test."); Index: source/ps/Mod.cpp =================================================================== --- source/ps/Mod.cpp +++ source/ps/Mod.cpp @@ -20,6 +20,7 @@ #include "ps/Mod.h" #include +#include #include "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" @@ -35,10 +36,9 @@ std::vector> g_LoadedModVersions; CmdLineArgs g_args; - JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) { - ScriptRequest rq(scriptInterface); + const ScriptRequest rq(scriptInterface); JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); const Paths paths(g_args); @@ -71,7 +71,82 @@ if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; - // Valid mod, add it to our structure + // Validate mod + if (!json.isObject()) + continue; + JS::RootedObject jsonObj(rq.cx, &json.toObject()); + bool hasProperty; + if (!JS_HasProperty(rq.cx, jsonObj, "name", &hasProperty) || !hasProperty) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 1); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + std::string name; + scriptInterface.FromJSProperty(rq, json, "name", name, false); + if (!std::regex_match(name, std::regex("^[a-zA-Z0-9\-\_]+$"))) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 7); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + if (!JS_HasProperty(rq.cx, jsonObj, "version", &hasProperty) || !hasProperty) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 2); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + std::string version; + scriptInterface.FromJSProperty(rq, json, "version", version, false); + if (!std::regex_match(version, std::regex("^[0-9]+(\.[0-9]+){0,2}$"))) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 8); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + if (!JS_HasProperty(rq.cx, jsonObj, "dependencies", &hasProperty) || !hasProperty) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 3); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + JS::RootedValue dependencies(rq.cx); + JS_GetProperty(rq.cx, jsonObj, "dependencies", &dependencies); + if (!dependencies.isObject()) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 4); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + if (!JS_HasProperty(rq.cx, jsonObj, "label", &hasProperty) || !hasProperty) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 5); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + std::string label; + scriptInterface.FromJSProperty(rq, json, "label", label, false); + if (!std::regex_match(label, std::regex(".*\\S.*"))) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 9); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } + if (!JS_HasProperty(rq.cx, jsonObj, "description", &hasProperty) || !hasProperty) + { + scriptInterface.SetProperty(json, "invalid", true); + scriptInterface.SetProperty(json, "error", 6); + JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); + continue; + } JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); }