diff --git a/ProjectLighthouse.Localization/BaseLayout.resx b/ProjectLighthouse.Localization/BaseLayout.resx index 6cd9c681..65d1229f 100644 --- a/ProjectLighthouse.Localization/BaseLayout.resx +++ b/ProjectLighthouse.Localization/BaseLayout.resx @@ -87,4 +87,10 @@ If not, please publish the source code somewhere accessible to your users. + + Read-Only Mode + + + This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled. + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs b/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs index 64f39021..f46cb4e6 100644 --- a/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs +++ b/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs @@ -23,5 +23,8 @@ public static class BaseLayoutStrings public static readonly TranslatableString LicenseWarn2 = create("license_warn_2"); public static readonly TranslatableString LicenseWarn3 = create("license_warn_3"); + public static readonly TranslatableString ReadOnlyWarnTitle = create("read_only_warn_title"); + public static readonly TranslatableString ReadOnlyWarn = create("read_only_warn"); + private static TranslatableString create(string key) => new(TranslationAreas.BaseLayout, key); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index 76f2b715..a906b170 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -119,6 +119,9 @@ public class CommentController : ControllerBase { GameTokenEntity token = this.GetToken(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + GameComment? comment = await this.DeserializeBody(); if (comment?.Message == null) return this.BadRequest(); @@ -159,6 +162,9 @@ public class CommentController : ControllerBase { GameTokenEntity token = this.GetToken(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + if ((slotId == 0 || SlotHelper.IsTypeInvalid(slotType)) == (username == null)) return this.BadRequest(); CommentEntity? comment = await this.database.Comments.FirstOrDefaultAsync(c => c.CommentId == commentId); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs index 3d3f16be..3405818f 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs @@ -59,6 +59,13 @@ along with this program. If not, see ."; announceText.Replace("%user", username); announceText.Replace("%id", token.UserId.ToString()); + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) + { + announceText.Insert(0, "This instance is currently in read-only mode. Level and photo uploads, comments, " + + "reviews, and certain profile changes will be restricted until read-only mode is " + + "disabled."); + } + #if DEBUG announceText.Append("\n\n---DEBUG INFO---\n" + $"user.UserId: {token.UserId}\n" + diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index e7962643..4c73d0a0 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -37,6 +37,9 @@ public class PhotosController : ControllerBase { GameTokenEntity token = this.GetToken(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + int photoCount = await this.database.Photos.CountAsync(p => p.CreatorId == token.UserId); if (photoCount >= ServerConfiguration.Instance.UserGeneratedContentLimits.PhotosQuota) return this.BadRequest(); @@ -90,7 +93,7 @@ public class PhotosController : ControllerBase case SlotType.Developer: { SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == photoSlot.SlotType && s.InternalSlotId == photoSlot.SlotId); - if (slot != null) + if (slot != null) photoSlot.SlotId = slot.SlotId; else photoSlot.SlotId = await SlotHelper.GetPlaceholderSlotId(this.database, photoSlot.SlotId, photoSlot.SlotType); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs index 3c1c2f82..7231ccaf 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs @@ -1,5 +1,6 @@ #nullable enable using System.Text; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Files; using LBPUnion.ProjectLighthouse.Logging; @@ -58,10 +59,14 @@ public class ResourcesController : ControllerBase string fullPath = Path.GetFullPath(path); FileHelper.EnsureDirectoryCreated(assetsDirectory); - // lbp treats code 409 as success and as an indicator that the file is already present + + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + + // LBP treats code 409 as success and as an indicator that the file is already present if (FileHelper.ResourceExists(hash)) return this.Conflict(); - // theoretically shouldn't be possible because of hash check but handle anyways + // Theoretically shouldn't be possible because of hash check but handle anyways if (!fullPath.StartsWith(FileHelper.FullResourcePath)) return this.BadRequest(); Logger.Info($"Processing resource upload (hash: {hash})", LogArea.Resources); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index e09a06e6..7bedc01d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -43,6 +43,9 @@ public class PublishController : ControllerBase UserEntity? user = await this.database.UserFromGameToken(token); if (user == null) return this.Forbid(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + GameUserSlot? slot = await this.DeserializeBody(); if (slot == null) { @@ -116,6 +119,9 @@ public class PublishController : ControllerBase UserEntity? user = await this.database.UserFromGameToken(token); if (user == null) return this.Forbid(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + GameUserSlot? slot = await this.DeserializeBody(); if (slot == null) @@ -335,6 +341,9 @@ public class PublishController : ControllerBase { GameTokenEntity token = this.GetToken(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == id); if (slot == null) return this.NotFound(); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index aa4e1c5b..7feae4bf 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -1,4 +1,5 @@ #nullable enable +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; @@ -92,6 +93,9 @@ public class ReviewController : ControllerBase { GameTokenEntity token = this.GetToken(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + GameReview? newReview = await this.DeserializeBody(); if (newReview == null) return this.BadRequest(); @@ -115,7 +119,7 @@ public class ReviewController : ControllerBase } review.Thumb = Math.Clamp(newReview.Thumb, -1, 1); review.LabelCollection = LabelHelper.RemoveInvalidLabels(newReview.LabelCollection); - + review.Text = newReview.Text; review.Deleted = false; review.Timestamp = TimeHelper.TimestampMillis; @@ -239,6 +243,9 @@ public class ReviewController : ControllerBase { GameTokenEntity token = this.GetToken(); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + int creatorId = await this.database.Slots.Where(s => s.SlotId == slotId).Select(s => s.CreatorId).FirstOrDefaultAsync(); if (creatorId == 0) return this.BadRequest(); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs index fb5b6128..cbed989b 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Files; @@ -73,6 +74,9 @@ public class UserController : ControllerBase if (update.Biography != null) { + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + if (update.Biography.Length > 512) return this.BadRequest(); user.Biography = update.Biography; @@ -85,6 +89,9 @@ public class UserController : ControllerBase { if (string.IsNullOrWhiteSpace(resource)) continue; + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + if (!FileHelper.ResourceExists(resource) && !resource.StartsWith('g')) return this.BadRequest(); if (!GameResourceHelper.IsValidTexture(resource)) return this.BadRequest(); diff --git a/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs b/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs index 161bb1a8..284522ff 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs @@ -39,6 +39,9 @@ public class UserPageController : ControllerBase WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request); if (token == null) return this.Redirect("~/login"); + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + if (msg == null) { Logger.Error($"Refusing to post comment from {token.UserId} on user {id}, {nameof(msg)} is null", LogArea.Comments); diff --git a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml index 5de98aa3..14f14782 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml @@ -178,6 +178,18 @@ } + @if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) + { +
+
+ + @Model.Translate(BaseLayoutStrings.ReadOnlyWarnTitle) +

+ @Html.Raw(Model.Translate(BaseLayoutStrings.ReadOnlyWarn)) +

+
+
+ }
diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml index 4a2a9869..cbab2fdb 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml @@ -1,4 +1,5 @@ @using System.Web +@using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.Database @using LBPUnion.ProjectLighthouse.Localization @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @@ -31,18 +32,32 @@ @if (Model.CommentsEnabled && Model.User != null) {
-
-
- + @if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) + { +
+

+ + @ServerConfiguration.Instance.Customization.ServerName is currently in read-only mode. + You will not be able to post comments until read-only mode is disabled. + +

- - + } + else + { +
+
+ +
+ +
+ } @if (Model.Comments.Count > 0) {
} } - + @{ int i = 0; foreach (KeyValuePair commentAndReaction in Model.Comments) diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml index 61034265..f8c16a5f 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml @@ -4,7 +4,6 @@ @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Types.Entities.Level @using LBPUnion.ProjectLighthouse.Types.Serialization - @{ bool isMobile = (bool?)ViewData["IsMobile"] ?? false; bool canDelete = (bool?)ViewData["CanDelete"] ?? false; @@ -29,6 +28,18 @@
} + @if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) + { +
+

+ + @ServerConfiguration.Instance.Customization.ServerName is currently in read-only mode. + You will not be able to post reviews in-game until read-only mode is disabled. + +

+
+ } + @for(int i = 0; i < Model.Reviews.Count; i++) { ReviewEntity review = Model.Reviews[i]; @@ -36,7 +47,7 @@ -1 => review.Reviewer?.BooHash, 0 => review.Reviewer?.MehHash, 1 => review.Reviewer?.YayHash, - + _ => throw new ArgumentOutOfRangeException(), }) ?? ""; @@ -49,7 +60,7 @@ -1 => "Boo!", 0 => "Meh.", 1 => "Yay!", - + _ => throw new ArgumentOutOfRangeException(), }; @@ -114,7 +125,7 @@ if (window.confirm("Are you sure you want to delete this?\nThis action cannot be undone.")){ window.location.hash = "reviews"; window.location.href = "/moderation/deleteReview/" + reviewId + "?callbackUrl=" + this.window.location; - } + } }
diff --git a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs index 066ca973..cf24b0b2 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs @@ -41,12 +41,21 @@ public class UserSettingsPage : BaseLayout string? avatarHash = await FileHelper.ParseBase64Image(avatar); - if (avatarHash != null) this.ProfileUser.IconHash = avatarHash; + if (avatarHash != null) + { + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + + this.ProfileUser.IconHash = avatarHash; + } if (this.User.IsAdmin) this.ProfileUser.ProfileTag = profileTag; if (biography != null) { + // Deny request if in read-only mode + if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest(); + biography = CensorHelper.FilterMessage(biography); if (this.ProfileUser.Biography != biography && biography.Length <= 512) this.ProfileUser.Biography = biography; diff --git a/ProjectLighthouse/Configuration/ConfigurationCategories/UserGeneratedContentLimitConfiguration.cs b/ProjectLighthouse/Configuration/ConfigurationCategories/UserGeneratedContentLimitConfiguration.cs index 6430b754..f725c278 100644 --- a/ProjectLighthouse/Configuration/ConfigurationCategories/UserGeneratedContentLimitConfiguration.cs +++ b/ProjectLighthouse/Configuration/ConfigurationCategories/UserGeneratedContentLimitConfiguration.cs @@ -11,6 +11,12 @@ public class UserGeneratedContentLimitConfiguration public int PhotosQuota { get; set; } = 500; + /// + /// When enabled, all UGC uploads are disabled. This includes levels, photos, reviews, + /// comments, and certain profile settings. + /// + public bool ReadOnlyMode { get; set; } = false; + public bool ProfileCommentsEnabled { get; set; } = true; public bool LevelCommentsEnabled { get; set; } = true; diff --git a/ProjectLighthouse/Configuration/ServerConfiguration.cs b/ProjectLighthouse/Configuration/ServerConfiguration.cs index 7258804e..30c7c89b 100644 --- a/ProjectLighthouse/Configuration/ServerConfiguration.cs +++ b/ProjectLighthouse/Configuration/ServerConfiguration.cs @@ -11,7 +11,7 @@ public class ServerConfiguration : ConfigurationBase // This is so Lighthouse can properly identify outdated configurations and update them with newer settings accordingly. // If you are modifying anything here, this value MUST be incremented. // Thanks for listening~ - public override int ConfigVersion { get; set; } = 25; + public override int ConfigVersion { get; set; } = 26; public override string ConfigName { get; set; } = "lighthouse.yml"; public string WebsiteListenUrl { get; set; } = "http://localhost:10060";