Skip to content

Commit eb0a394

Browse files
committed
ready to deploy
1 parent 8b64026 commit eb0a394

11 files changed

Lines changed: 337 additions & 44 deletions

File tree

Basis Server/BasisNetworkServer/Core/BasisServerHandleEvents.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,8 @@ public static void SendAvatarMessageToClients(NetPacketReader Reader, NetPeer Pe
767767
ClientAvatarChangeMessage.Deserialize(Reader);
768768
Reader.Recycle();
769769

770-
// Global avatar lock: reject network broadcast but still save state locally
770+
// Global avatar lock: drop the change outright — neither broadcast nor saved, so a
771+
// late joiner isn't handed an avatar the lock exists to keep out of the instance.
771772
if (BasisNetworkServer.Security.BasisGlobalLockManager.AvatarsLocked)
772773
{
773774
bool hasBypass = false;

Basis Server/BasisNetworkServer/Security/BasisGlobalLockManager.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Basis.Network.Core;
1+
using Basis.Network.Core;
22
using System.Threading;
33
using static BasisNetworkCore.Serializable.SerializableBasis;
44

@@ -77,6 +77,38 @@ public static void InitializeFromConfig(Configuration config)
7777
Interlocked.Exchange(ref _endEffectorIKDisabled, config.EndEffectorIKDisabled ? 1 : 0);
7878
}
7979

80+
/// <summary>
81+
/// Copies the live lock state back onto the configuration object so a caller can persist it
82+
/// to config.xml. The mirror image of <see cref="InitializeFromConfig"/> — every field that
83+
/// seeds a flag at boot is written here, or an admin's toggle silently reverts on restart.
84+
/// </summary>
85+
public static void WriteToConfig(Configuration config)
86+
{
87+
if (config == null)
88+
{
89+
return;
90+
}
91+
92+
config.AvatarsLocked = AvatarsLocked;
93+
config.PropsLocked = PropsLocked;
94+
config.WorldsLocked = WorldsLocked;
95+
config.ServersLocked = ServersLocked;
96+
config.ThirdPersonDisabled = ThirdPersonDisabled;
97+
config.AdditionalAvatarDataLock = AdditionalAvatarDataLock;
98+
config.CameraMetadataDisallowMask = CameraMetadataDisallowMask;
99+
config.PlayspaceMoverLocked = PlayspaceMoverLocked;
100+
config.DirectConnectLocked = DirectConnectLocked;
101+
config.CilboxLocked = CilboxLocked;
102+
config.ImagesLocked = ImagesLocked;
103+
config.TextChatLocked = TextChatLocked;
104+
config.VoiceChatLocked = VoiceChatLocked;
105+
config.MediaPlayerLocked = MediaPlayerLocked;
106+
config.CameraCaptureLocked = CameraCaptureLocked;
107+
config.PropGrabbingLocked = PropGrabbingLocked;
108+
config.SafeDisplayNamesForced = SafeDisplayNamesForced;
109+
config.EndEffectorIKDisabled = EndEffectorIKDisabled;
110+
}
111+
80112
/// <summary>
81113
/// Toggle avatar loading. Returns the new state (true = locked).
82114
/// </summary>

Basis Server/BasisNetworkServer/Security/BasisPlayerModeration.cs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public static void OnAdminMessage(NetPeer peer, NetPacketReader reader)
370370

371371
case AdminRequestMode.GlobalToggleThirdPerson:
372372
Require(peer, PermNodes.ModerationGlobalLock, () =>
373-
HandleGlobalToggle(peer, "Third-person camera", BasisGlobalLockManager.ToggleThirdPerson()));
373+
HandleGlobalFeatureToggle(peer, "The third-person camera", BasisGlobalLockManager.ToggleThirdPerson()));
374374
break;
375375

376376
case AdminRequestMode.GlobalToggleAdditionalAvatarDataLock:
@@ -418,7 +418,7 @@ public static void OnAdminMessage(NetPeer peer, NetPacketReader reader)
418418

419419
case AdminRequestMode.GlobalToggleCilbox:
420420
Require(peer, PermNodes.ModerationGlobalLock, () =>
421-
HandleGlobalToggle(peer, "Avatar Cilbox code", BasisGlobalLockManager.ToggleCilbox()));
421+
HandleGlobalFeatureToggle(peer, "Avatar Cilbox code", BasisGlobalLockManager.ToggleCilbox()));
422422
break;
423423

424424
case AdminRequestMode.GlobalToggleImages:
@@ -428,37 +428,37 @@ public static void OnAdminMessage(NetPeer peer, NetPacketReader reader)
428428

429429
case AdminRequestMode.GlobalToggleEndEffectorIK:
430430
Require(peer, PermNodes.ModerationGlobalLock, () =>
431-
HandleGlobalToggle(peer, "Remote end-effector IK", BasisGlobalLockManager.ToggleEndEffectorIK()));
431+
HandleGlobalFeatureToggle(peer, "Remote end-effector IK", BasisGlobalLockManager.ToggleEndEffectorIK()));
432432
break;
433433

434434
case AdminRequestMode.GlobalToggleTextChat:
435435
Require(peer, PermNodes.ModerationGlobalLock, () =>
436-
HandleGlobalToggle(peer, "Text chat", BasisGlobalLockManager.ToggleTextChat()));
436+
HandleGlobalFeatureToggle(peer, "Text chat", BasisGlobalLockManager.ToggleTextChat()));
437437
break;
438438

439439
case AdminRequestMode.GlobalToggleVoiceChat:
440440
Require(peer, PermNodes.ModerationGlobalLock, () =>
441-
HandleGlobalToggle(peer, "Voice chat", BasisGlobalLockManager.ToggleVoiceChat()));
441+
HandleGlobalFeatureToggle(peer, "Voice chat", BasisGlobalLockManager.ToggleVoiceChat()));
442442
break;
443443

444444
case AdminRequestMode.GlobalToggleMediaPlayer:
445445
Require(peer, PermNodes.ModerationGlobalLock, () =>
446-
HandleGlobalToggle(peer, "Media player", BasisGlobalLockManager.ToggleMediaPlayer()));
446+
HandleGlobalFeatureToggle(peer, "Media players", BasisGlobalLockManager.ToggleMediaPlayer()));
447447
break;
448448

449449
case AdminRequestMode.GlobalToggleCameraCapture:
450450
Require(peer, PermNodes.ModerationGlobalLock, () =>
451-
HandleGlobalToggle(peer, "Camera capture", BasisGlobalLockManager.ToggleCameraCapture()));
451+
HandleGlobalFeatureToggle(peer, "Camera capture", BasisGlobalLockManager.ToggleCameraCapture()));
452452
break;
453453

454454
case AdminRequestMode.GlobalTogglePropGrabbing:
455455
Require(peer, PermNodes.ModerationGlobalLock, () =>
456-
HandleGlobalToggle(peer, "Prop grabbing", BasisGlobalLockManager.TogglePropGrabbing()));
456+
HandleGlobalFeatureToggle(peer, "Prop grabbing", BasisGlobalLockManager.TogglePropGrabbing()));
457457
break;
458458

459459
case AdminRequestMode.GlobalToggleSafeDisplayNames:
460460
Require(peer, PermNodes.ModerationGlobalLock, () =>
461-
HandleGlobalToggle(peer, "Safe display names", BasisGlobalLockManager.ToggleSafeDisplayNames()));
461+
HandleGlobalProtectionToggle(peer, "Safe display names", BasisGlobalLockManager.ToggleSafeDisplayNames()));
462462
break;
463463

464464
case AdminRequestMode.SetGlobalAvatarScaleLimits:
@@ -1267,6 +1267,7 @@ private static void BroadcastGlobalLockNotice(NetPeer peer, string adminReply, s
12671267
NetworkServer.BroadcastMessageToClients(writer, BasisNetworkCommons.AdminChannel, NetworkServer.PeerSnapshot, DeliveryMethod.ReliableOrdered);
12681268
NetworkServer.ReturnWriter(writer);
12691269

1270+
PersistGlobalLockState();
12701271
BasisGlobalLockManager.BroadcastLockState();
12711272
}
12721273

@@ -1276,6 +1277,28 @@ private static void HandleGlobalToggle(NetPeer peer, string contentType, bool no
12761277
HandleGlobalStateNotification(peer, $"{contentType} loading has been globally {state} by an admin.");
12771278
}
12781279

1280+
/// <summary>
1281+
/// Notification for locks over a live feature rather than content loading (chat, voice,
1282+
/// grabbing, ...). HandleGlobalToggle's "<c>X loading</c>" template reads as nonsense for
1283+
/// these — nothing is being loaded — so they get a plain "<c>X has been ... DISABLED</c>".
1284+
/// </summary>
1285+
private static void HandleGlobalFeatureToggle(NetPeer peer, string featureName, bool nowLocked)
1286+
{
1287+
string state = nowLocked ? "DISABLED" : "ENABLED";
1288+
HandleGlobalStateNotification(peer, $"{featureName} has been globally {state} by an admin.");
1289+
}
1290+
1291+
/// <summary>
1292+
/// Notification for a protection that is ENABLED when its flag is set — the opposite sense
1293+
/// to every lock above, so the shared templates would announce the exact inverse of what
1294+
/// the admin just did.
1295+
/// </summary>
1296+
private static void HandleGlobalProtectionToggle(NetPeer peer, string protectionName, bool nowEnforced)
1297+
{
1298+
string state = nowEnforced ? "ENABLED" : "DISABLED";
1299+
HandleGlobalStateNotification(peer, $"{protectionName} has been globally {state} by an admin.");
1300+
}
1301+
12791302
/// <summary>
12801303
/// Notifies the toggling admin + all clients with a pre-composed, unambiguous message and
12811304
/// rebroadcasts the lock state. Toggles whose semantics don't fit the
@@ -1297,9 +1320,21 @@ private static void HandleGlobalStateNotification(NetPeer peer, string notificat
12971320
NetworkServer.ReturnWriter(writer);
12981321

12991322
// Broadcast updated lock state so clients track it
1323+
PersistGlobalLockState();
13001324
BasisGlobalLockManager.BroadcastLockState();
13011325
}
13021326

1327+
/// <summary>
1328+
/// Mirrors the live global lock state onto Configuration and writes config.xml. Every lock
1329+
/// seeds itself from config at boot, so a toggle that isn't persisted here silently reverts
1330+
/// on the next restart.
1331+
/// </summary>
1332+
private static void PersistGlobalLockState()
1333+
{
1334+
BasisGlobalLockManager.WriteToConfig(NetworkServer.Configuration);
1335+
SaveConfig();
1336+
}
1337+
13031338
private static void HandleHeadlessAudioSet(NetPeer peer, NetPacketReader reader)
13041339
{
13051340
if (reader.AvailableBytes < 1)
@@ -1343,6 +1378,9 @@ private static void HandleHeadlessDisallowSet(NetPeer peer, NetPacketReader read
13431378
BasisHeadlessConnectionPolicyManager.DisconnectConnectedHeadlessPeers();
13441379
}
13451380

1381+
// Seeded from Configuration.DisallowHeadless at boot — persist or it reverts on restart.
1382+
NetworkServer.Configuration.DisallowHeadless = BasisHeadlessConnectionPolicyManager.HeadlessDisallowed;
1383+
SaveConfig();
13461384
BasisHeadlessConnectionPolicyManager.BroadcastState();
13471385
}
13481386

@@ -1378,6 +1416,7 @@ private static void HandleCameraPolicySet(NetPeer peer, NetPacketReader reader)
13781416
BasisGlobalLockManager.SetCameraMetadataDisallowMask(mask);
13791417
BNL.Log($"Camera photo-metadata disallow mask set to {mask}.");
13801418
SendBackMessage(peer, $"Camera metadata policy updated (mask {mask}).");
1419+
PersistGlobalLockState();
13811420
BasisGlobalLockManager.BroadcastLockState();
13821421
}
13831422

Basis Server/BasisServerTests/Security/SecurityListAndLockManagerTests.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Basis.Network.Core;
1+
using Basis.Network.Core;
22
using Basis.Network.Server.Auth;
33
using BasisNetworkCore.Security;
44
using BasisNetworkServer.BasisNetworking;
@@ -503,6 +503,7 @@ public class BasisGlobalLockManagerTests
503503
MediaPlayerLocked = false,
504504
CameraCaptureLocked = false,
505505
PropGrabbingLocked = false,
506+
SafeDisplayNamesForced = false,
506507
};
507508

508509
private static Configuration AllLocked() => new()
@@ -524,6 +525,7 @@ public class BasisGlobalLockManagerTests
524525
MediaPlayerLocked = true,
525526
CameraCaptureLocked = true,
526527
PropGrabbingLocked = true,
528+
SafeDisplayNamesForced = true,
527529
};
528530

529531
private static void AssertAllFlags(bool expected)
@@ -544,6 +546,50 @@ private static void AssertAllFlags(bool expected)
544546
Assert.Equal(expected, BasisGlobalLockManager.MediaPlayerLocked);
545547
Assert.Equal(expected, BasisGlobalLockManager.CameraCaptureLocked);
546548
Assert.Equal(expected, BasisGlobalLockManager.PropGrabbingLocked);
549+
Assert.Equal(expected, BasisGlobalLockManager.SafeDisplayNamesForced);
550+
}
551+
552+
/// <summary>
553+
/// Every lock seeds itself from Configuration at boot, so a toggle that never reaches
554+
/// Configuration silently reverts on restart. WriteToConfig is the mirror of
555+
/// InitializeFromConfig and must carry every field back — including the mask.
556+
/// </summary>
557+
[Fact]
558+
public void WriteToConfig_RoundTripsEveryFlagAndTheMask()
559+
{
560+
try
561+
{
562+
BasisGlobalLockManager.InitializeFromConfig(AllLocked());
563+
564+
Configuration persisted = AllUnlocked();
565+
BasisGlobalLockManager.WriteToConfig(persisted);
566+
567+
// Reseeding from what was written must reproduce the state that was written out.
568+
BasisGlobalLockManager.InitializeFromConfig(AllUnlocked());
569+
AssertAllFlags(false);
570+
BasisGlobalLockManager.InitializeFromConfig(persisted);
571+
AssertAllFlags(true);
572+
Assert.Equal(0xAB, BasisGlobalLockManager.CameraMetadataDisallowMask);
573+
}
574+
finally
575+
{
576+
BasisGlobalLockManager.InitializeFromConfig(AllUnlocked());
577+
}
578+
}
579+
580+
[Fact]
581+
public void WriteToConfig_IgnoresANullConfiguration()
582+
{
583+
BasisGlobalLockManager.InitializeFromConfig(AllLocked());
584+
try
585+
{
586+
BasisGlobalLockManager.WriteToConfig(null);
587+
AssertAllFlags(true);
588+
}
589+
finally
590+
{
591+
BasisGlobalLockManager.InitializeFromConfig(AllUnlocked());
592+
}
547593
}
548594

549595
[Fact]

Basis/Packages/com.basis.camera/Runtime/BasisHandHeldCamera.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Basis;
1+
using Basis;
22
using Basis.BasisUI;
33
using Basis.ImagePickup;
44
using Basis.Scripts.Audio;
@@ -1215,6 +1215,14 @@ public void Timer()
12151215
return;
12161216
}
12171217

1218+
// Same gate CapturePhoto applies: the timer is just a delayed capture, so a locked client
1219+
// must not start one — and must not broadcast the countdown remotes replay.
1220+
if (BasisNetworkModeration.CameraCaptureBlockedLocally)
1221+
{
1222+
BasisDebug.LogWarning("Timer blocked: camera capture is locked by an admin.", BasisDebug.LogTag.Camera);
1223+
return;
1224+
}
1225+
12181226
// Notify remote clients so they replay the same tick/shutter timing
12191227
if (BasisNetworkConnection.LocalPlayerPeer != null)
12201228
{
@@ -1255,6 +1263,18 @@ private IEnumerator DelayedAction(float delaySeconds)
12551263
countdownText.text = "!";
12561264
yield return new WaitForSeconds(0.5f);
12571265

1266+
countdownRoutine = null;
1267+
1268+
// Re-checked here because an admin can lock capture during the countdown, and before the
1269+
// shutter sound for the same reason CapturePhoto checks early: a refusal must not sound
1270+
// like a photo was taken.
1271+
if (BasisNetworkModeration.CameraCaptureBlockedLocally)
1272+
{
1273+
BasisDebug.LogWarning("Timer capture blocked: camera capture is locked by an admin.", BasisDebug.LogTag.Camera);
1274+
countdownText.text = string.Empty;
1275+
yield break;
1276+
}
1277+
12581278
// Choose formats based on captureFormat
12591279
GetCaptureFormats(out TextureFormat format, out RenderTextureFormat renderFormat);
12601280

@@ -1264,8 +1284,6 @@ private IEnumerator DelayedAction(float delaySeconds)
12641284
BasisUISounds.PlayAt(BasisUISoundEvent.CameraShutter, BasisDeviceManagement.Instance.CameraShutterSound, captureCamera.transform.position, SMModuleAudio.ActivePropVolume);
12651285
}
12661286

1267-
countdownRoutine = null;
1268-
12691287
if (capture360Enabled)
12701288
StartCoroutine(TakeScreenshot360(captureFormat == "EXR"));
12711289
else

0 commit comments

Comments
 (0)