Skip to content

Conversation

@JacksonTheMaster
Copy link
Collaborator

@JacksonTheMaster JacksonTheMaster commented Jan 28, 2026

Introduce functions for installing and uninstalling SLP, along with a comprehensive mod package upload system.

Enhance the UI for managing mods
add related localization
and implement logging for modding activities
updated the API to handle mods and workshop details
add functionality for workshop mod updates
slighly refactored the CLI for improved structure
add new commands for testing and managing mods
improve credits page and add donors Stealthbob & Sumisukyo

…rs and UI. SLP defaults to false, but if SLP is enabled SLP auto updates default to true
update .gitignore to include .agent-context and .github/copilot-instructions
Implemented comprehensive mod package upload functionality (&UI)
- Add functions to handle modpkg upload, extraction, and deletion
- Add route POST /api/v2/slp/upload for mod package uploads
- Add logger subsystem logger.Modding
- Used universal popup system from Dashboard for UI feedback
can be called to update the installed mods or to download new mods
…onse with an array of "log" that is returned
- remove setDummyBuildID()
- fix SLP-related auto update logic config file writer
- fix typo in comment
- fix remove redundant os.MkdirlAll call in modpackages.go
- remove debug statement UpdateWorkshopItems as its not needed
Copilot AI review requested due to automatic review settings January 28, 2026 11:39
@JacksonTheMaster JacksonTheMaster added StationeersServerUI go Pull requests that update go code labels Jan 28, 2026
@JacksonTheMaster JacksonTheMaster self-assigned this Jan 28, 2026
popup.className = 'popup';
popupTitle.textContent = '';
popupMessage.textContent = message;
popupMessage.innerHTML = message.replace(/\n/g, '<br>');

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.
DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI about 3 hours ago

In general, the problem is that showPopup passes potentially untrusted message text directly to innerHTML, causing any HTML meta-characters in message (coming from user-controlled sources like selectedModFile.name) to be interpreted as markup. To fix this without changing existing behavior too much, we should stop using innerHTML for raw strings and instead safely encode the message and only then add line breaks. A common pattern is: escape the message to plain text, then replace \n with <br> on the escaped text and inject the result as HTML, or split on \n and append text and <br> nodes manually.

The single best minimal-change fix is to introduce a small helper escapeHtml(text) in popup.js that replaces &, <, >, ", and ' with their HTML entities, and use it in showPopup so that message is escaped before \n is converted to <br>. Concretely, add the helper at the top of popup.js, and change line 8 from popupMessage.innerHTML = message.replace(/\n/g, '<br>'); to something like:

const safeHtml = escapeHtml(String(message)).replace(/\n/g, '<br>');
popupMessage.innerHTML = safeHtml;

This preserves the existing line-break behavior while ensuring any user-controlled content (like file names) cannot inject HTML or scripts. No other files need changes; slp.js and config.html can remain as they are because all tainted flow is mediated by showPopup.


Suggested changeset 1
UIMod/onboard_bundled/assets/js/popup.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/UIMod/onboard_bundled/assets/js/popup.js b/UIMod/onboard_bundled/assets/js/popup.js
--- a/UIMod/onboard_bundled/assets/js/popup.js
+++ b/UIMod/onboard_bundled/assets/js/popup.js
@@ -1,3 +1,15 @@
+function escapeHtml(text) {
+    if (text === null || text === undefined) {
+        return '';
+    }
+    return String(text)
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#39;');
+}
+
 function showPopup(status, message) {
     const popup = document.getElementById('universalPopup');
     const popupTitle = document.getElementById('popupTitle');
@@ -5,7 +17,8 @@
     
     popup.className = 'popup';
     popupTitle.textContent = '';
-    popupMessage.innerHTML = message.replace(/\n/g, '<br>');
+    const safeMessageHtml = escapeHtml(message).replace(/\n/g, '<br>');
+    popupMessage.innerHTML = safeMessageHtml;
 
     switch(status.toLowerCase()) {
         case 'error':
EOF
@@ -1,3 +1,15 @@
function escapeHtml(text) {
if (text === null || text === undefined) {
return '';
}
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

function showPopup(status, message) {
const popup = document.getElementById('universalPopup');
const popupTitle = document.getElementById('popupTitle');
@@ -5,7 +17,8 @@

popup.className = 'popup';
popupTitle.textContent = '';
popupMessage.innerHTML = message.replace(/\n/g, '<br>');
const safeMessageHtml = escapeHtml(message).replace(/\n/g, '<br>');
popupMessage.innerHTML = safeMessageHtml;

switch(status.toLowerCase()) {
case 'error':
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should eventually fix this, but not now

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive mod management features for Stationeers Launch Pad (SLP), enabling server administrators to install SLP, upload mod packages, manage workshop mods, and view installed mods through a web interface.

Changes:

  • Added SLP installation/uninstallation functionality with automatic GitHub release fetching
  • Implemented mod package upload system with zip extraction and validation
  • Integrated Steam Workshop mod download and update capabilities via SteamCMD
  • Enhanced web UI with new SLP configuration tab, mod listing, and file upload interface
  • Added localization support for English, German, and Swedish languages
  • Refactored CLI commands into separate files for better organization
  • Introduced new configuration variables for SLP features and auto-update settings

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/web/slp-launchpad.go New HTTP handlers for SLP install, uninstall, upload, and mod listing
src/web/routes.go Registered new API endpoints for SLP and modding features
src/web/configpage.go Added SLP template variables to configuration page
src/web/templatevars.go Defined new UI text variables for SLP interface
src/modding/launchpad.go Core SLP installation/uninstallation logic with GitHub API integration
src/modding/launchpad-config.go SLP auto-update configuration management
src/modding/modpackages.go Mod package upload, extraction, and processing
src/modding/modlist.go Mod discovery, metadata parsing, and workshop handle extraction
src/steamcmd/workshop.go Steam Workshop item download and copy operations
src/config/*.go Added SLP-related configuration variables, getters, and setters
src/logger/logger.go Added dedicated modding subsystem logger
src/cli/*.go Refactored CLI into separate files with new mod-related commands
src/core/loader/loader.go Integrated SLP auto-update check on startup
src/setup/update/*.go Exported WriteCounter for reuse in download progress tracking
UIMod/onboard_bundled/ui/config.html Added SLP tab with installation, upload, and management UI
UIMod/onboard_bundled/assets/js/slp.js JavaScript for SLP operations, file upload, and mod display
UIMod/onboard_bundled/assets/js/popup.js Enhanced popup to support multi-line messages
UIMod/onboard_bundled/assets/css/*.css Added extensive styling for SLP interface components
UIMod/onboard_bundled/localization/*.json Added SLP-related translations for three languages
src/web/TwoBoxForm.go Added "multiplayersafe" branch option
.gitignore Added exclusions for agent context, mods directory, and instructions
.devcontainer/devcontainer.json Removed supermaven extension reference

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +377 to +380
imageHtml = `
<div class="mod-image-container" data-mod-index="${index}">
<img id="mod-image-${index}" src="data:image/png;base64,${firstImageData}" alt="Mod image">
</div>
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Base64 image data from user-uploaded mods is inserted directly into img src without validation. While base64 data URIs are generally safe, there's no verification that the data is actually a valid image or that it doesn't exceed reasonable size limits. Consider validating the image format and size on the backend before encoding to base64, or add client-side validation of the base64 data structure.

Copilot uses AI. Check for mistakes.
popup.className = 'popup';
popupTitle.textContent = '';
popupMessage.textContent = message;
popupMessage.innerHTML = message.replace(/\n/g, '<br>');
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential XSS vulnerability. The message parameter is directly inserted into innerHTML after only replacing newlines with <br> tags. This allows any HTML/JavaScript in the message to be executed. Since error messages can include user-controlled data (like filenames or error text from uploads), this creates an XSS risk. Use textContent instead of innerHTML, or properly sanitize the message content before insertion.

Suggested change
popupMessage.innerHTML = message.replace(/\n/g, '<br>');
// Safely render message as text while preserving newlines as <br> elements
popupMessage.textContent = '';
if (typeof message === 'string' && message.length > 0) {
const parts = message.split('\n');
parts.forEach(function(part, index) {
popupMessage.appendChild(document.createTextNode(part));
if (index < parts.length - 1) {
popupMessage.appendChild(document.createElement('br'));
}
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +77
cmd := exec.Command(
filepath.Join(steamcmddir, executable),
"+force_install_dir", "../",
"+login", "anonymous",
"+workshop_download_item", "544550", appID,
"validate",
"+quit",
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workshop handles from user-uploaded mod packages are not validated before being passed to SteamCMD. While the command uses exec.Command with separate arguments (preventing shell injection), workshop handles should still be validated to ensure they're numeric Steam Workshop IDs. Add validation to ensure workshop handles match the expected format (e.g., only digits) to prevent potential issues with SteamCMD.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +63
if err := os.Remove(modconfigPath); err != nil && !os.IsNotExist(err) {
logger.Modding.Warnf("Failed to remove existing modconfig.xml: %v", err)
}
if !os.IsNotExist(err) {
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic error in error checking. Line 63 checks !os.IsNotExist(err) but err is from line 60's os.Remove call. If the file exists and was successfully removed, err will be nil, making !os.IsNotExist(err) true (since nil is not IsNotExist), so line 64 will log "Removed existing modconfig.xml" even though it might not have been removed. The condition should check if err == nil instead to indicate successful removal.

Suggested change
if err := os.Remove(modconfigPath); err != nil && !os.IsNotExist(err) {
logger.Modding.Warnf("Failed to remove existing modconfig.xml: %v", err)
}
if !os.IsNotExist(err) {
if err := os.Remove(modconfigPath); err != nil {
if !os.IsNotExist(err) {
logger.Modding.Warnf("Failed to remove existing modconfig.xml: %v", err)
}
} else {

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +32
const maxZipSize = 500 * 1024 * 1024 // 500 MB
sizeLimitedReader := io.LimitReader(r, maxZipSize+1)

// Read the entire zip file into memory
zipBytes, err := io.ReadAll(sizeLimitedReader)
if err != nil {
logger.Modding.Errorf("Failed to read mod package zip file, filesize might exceed 500mb: %v", err)
return fmt.Errorf("failed to read mod package zip file, filesize might exceed 500mb: %w", err)
}

if len(zipBytes) == 0 {
logger.Modding.Error("Received empty mod package")
return fmt.Errorf("mod package is empty")
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The size limit check is flawed. Using io.LimitReader(r, maxZipSize+1) and then checking if len(zipBytes) == 0 doesn't properly validate if the file exceeds 500MB. If a file is exactly maxZipSize+1 bytes, it will be read successfully but not rejected. Add an explicit check: if len(zipBytes) > maxZipSize to properly enforce the limit and provide a clear error message.

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +188
uploadZone.innerHTML = '<span class="upload-icon">✓</span>' +
'<div class="upload-text">File Selected</div>' +
'<div class="upload-subtext">' + selectedModFile.name + '</div>' +
'<div class="upload-subtext" style="margin-top: 5px; color: #00d4ff;">' + (selectedModFile.size / 1024 / 1024).toFixed(2) + ' MB</div>';
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-provided filename is inserted directly into HTML without proper escaping. This creates a potential XSS vulnerability. The filename could contain malicious HTML/JavaScript that would be executed. Use proper HTML escaping (e.g., textContent instead of innerHTML, or escape the filename before inserting it).

Suggested change
uploadZone.innerHTML = '<span class="upload-icon">✓</span>' +
'<div class="upload-text">File Selected</div>' +
'<div class="upload-subtext">' + selectedModFile.name + '</div>' +
'<div class="upload-subtext" style="margin-top: 5px; color: #00d4ff;">' + (selectedModFile.size / 1024 / 1024).toFixed(2) + ' MB</div>';
// Set static structure via innerHTML and inject dynamic values safely via DOM APIs
uploadZone.innerHTML = '<span class="upload-icon">✓</span>' +
'<div class="upload-text">File Selected</div>';
const nameDiv = document.createElement('div');
nameDiv.className = 'upload-subtext';
nameDiv.textContent = selectedModFile.name;
uploadZone.appendChild(nameDiv);
const sizeDiv = document.createElement('div');
sizeDiv.className = 'upload-subtext';
sizeDiv.style.marginTop = '5px';
sizeDiv.style.color = '#00d4ff';
sizeDiv.textContent = (selectedModFile.size / 1024 / 1024).toFixed(2) + ' MB';
uploadZone.appendChild(sizeDiv);

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +156
rc.Close()
return fmt.Errorf("failed to create output file: %w", err)
}

if _, err := io.Copy(outFile, rc); err != nil {
outFile.Close()
rc.Close()
return fmt.Errorf("failed to write file: %w", err)
}

outFile.Close()
rc.Close()

Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.

Suggested change
rc.Close()
return fmt.Errorf("failed to create output file: %w", err)
}
if _, err := io.Copy(outFile, rc); err != nil {
outFile.Close()
rc.Close()
return fmt.Errorf("failed to write file: %w", err)
}
outFile.Close()
rc.Close()
rcCloseErr := rc.Close()
if rcCloseErr != nil {
return fmt.Errorf("failed to create output file: %w (and failed to close archive file: %v)", err, rcCloseErr)
}
return fmt.Errorf("failed to create output file: %w", err)
}
if _, err := io.Copy(outFile, rc); err != nil {
closeErr := outFile.Close()
rcCloseErr := rc.Close()
if closeErr != nil {
return fmt.Errorf("failed to write file: %w (and failed to close output file: %v)", err, closeErr)
}
if rcCloseErr != nil {
return fmt.Errorf("failed to write file: %w (and failed to close archive file: %v)", err, rcCloseErr)
}
return fmt.Errorf("failed to write file: %w", err)
}
if err := outFile.Close(); err != nil {
rcCloseErr := rc.Close()
if rcCloseErr != nil {
return fmt.Errorf("failed to close output file: %w (and failed to close archive file: %v)", err, rcCloseErr)
}
return fmt.Errorf("failed to close output file: %w", err)
}
if err := rc.Close(); err != nil {
return fmt.Errorf("failed to close archive file: %w", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +156
outFile.Close()
rc.Close()

Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.

Suggested change
outFile.Close()
rc.Close()
// Close the archive entry reader first; errors here are less critical
if err := rc.Close(); err != nil {
return fmt.Errorf("failed to close archive file reader: %w", err)
}
// Ensure any buffered data is flushed and detect write/FS errors
if err := outFile.Close(); err != nil {
return fmt.Errorf("failed to close output file: %w", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines +245 to +255
outFile.Close()
return err
}

_, err = io.Copy(outFile, rc)
rc.Close()
outFile.Close()

if err != nil {
return err
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.

Suggested change
outFile.Close()
return err
}
_, err = io.Copy(outFile, rc)
rc.Close()
outFile.Close()
if err != nil {
return err
}
if cerr := outFile.Close(); cerr != nil {
return fmt.Errorf("failed to close output file %q after error opening zip entry %q: %w (original error: %v)", fpath, f.Name, cerr, err)
}
return err
}
copyErr := io.Copy(outFile, rc)
rcErr := rc.Close()
closeErr := outFile.Close()
if copyErr != nil {
return copyErr
}
if rcErr != nil {
return rcErr
}
if closeErr != nil {
return closeErr
}

Copilot uses AI. Check for mistakes.
Comment on lines +250 to +255
rc.Close()
outFile.Close()

if err != nil {
return err
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.

Suggested change
rc.Close()
outFile.Close()
if err != nil {
return err
}
rcCloseErr := rc.Close()
outFileCloseErr := outFile.Close()
if err != nil {
return err
}
if outFileCloseErr != nil {
return outFileCloseErr
}
if rcCloseErr != nil {
return rcCloseErr
}

Copilot uses AI. Check for mistakes.
@JacksonTheMaster JacksonTheMaster merged commit de53cf3 into main Jan 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go Pull requests that update go code StationeersServerUI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants