Skip to content

Commit 3ed59cc

Browse files
committed
added shout mode disable on bar, added a safer way to open folders
1 parent 9d13f03 commit 3ed59cc

11 files changed

Lines changed: 242 additions & 23 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace Basis.BasisUI
5+
{
6+
/// <summary>
7+
/// Reveals a file or folder in the host OS file browser, defensively.
8+
///
9+
/// This launches an OS process / URL handler, and some callers pass paths that embed
10+
/// server-influenced text (e.g. a pulled-log folder named after the remote server). To stop
11+
/// that becoming an argument- or URL-injection vector this:
12+
/// • canonicalises the path and requires it to be a real, existing local file/folder, so a
13+
/// hostile string (a process-flag or URL-scheme payload) can never reach a launcher;
14+
/// • refuses any path containing a double-quote or control character — a legitimate reveal
15+
/// target never has one, and a double-quote is the only thing that could break out of the
16+
/// quoted argument we pass;
17+
/// • never routes through a shell, opens folders by path (no argument string) on Windows,
18+
/// invokes the absolute /usr/bin/open on macOS, and on the fallback emits only a file://
19+
/// URI (never an arbitrary scheme);
20+
/// • swallows and logs every failure, so a UI callback can never be broken by it.
21+
/// No-op for an empty / invalid / non-existent path.
22+
/// </summary>
23+
public static class BasisFileBrowserUtility
24+
{
25+
public static void Reveal(string path, bool selectFile = false)
26+
{
27+
try
28+
{
29+
if (string.IsNullOrWhiteSpace(path)) return;
30+
31+
// Resolve to an absolute path and require it to exist locally. Only a real on-disk
32+
// target ever reaches a launcher below.
33+
string fullPath = Path.GetFullPath(path);
34+
35+
bool isFile = File.Exists(fullPath);
36+
bool isDirectory = Directory.Exists(fullPath);
37+
if (!isFile && !isDirectory) return;
38+
39+
// A legitimate reveal target carries no double-quote or control character; if one
40+
// does (a crafted name), refuse rather than risk breaking out of the quoted argument.
41+
if (HasUnsafeCharacters(fullPath)) return;
42+
43+
// Only a real file can be highlighted; otherwise reveal the directory itself.
44+
if (selectFile && !isFile) selectFile = false;
45+
string directory = isDirectory ? fullPath : (Path.GetDirectoryName(fullPath) ?? fullPath);
46+
47+
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
48+
if (selectFile)
49+
{
50+
// Pin to the absolute system explorer so a CWD-planted "explorer.exe" cannot run
51+
// instead. explorer expects exactly "/select,<file>"; the file is verified to exist
52+
// and Windows file names cannot contain a double-quote, so it cannot break out.
53+
string explorer = Path.Combine(
54+
Environment.GetFolderPath(Environment.SpecialFolder.Windows), "explorer.exe");
55+
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
56+
{
57+
FileName = explorer,
58+
Arguments = $"/select,\"{fullPath}\"",
59+
UseShellExecute = true
60+
});
61+
}
62+
else
63+
{
64+
// Open the directory itself — there is no argument string to inject into, and
65+
// shell-executing a verified directory always opens the browser, never runs a file.
66+
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
67+
{
68+
FileName = directory,
69+
UseShellExecute = true
70+
});
71+
}
72+
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
73+
// Absolute path (no PATH lookup to hijack) and no shell (UseShellExecute = false); the
74+
// quoted target has no quote/control chars, so it cannot inject extra `open` flags
75+
// (e.g. -a to launch an arbitrary application).
76+
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
77+
{
78+
FileName = "/usr/bin/open",
79+
Arguments = selectFile ? $"-R \"{fullPath}\"" : $"\"{directory}\"",
80+
UseShellExecute = false
81+
});
82+
#else
83+
// Build a real file:// URI from the verified directory so the URL handler can never be
84+
// steered to a remote or script scheme. (This platform cannot highlight a file.)
85+
UnityEngine.Application.OpenURL(new Uri(directory).AbsoluteUri);
86+
#endif
87+
}
88+
catch (Exception e)
89+
{
90+
// Best-effort convenience: a failure here must never propagate into the caller.
91+
BasisDebug.LogWarning($"Could not reveal path in file browser: {e.Message}");
92+
}
93+
}
94+
95+
// Rejects the double-quote (the only character that can break out of the quoted argument we
96+
// pass to a launcher) plus control characters (which a real path never contains and which can
97+
// corrupt argument/log parsing). Spaces, single quotes and unicode are intentionally allowed.
98+
private static bool HasUnsafeCharacters(string value)
99+
{
100+
foreach (char c in value)
101+
{
102+
if (c == '"' || c < ' ' || c == (char)0x7F) return true;
103+
}
104+
return false;
105+
}
106+
}
107+
}

Basis/Packages/com.basis.framework/BasisUI/BasisFileBrowserUtility.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Basis/Packages/com.basis.framework/BasisUI/BasisSettingsDefaults.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,8 @@ public static bool IsRoleEnabledForCalibration(BasisBoneTrackedRole role)
11071107
// ---------------- ADMIN ----------------
11081108
public static BasisSettingsBinding<bool> AdminAutoRefreshPlayerList = new("admin_autorefresh_playerlist", new BasisPlatformDefault<bool>(true));
11091109

1110+
public static BasisSettingsBinding<bool> ShoutShowOnMenuBar = new("admin_shout_on_menubar", new BasisPlatformDefault<bool>(false));
1111+
11101112
// Limiter
11111113
public static BasisSettingsBinding<float> LimitThreshold = new("limitthreshold", new BasisPlatformDefault<float>(0.95f)); // pre-clip
11121114

@@ -1607,6 +1609,7 @@ public static void LoadAll()
16071609

16081610
// Admin
16091611
AdminAutoRefreshPlayerList.LoadBindingValue();
1612+
ShoutShowOnMenuBar.LoadBindingValue();
16101613

16111614
// Remote Player Audio
16121615
RAMinDistance.LoadBindingValue();

Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5050,6 +5050,14 @@
50505050
"key": "settings.main.title.copyBuildInfo",
50515051
"value": "Copy Build Info"
50525052
},
5053+
{
5054+
"key": "settings.admin.title.shout",
5055+
"value": "Shout"
5056+
},
5057+
{
5058+
"key": "settings.admin.title.showShoutOnMenuBar",
5059+
"value": "Show Shout On Menu Bar"
5060+
},
50535061
{
50545062
"key": "settings.admin.title.globalContentLocks",
50555063
"value": "Global Content Locks"

Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProviderParts/SettingsProviderAdminTab.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ public static PanelTabPage AdminTab(PanelTabGroup tabGroup)
3535

3636
AdminTabController controller = tab.gameObject.AddComponent<AdminTabController>();
3737

38+
// --- Menu-bar shout (local opt-in; off by default) ---
39+
PanelElementDescriptor shoutGroup =
40+
PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, container);
41+
shoutGroup.SetTitle(BasisLocalization.Get("settings.admin.title.shout"));
42+
shoutGroup.SetDescription("Local-only preference for your own menu. Does not affect other players or the server.");
43+
44+
PanelToggle shoutOnMenuBarToggle = PanelToggle.CreateNewEntry(shoutGroup.ContentParent);
45+
shoutOnMenuBarToggle.Descriptor.SetTitle(BasisLocalization.Get("settings.admin.title.showShoutOnMenuBar"));
46+
shoutOnMenuBarToggle.Descriptor.SetDescription("Adds the Shout option to the mic-mode button on your main menu bar. Off by default, so the button stays hidden until you enable it here.");
47+
shoutOnMenuBarToggle.AssignBinding(BasisSettingsDefaults.ShoutShowOnMenuBar);
48+
3849
// --- Global lock group ---
3950
PanelElementDescriptor lockGroup =
4051
PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, container);

Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProviderParts/SettingsProviderConsoleTab.cs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Diagnostics;
43
using System.IO;
54
using System.Linq;
65
using System.Text;
@@ -315,19 +314,6 @@ private static void OpenLatestCrashReportFolder()
315314
}
316315

317316
private static void OpenInFileBrowser(string path, bool selectFile)
318-
{
319-
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
320-
Process.Start(new ProcessStartInfo
321-
{
322-
FileName = "explorer.exe",
323-
Arguments = selectFile ? $"/select,\"{path}\"" : $"\"{path}\"",
324-
UseShellExecute = true
325-
});
326-
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
327-
Process.Start("open", selectFile ? $"-R \"{path}\"" : $"\"{path}\"");
328-
#else
329-
Application.OpenURL(selectFile ? Path.GetDirectoryName(path) : path);
330-
#endif
331-
}
317+
=> BasisFileBrowserUtility.Reveal(path, selectFile);
332318
}
333319
}

Basis/Packages/com.basis.framework/Networking/BasisCrashReportStore.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Text;
55
using Basis.Scripts.Networking;
6+
using Basis.Scripts.UI.UI_Panels;
67
using UnityEngine;
78

89
/// <summary>
@@ -32,6 +33,13 @@ public static class BasisCrashReportStore
3233
private const int MaxMessageChars = 2000;
3334
private const int MaxStackChars = 12000;
3435

36+
// Brief acknowledgement that the carried-over crash reports are being flushed on reconnect.
37+
// These are fire-and-forget sends queued in a tight loop, so the loading bar's idle timeout
38+
// clears this rather than a tracked per-packet curve.
39+
private const string ReplayIndicatorKey = "CrashReportReplay";
40+
private const string ReplayIndicatorLabel = "Uploading crash reports";
41+
private const float ReplayIndicatorPercent = 80f;
42+
3543
private static readonly object FileLock = new object();
3644
private static readonly List<Entry> _previous = new List<Entry>();
3745

@@ -162,6 +170,7 @@ public static void TryReplay()
162170
}
163171
_replayed = true;
164172

173+
BasisUILoadingBar.ProgressReport(ReplayIndicatorKey, ReplayIndicatorPercent, ReplayIndicatorLabel);
165174
foreach (Entry e in toSend)
166175
{
167176
BasisErrorReportSender.SendPrevious(e.System, e.Message, e.Stack);

Basis/Packages/com.basis.framework/Networking/BasisErrorReportSender.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Basis.Network.Core;
22
using Basis.Scripts.Device_Management;
33
using Basis.Scripts.Networking;
4+
using Basis.Scripts.UI.UI_Panels;
45
using System.Text.RegularExpressions;
56

67
/// <summary>
@@ -15,10 +16,18 @@ public static class BasisErrorReportSender
1516
private const int MaxMessageChars = 2000;
1617
private const int MaxStackChars = 12000;
1718

19+
// A live report is one fire-and-forget reliable packet, so there's no real progress to track —
20+
// this briefly surfaces that a report left the device and clears itself via the loading bar's
21+
// idle timeout. Keyed and shared, so a burst of distinct errors keeps one indicator up instead
22+
// of stacking many.
23+
private const string UploadIndicatorKey = "ErrorReportUpload";
24+
private const string UploadIndicatorLabel = "Uploading error report";
25+
private const float UploadIndicatorPercent = 80f;
26+
1827
public static void Report(byte severity, string system, string message, string stackTrace)
1928
{
2029
if (!BasisNetworkModeration.CrashReportingEnabled) return;
21-
BasisDeviceManagement.EnqueueOnMainThread(() => Send(severity, system, message, stackTrace));
30+
BasisDeviceManagement.EnqueueOnMainThread(() => Send(severity, system, message, stackTrace, showUploadIndicator: true));
2231
}
2332

2433
/// <summary>
@@ -30,10 +39,12 @@ public static void Report(byte severity, string system, string message, string s
3039
public static void SendPrevious(string system, string message, string stackTrace)
3140
{
3241
if (!BasisNetworkModeration.CrashReportingEnabled) return;
33-
BasisDeviceManagement.EnqueueOnMainThread(() => Send(2, system, message, stackTrace));
42+
// No per-report indicator here: the carried-over batch is acknowledged once as a whole by
43+
// BasisCrashReportStore.TryReplay rather than flashing once per replayed report.
44+
BasisDeviceManagement.EnqueueOnMainThread(() => Send(2, system, message, stackTrace, showUploadIndicator: false));
3445
}
3546

36-
private static void Send(byte severity, string system, string message, string stackTrace)
47+
private static void Send(byte severity, string system, string message, string stackTrace, bool showUploadIndicator)
3748
{
3849
try
3950
{
@@ -57,6 +68,12 @@ private static void Send(byte severity, string system, string message, string st
5768
BasisNetworkCommons.EventsChannel,
5869
DeliveryMethod.ReliableOrdered);
5970

71+
// Briefly surface that a report left the device (see UploadIndicator* notes above).
72+
if (showUploadIndicator)
73+
{
74+
BasisUILoadingBar.ProgressReport(UploadIndicatorKey, UploadIndicatorPercent, UploadIndicatorLabel);
75+
}
76+
6077
// A successful live send means we're connected and reporting is enabled, so this
6178
// is also the right moment to flush any crash reports held over from a previous
6279
// session. Guarded internally so it only happens once.

Basis/Packages/com.basis.framework/Networking/BasisLogBundleReceiver.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks;
44
using Basis.Network.Core;
55
using Basis.Scripts.Device_Management;
6+
using Basis.Scripts.UI.UI_Panels;
67
using K4os.Compression.LZ4;
78
using UnityEngine;
89

@@ -31,6 +32,28 @@ public static class BasisLogBundleReceiver
3132
private static byte[] _buffer;
3233
private static int _offset;
3334

35+
// Loading-bar progress (admin "pull server logs" is a chunked download with real elapsed time).
36+
private const string ProgressKey = "ServerLogDownload";
37+
private const string DownloadLabel = "Downloading server logs";
38+
private const string ExtractLabel = "Extracting server logs";
39+
private static int _lastReportedPercent;
40+
41+
// Chunks map to 0–90% so the bar stays open across the End()/extraction handoff; extraction
42+
// reports 95% and completion reports 100% (which removes the entry). Throttled to whole-percent
43+
// changes so a many-chunk transfer doesn't flood the main-thread dispatch queue.
44+
private static void ReportReceiveProgress()
45+
{
46+
float pct = _totalChunks > 0 ? (_received / (float)_totalChunks) * 90f : 2f;
47+
if (pct < 2f) pct = 2f;
48+
int rounded = Mathf.RoundToInt(pct);
49+
if (rounded == _lastReportedPercent) return;
50+
_lastReportedPercent = rounded;
51+
BasisUILoadingBar.ProgressReport(ProgressKey, pct, DownloadLabel);
52+
}
53+
54+
// Reporting 100 removes the entry (and closes the bar if nothing else is loading).
55+
private static void ClearProgress() => BasisUILoadingBar.ProgressReport(ProgressKey, 100f, DownloadLabel);
56+
3457
public static void Begin(NetDataReader reader)
3558
{
3659
try
@@ -62,6 +85,8 @@ public static void Begin(NetDataReader reader)
6285
_received = 0;
6386
_active = true;
6487
BasisDebug.Log($"Receiving server log bundle: {payloadBytes / 1024} KB in {totalChunks} chunk(s).", BasisDebug.LogTag.Networking);
88+
_lastReportedPercent = -1;
89+
ReportReceiveProgress();
6590
}
6691
catch (Exception e)
6792
{
@@ -87,6 +112,7 @@ public static void Chunk(NetDataReader reader)
87112
Buffer.BlockCopy(data, 0, _buffer, _offset, data.Length);
88113
_offset += data.Length;
89114
_received++;
115+
ReportReceiveProgress();
90116
}
91117
catch (Exception e)
92118
{
@@ -121,6 +147,7 @@ public static void End(NetDataReader reader)
121147
if (!ok)
122148
{
123149
BasisDebug.LogError($"Server reported log bundle failure: {message}");
150+
ClearProgress();
124151
BasisNetworkModeration.DisplayMessage(string.IsNullOrEmpty(message) ? "Server failed to send logs." : message);
125152
Reset();
126153
return;
@@ -129,6 +156,7 @@ public static void End(NetDataReader reader)
129156
if (_offset != _payloadBytes || _received != _totalChunks)
130157
{
131158
BasisDebug.LogError($"Log bundle incomplete ({_offset}/{_payloadBytes} bytes, {_received}/{_totalChunks} chunks).");
159+
ClearProgress();
132160
BasisNetworkModeration.DisplayMessage("Log transfer was incomplete; please try again.");
133161
Reset();
134162
return;
@@ -146,6 +174,7 @@ public static void End(NetDataReader reader)
146174
string destDir = Path.Combine(root, $"{serverNameSafe}_{stamp}");
147175
Reset();
148176

177+
BasisUILoadingBar.ProgressReport(ProgressKey, 95f, ExtractLabel);
149178
_ = Task.Run(() => ExpandAndNotify(payload, payloadLen, rawLen, compressed, destDir));
150179
}
151180

@@ -172,12 +201,14 @@ private static void ExpandAndNotify(byte[] payload, int payloadLen, int rawLen,
172201
int fileCount = ExtractContainer(raw, rawLen, destDir);
173202

174203
BasisDebug.Log($"Saved {fileCount} server log file(s) to {destDir}", BasisDebug.LogTag.Networking);
204+
ClearProgress();
175205
BasisDeviceManagement.EnqueueOnMainThread(() =>
176-
BasisNetworkModeration.DisplayMessage($"Server logs saved to:\n{destDir}"));
206+
BasisNetworkModeration.DisplayMessageWithFolder($"Server logs saved to:\n{destDir}", destDir));
177207
}
178208
catch (Exception e)
179209
{
180210
BasisDebug.LogError($"Failed to save server logs: {e.Message}");
211+
ClearProgress();
181212
BasisDeviceManagement.EnqueueOnMainThread(() =>
182213
BasisNetworkModeration.DisplayMessage($"Failed to save server logs: {e.Message}"));
183214
}

0 commit comments

Comments
 (0)