diff --git a/GanjooRazor/Pages/GanjoorPage.cshtml.cs b/GanjooRazor/Pages/GanjoorPage.cshtml.cs index cb9e0716..8802af7c 100644 --- a/GanjooRazor/Pages/GanjoorPage.cshtml.cs +++ b/GanjooRazor/Pages/GanjoorPage.cshtml.cs @@ -239,10 +239,14 @@ namespace GanjooRazor.Pages }); } + string rawError = await ReadErrorMessageAsync(response); + var sanitizerInfo = TryParseSanitizerTextDroppedError(rawError); + return Partial("~/Pages/Partials/GanjoorPage/_CommentPartial.cshtml", new _CommentPartialModel() { Comment = null, - Error = await ReadErrorMessageAsync(response), + Error = sanitizerInfo != null ? sanitizerInfo.Message : rawError, + SanitizerRemainingText = sanitizerInfo?.RemainingText, InReplyTo = null, LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]), PoemId = poemId, @@ -270,7 +274,7 @@ namespace GanjooRazor.Pages var response = await secureClient.PutAsync($"{APIRoot.Url}/api/ganjoor/comment/{id}", new StringContent(JsonConvert.SerializeObject(comment), Encoding.UTF8, "application/json")); if (!response.IsSuccessStatusCode) { - return new BadRequestObjectResult(await ReadErrorMessageAsync(response)); + return await BadRequestFromApiErrorAsync(response); } return new JsonResult(true); }); @@ -1145,7 +1149,7 @@ namespace GanjooRazor.Pages var response = await secureClient.PutAsync($"{APIRoot.Url}/api/ganjoor/bookmark/{id}", new StringContent(JsonConvert.SerializeObject(note), Encoding.UTF8, "application/json")); if (!response.IsSuccessStatusCode) { - return new BadRequestObjectResult(await ReadErrorMessageAsync(response)); + return await BadRequestFromApiErrorAsync(response); } return new JsonResult(true); }); diff --git a/GanjooRazor/Pages/GanjoorPageModelBase.cs b/GanjooRazor/Pages/GanjoorPageModelBase.cs index 738cd5e3..b35a0679 100644 --- a/GanjooRazor/Pages/GanjoorPageModelBase.cs +++ b/GanjooRazor/Pages/GanjoorPageModelBase.cs @@ -49,6 +49,41 @@ namespace GanjooRazor.Pages return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); } + /// + /// Shape of the special error string the API sends (still just a plain string, see + /// GanjoorService._BuildSanitizerDroppedTextError on the RMuseum side) when a comment/note/ + /// suggestion was rejected because sanitizing it had to drop real text. Message is the + /// human-readable explanation; RemainingText is the plain text that would have remained, + /// so the client can diff it against what the user actually typed and show exactly what + /// would have been dropped. + /// + protected class SanitizerTextDroppedInfo + { + public bool SanitizerTextDropped { get; set; } + public string Message { get; set; } + public string RemainingText { get; set; } + } + + /// + /// Returns the parsed if rawErrorMessage is that + /// special shape, or null for any ordinary plain-text error (including when rawErrorMessage + /// isn't JSON at all, which is the common case). + /// + protected static SanitizerTextDroppedInfo TryParseSanitizerTextDroppedError(string rawErrorMessage) + { + if (string.IsNullOrWhiteSpace(rawErrorMessage) || rawErrorMessage.TrimStart()[0] != '{') + return null; + try + { + var info = JsonConvert.DeserializeObject(rawErrorMessage); + return (info != null && info.SanitizerTextDropped) ? info : null; + } + catch + { + return null; + } + } + /// /// Runs against an HttpClient authenticated from the current /// session cookies (via ). If the session @@ -61,6 +96,29 @@ namespace GanjooRazor.Pages /// (rather than a bare 400) should keep their own using/PrepareClient block instead - wrapping /// those here would silently change what the browser shows on a real (non-AJAX) form submit. /// + /// + /// Builds the BadRequest to return for an AJAX handler from a failed API response: the + /// plain error string as before for an ordinary error, or - when it's the "sanitizing had + /// to drop real text" case - a small JSON object ({ sanitizerTextDropped, message, + /// remainingText }) so the client's error callback can show the user what got dropped + /// instead of just displaying raw text. + /// + protected static async Task BadRequestFromApiErrorAsync(HttpResponseMessage response) + { + string rawError = await ReadErrorMessageAsync(response); + var sanitizerInfo = TryParseSanitizerTextDroppedError(rawError); + if (sanitizerInfo != null) + { + return new BadRequestObjectResult(new + { + sanitizerTextDropped = true, + message = sanitizerInfo.Message, + remainingText = sanitizerInfo.RemainingText + }); + } + return new BadRequestObjectResult(rawError); + } + protected async Task WithSecureClientAsync( Func> operation, IActionResult unauthorizedResult = null) diff --git a/GanjooRazor/Pages/Misc/Photos.cshtml b/GanjooRazor/Pages/Misc/Photos.cshtml index 27703ad4..a0d57c57 100644 --- a/GanjooRazor/Pages/Misc/Photos.cshtml +++ b/GanjooRazor/Pages/Misc/Photos.cshtml @@ -61,6 +61,7 @@ var form = $(this); var url = form.attr('action'); + var originalHtml = form.find('textarea').val(); $.ajax({ type: "POST", @@ -72,10 +73,18 @@ $(buttonSelector).prop("disabled", false); }, success: function(data) { - $(data).appendTo(parent1); + var $rendered = $(data); + var hadAnyError = $rendered.is('#comment-error'); + var hadSanitizerError = checkAppendedHtmlForSanitizerError($rendered, originalHtml); + + if (!hadSanitizerError) { + $rendered.appendTo(parent1); + } $(buttonSelector).text('پیشنهاد'); $(buttonSelector).prop("disabled", false); - form[0].reset(); + if (!hadAnyError) { + form[0].reset(); + } }, }); diff --git a/GanjooRazor/Pages/Misc/Photos.cshtml.cs b/GanjooRazor/Pages/Misc/Photos.cshtml.cs index c7107603..88149deb 100644 --- a/GanjooRazor/Pages/Misc/Photos.cshtml.cs +++ b/GanjooRazor/Pages/Misc/Photos.cshtml.cs @@ -113,7 +113,7 @@ namespace GanjooRazor.Pages return Page(); } - private IActionResult SpecLineErrorPartial(string error) + private IActionResult SpecLineErrorPartial(string error, string remainingText = null) { return Partial("_PoetSpecLinePartial", new _PoetSpecLinePartialModel() { @@ -121,7 +121,8 @@ namespace GanjooRazor.Pages { Id = 0, Contents = error - } + }, + SanitizerRemainingText = remainingText }); } @@ -155,7 +156,9 @@ namespace GanjooRazor.Pages }); } - return SpecLineErrorPartial(await ReadErrorMessageAsync(response)); + string rawError = await ReadErrorMessageAsync(response); + var sanitizerInfo = TryParseSanitizerTextDroppedError(rawError); + return SpecLineErrorPartial(sanitizerInfo != null ? sanitizerInfo.Message : rawError, sanitizerInfo?.RemainingText); }, SpecLineErrorPartial(NotLoggedInMessage)); } diff --git a/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml b/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml index b5e85ca7..b7519eb4 100644 --- a/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml +++ b/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml @@ -3,8 +3,15 @@ Layout = null; } -
- @Html.Raw(Model.Line.Contents) +
+ @if (Model.Line.Id == 0) + { + @Model.Line.Contents + } + else + { + @Html.Raw(Model.Line.Contents) + }
@if (Model.Line.Id != 0 && !Model.Line.Published) diff --git a/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml.cs b/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml.cs index ddfaa106..5eac24a9 100644 --- a/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml.cs +++ b/GanjooRazor/Pages/Misc/_PoetSpecLinePartial.cshtml.cs @@ -7,5 +7,12 @@ namespace GanjooRazor.Pages { public bool ModeratePoetPhotos { get; set; } public GanjoorPoetSuggestedSpecLineViewModel Line { get; set; } + + /// + /// set (alongside Line.Contents holding the error message, when Line.Id == 0) when the + /// error is the "sanitizing had to drop real text" case - see + /// GanjooRazor.Pages._CommentPartialModel.SanitizerRemainingText for the same pattern. + /// + public string SanitizerRemainingText { get; set; } } } diff --git a/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml b/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml index 0ad67210..f44ed885 100644 --- a/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml +++ b/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml @@ -6,9 +6,9 @@ @if (Model.Comment == null) { -
+

خطا

- @Html.Raw(Model.Error) + @Model.Error
} else diff --git a/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml.cs b/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml.cs index 6e6d12fa..59339595 100644 --- a/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml.cs +++ b/GanjooRazor/Pages/Partials/GanjoorPage/_CommentPartial.cshtml.cs @@ -7,6 +7,13 @@ namespace GanjooRazor.Pages { public GanjoorCommentSummaryViewModel Comment { get; set; } public string Error { get; set; } + + /// + /// set (alongside Error) when Error is the "sanitizing had to drop real text" case - + /// the plain text that would remain, so client JS can diff it against what the user + /// actually typed and show them exactly what got dropped. Null/empty for any other error. + /// + public string SanitizerRemainingText { get; set; } public GanjoorCommentSummaryViewModel InReplyTo { get; set; } public bool LoggedIn { get; set; } public string DivSuffix { get; set; } @@ -15,7 +22,7 @@ namespace GanjooRazor.Pages { get { - return InReplyTo == null ? "äæÔÊå" : "�ÇÓÎ ÏÇÏå"; + return InReplyTo == null ? "نوشته" : "پاسخ داده"; } } public bool Bookmarked { get; set; } diff --git a/GanjooRazor/wwwroot/js/bk.js b/GanjooRazor/wwwroot/js/bk.js index 5385d046..b6dd09c9 100644 --- a/GanjooRazor/wwwroot/js/bk.js +++ b/GanjooRazor/wwwroot/js/bk.js @@ -1,4 +1,160 @@ -// From David Flanagan's "JavaScript: The Definitive Guide" 5th Ed, +// --- Sanitizer "text was dropped" feedback ---------------------------------------------- +// Shared by every place a TinyMCE-edited field (a comment, a bookmark note, a suggested +// poet spec-line, ...) gets rejected because the server's HTML sanitizer had to drop real +// text (see GanjoorService._BuildSanitizerDroppedTextError on the API side). Instead of just +// showing a generic error, this shows the user - highlighted, using the same diffChars utility +// already used elsewhere in this file for comparing correction blocks - exactly which part of +// what they typed would be dropped and why. That's almost always because they pasted the text +// in from somewhere else (Word, a chat app, a web page) that silently carried invalid/broken +// formatting along with it, so the message says that rather than talking about "tags" or the +// "<"/">" characters ordinary users don't recognize and have usually never typed themselves. + +function _htmlToPlainText(html) { + var tmp = document.createElement('div'); + tmp.innerHTML = html || ''; + return (tmp.textContent || tmp.innerText || '').replace(/\s+/g, ' ').trim(); +} + +function _ensureJsDiffLoaded(callback) { + if (window.JsDiff) { + callback(); + return; + } + var script = document.createElement('script'); + script.src = '/lib/diff.js'; + script.onload = callback; + document.head.appendChild(script); +} + +function showSanitizerTextDroppedPopup(message, originalHtml, remainingPlainText) { + _ensureJsDiffLoaded(function () { + var originalPlainText = _htmlToPlainText(originalHtml); + var diff = JsDiff.diffChars(originalPlainText, remainingPlainText || ''); + var fragment = document.createDocumentFragment(); + for (var i = 0; i < diff.length; i++) { + if (diff[i].added) { + continue; // sanitizing only removes content, nothing to highlight as "added" + } + var node; + if (diff[i].removed) { + node = document.createElement('del'); + node.style.color = 'red'; + node.appendChild(document.createTextNode(diff[i].value)); + } else { + node = document.createTextNode(diff[i].value); + } + fragment.appendChild(node); + } + + var existing = document.getElementById('sanitizer-warning-modal'); + if (existing) { + existing.parentNode.removeChild(existing); + } + + var isDark = (window.matchMedia("(prefers-color-scheme: dark)").matches && localStorage.getItem("scheme") != "light") || localStorage.getItem("scheme") == "dark"; + + var overlay = document.createElement('div'); + overlay.id = 'sanitizer-warning-modal'; + overlay.style.cssText = 'display:block;position:fixed;z-index:9999;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgba(0,0,0,0.5);'; + overlay.onclick = function (e) { + if (e.target === overlay) { + overlay.parentNode.removeChild(overlay); + } + }; + + var box = document.createElement('div'); + box.style.cssText = 'margin:8% auto;padding:20px;max-width:600px;border-radius:8px;direction:rtl;text-align:right;' + + (isDark ? 'background-color:#222;color:#eee;' : 'background-color:#fff;color:#000;'); + + var messageP = document.createElement('p'); + messageP.textContent = message; + box.appendChild(messageP); + + if (fragment.childNodes.length > 0) { + var diffTitle = document.createElement('p'); + var strong = document.createElement('strong'); + strong.textContent = 'بخشی که حذف خواهد شد با رنگ قرمز و خط‌خورده مشخص شده است:'; + diffTitle.appendChild(strong); + box.appendChild(diffTitle); + + var diffBox = document.createElement('div'); + diffBox.style.cssText = 'border:1px solid #999;padding:10px;border-radius:4px;margin-bottom:12px;white-space:pre-wrap;'; + diffBox.appendChild(fragment); + box.appendChild(diffBox); + } + + var closeBtn = document.createElement('button'); + closeBtn.type = 'button'; + closeBtn.textContent = 'بستن'; + closeBtn.onclick = function () { + overlay.parentNode.removeChild(overlay); + }; + box.appendChild(closeBtn); + + overlay.appendChild(box); + document.body.appendChild(overlay); + }); +} + +// Parses a raw error payload (jQuery's xhr.responseText, or the text of a server-rendered +// error block) looking for the sanitizer-text-dropped shape. Returns the parsed +// {message, remainingText} or null for any ordinary error (including plain, non-JSON text, +// which is the common case). +function _parseSanitizerDroppedTextError(raw) { + if (typeof raw !== 'string' || raw.length === 0) return null; + try { + var obj = JSON.parse(raw); + if (obj && typeof obj === 'object' && obj.sanitizerTextDropped === true) { + return obj; + } + } catch (e) { + // not JSON - an ordinary plain-text error, nothing to do here + } + return null; +} + +// For AJAX (BadRequestObjectResult) handlers: xhr is jQuery's jqXHR from an error callback. +// originalHtml is the HTML the user actually submitted (for the diff). Returns true if it +// showed the popup (nothing else to do), or false for an ordinary error (caller's normal +// error handling, if any, still applies). +function tryShowSanitizerErrorFromXhr(xhr, originalHtml) { + var parsed = _parseSanitizerDroppedTextError(xhr && xhr.responseText); + if (parsed == null) return false; + showSanitizerTextDroppedPopup(parsed.message, originalHtml, parsed.remainingText); + return true; +} + +// For server-rendered-partial handlers (postComment, postReplyComment, suggestNote): call this +// on the just-received (not yet necessarily inserted, though it's fine either way) root +// element of the rendered partial. If it carries the sanitizer-drop marker, this removes that +// error block from it (the popup below covers it, so leaving the same message sitting inline +// in the page too would just be clutter) and shows the popup. originalHtml is the HTML the user +// actually submitted (for the diff). Returns true if it handled a sanitizer-drop error. +// $rendered is the jQuery-wrapped set from $(data) for a just-received server-rendered +// partial (postComment, postReplyComment, suggestNote) - using jQuery's own filter/find here +// rather than indexing into the raw DOM nodes is what makes this robust to Razor's rendered +// HTML having a leading/trailing whitespace text node ahead of the actual element, which is +// common and would otherwise make a plain "first DOM node" check miss the real element. +// Returns true when the error div IS the partial's root element (true for every partial that +// currently marks it this way) - callers should skip inserting $rendered into the page at all +// in that case, since the popup this shows already covers the same message plus the diff. +function checkAppendedHtmlForSanitizerError($rendered, originalHtml) { + var $rootMatch = $rendered.filter('[data-sanitizer-remaining-text]'); + var $errorDiv = $rootMatch.length > 0 ? $rootMatch : $rendered.find('[data-sanitizer-remaining-text]'); + if ($errorDiv.length === 0) return false; + + var remainingText = $errorDiv.attr('data-sanitizer-remaining-text'); + if (!remainingText) return false; + + var message = $errorDiv.text().trim(); + if ($rootMatch.length === 0) { + $errorDiv.remove(); + } + showSanitizerTextDroppedPopup(message, originalHtml, remainingText); + return true; +} + +// From David Flanagan's "JavaScript: The Definitive Guide" 5th Ed, // http://www.davidflanagan.com/javascript5/display.php?n=15-4&f=15/04.js //modified 4 ganjoor a little bit @@ -812,13 +968,17 @@ function savePrivateNote() { var url = '?handler=BookmarkNote'; var bookmarkId = $("#editbookmarkId").val(); + var originalHtml = $("textarea#editNoteText").val(); $.ajax({ type: "PUT", url: url, data: { id: bookmarkId, - note: $("textarea#editNoteText").val() + note: originalHtml + }, + error: function (xhr) { + tryShowSanitizerErrorFromXhr(xhr, originalHtml); }, success: function () { document.getElementById('bookmark-note-dialog').style.display = 'none'; @@ -997,6 +1157,8 @@ function postComment(coupletIndex, buttonSelector) { var form = $(this); var url = form.attr('action'); + var originalHtml = form.find('textarea').val(); + $.ajax({ type: "POST", url: url, @@ -1006,12 +1168,20 @@ function postComment(coupletIndex, buttonSelector) { $(buttonSelector).prop("disabled", false); }, success: function (data) { - $(data).appendTo(parent1); - if (parent2 != null) - $(data).appendTo(parent2); + var $rendered = $(data); + var hadAnyError = $rendered.is('#comment-error'); + var hadSanitizerError = checkAppendedHtmlForSanitizerError($rendered, originalHtml); + + if (!hadSanitizerError) { + $rendered.appendTo(parent1); + if (parent2 != null) + $rendered.clone(true).appendTo(parent2); + } $(buttonSelector).text('درج حاشیه'); $(buttonSelector).prop("disabled", false); - form[0].reset(); + if (!hadAnyError) { + form[0].reset(); + } }, }); @@ -1043,6 +1213,7 @@ function postReplyComment() { var form = $(this); var url = form.attr('action'); + var originalHtml = form.find('textarea').val(); $.ajax({ type: "POST", @@ -1053,10 +1224,18 @@ function postReplyComment() { $('#replycomment').prop("disabled", false); }, success: function (data) { - document.getElementById('id03').style.display = 'none'; - $(data).appendTo(parent); + var $rendered = $(data); + var hadAnyError = $rendered.is('#comment-error'); + var hadSanitizerError = checkAppendedHtmlForSanitizerError($rendered, originalHtml); + + if (!hadAnyError) { + document.getElementById('id03').style.display = 'none'; + $("#replycommentform")[0].reset(); + } + if (!hadSanitizerError) { + $rendered.appendTo(parent); + } $('#replycomment').prop("disabled", false); - $("#replycommentform")[0].reset(); }, }); @@ -1157,14 +1336,17 @@ function editComment() { var coupletIndex = $("#editCommentCoupletIndex").val(); + var originalHtml = $("textarea#editCommentText").val(); + $.ajax({ type: "PUT", url: url, data: { id: commentId, - comment: $("textarea#editCommentText").val() + comment: originalHtml }, - error: function () { + error: function (xhr) { + tryShowSanitizerErrorFromXhr(xhr, originalHtml); $('#editcomment').text('ویرایش حاشیه'); $('#editcomment').prop("disabled", false); }, diff --git a/RMuseum/RMuseum.xml b/RMuseum/RMuseum.xml index f9ae3ca3..5e5fe707 100644 --- a/RMuseum/RMuseum.xml +++ b/RMuseum/RMuseum.xml @@ -21113,12 +21113,31 @@ or an actually-disallowed tag such as script/style whose whole subtree is removed) + + + shown to the user (via _BuildSanitizerDroppedTextError) when sanitizing had to drop + real text. Deliberately says nothing about "tags", "<" or ">" - ordinary users + don't know what those are and have usually never typed one themselves; almost always + this happens because they pasted the text in from somewhere else (Word, a chat app, a + web page) that silently carried invalid/broken formatting along with it. + + + + + builds the error string returned to the client when sanitizing dropped real text. + This is a JSON object encoded as a string (not a status-code/contract change) so it + still flows through every existing "the API error is just a display string" code path + unchanged, while GanjooRazor can additionally recognize and unpack it to show the + user exactly what got dropped (see SanitizerTextDroppedInfo on the GanjooRazor side) + + sanitizes comment HTML; TextWasDropped is true when the sanitizing process removed a meaningful chunk of the user's actual text (see _CommentSanitizationDroppedText) - callers should not silently save Html in that case, but ask the user to fix their - markup instead + markup instead (RemainingPlainText/_BuildSanitizerDroppedTextError let them show what + was dropped) diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Bookmarking.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Bookmarking.cs index 42441dab..61bc2ee4 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Bookmarking.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Bookmarking.cs @@ -220,7 +220,7 @@ namespace RMuseum.Services.Implementation var processedNote = await _ProcessCommentHtml(note, _context); if (processedNote.TextWasDropped) { - return new RServiceResult(false, "بخشی از متن یادداشت شما به دلیل داشتن نشانه‌های HTML نامعتبر یا ناقص (مثلاً علامت‌های «کوچکتر از» یا «بزرگتر از» به‌تنهایی، یا برچسبی که بسته نشده) هنگام پاک‌سازی حذف شد. لطفاً متن را بررسی و اصلاح کنید و دوباره ثبت نمایید."); + return new RServiceResult(false, _BuildSanitizerDroppedTextError(processedNote.RemainingPlainText)); } note = processedNote.Html; } diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs index 655f0dac..31b1dfff 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Maintenance.cs @@ -1,6 +1,7 @@ using AngleSharp.Html.Parser; using Ganss.Xss; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using RMuseum.DbContext; using RMuseum.Models.Ganjoor; using RSecurityBackend.Models.Generic; @@ -444,13 +445,41 @@ namespace RMuseum.Services.Implementation return sanitizedPlainText.Length < originalPlainText.Length * 0.95; } + /// + /// shown to the user (via _BuildSanitizerDroppedTextError) when sanitizing had to drop + /// real text. Deliberately says nothing about "tags", "<" or ">" - ordinary users + /// don't know what those are and have usually never typed one themselves; almost always + /// this happens because they pasted the text in from somewhere else (Word, a chat app, a + /// web page) that silently carried invalid/broken formatting along with it. + /// + private const string _sanitizerTextDroppedMessage = + "به‌نظر می‌رسد متنی که ارسال کرده‌اید از جای دیگری (مثلاً Word یا یک صفحهٔ وب) کپی و در اینجا پیست شده و قالب‌بندی پنهان و نامعتبری را با خود آورده است. به همین دلیل بخشی از متن هنگام پاک‌سازی حذف شد؛ بخش حذف‌شده در ادامه با رنگ قرمز و خط‌خورده به شما نشان داده می‌شود. لطفاً متن را در یک ویرایشگر متن ساده (مثل Notepad) پاک‌سازی کنید یا از نو تایپ کنید و سپس دوباره ارسال نمایید."; + + /// + /// builds the error string returned to the client when sanitizing dropped real text. + /// This is a JSON object encoded as a string (not a status-code/contract change) so it + /// still flows through every existing "the API error is just a display string" code path + /// unchanged, while GanjooRazor can additionally recognize and unpack it to show the + /// user exactly what got dropped (see SanitizerTextDroppedInfo on the GanjooRazor side) + /// + private static string _BuildSanitizerDroppedTextError(string remainingPlainText) + { + return JsonConvert.SerializeObject(new + { + sanitizerTextDropped = true, + message = _sanitizerTextDroppedMessage, + remainingText = remainingPlainText ?? "" + }); + } + /// /// sanitizes comment HTML; TextWasDropped is true when the sanitizing process removed /// a meaningful chunk of the user's actual text (see _CommentSanitizationDroppedText) - /// callers should not silently save Html in that case, but ask the user to fix their - /// markup instead + /// markup instead (RemainingPlainText/_BuildSanitizerDroppedTextError let them show what + /// was dropped) /// - private async Task<(string Html, bool TextWasDropped)> _ProcessCommentHtml(string commentText, RMuseumDbContext context) + private async Task<(string Html, bool TextWasDropped, string RemainingPlainText)> _ProcessCommentHtml(string commentText, RMuseumDbContext context) { string originalPlainText = _ExtractPlainText(commentText); @@ -496,12 +525,13 @@ namespace RMuseum.Services.Implementation // removed, before Linkify/internal-link processing below can itself change the // visible text (e.g. replacing a bare URL's text with a page title) in a way that // is not a loss. - bool textWasDropped = _CommentSanitizationDroppedText(originalPlainText, _ExtractPlainText(sanitizedHtml)); + string sanitizedPlainText = _ExtractPlainText(sanitizedHtml); + bool textWasDropped = _CommentSanitizationDroppedText(originalPlainText, sanitizedPlainText); // Process URLs (Linkify) and internal Ganjoor links sanitizedHtml = await _ProcessUrls(sanitizedHtml, context); - return (sanitizedHtml, textWasDropped); + return (sanitizedHtml, textWasDropped, sanitizedPlainText); } private async Task _ProcessUrls(string html, RMuseumDbContext context) diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PhotosProject.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PhotosProject.cs index 7ab8e24a..b2aeb0cf 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PhotosProject.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-PhotosProject.cs @@ -167,7 +167,7 @@ namespace RMuseum.Services.Implementation var processedContents = await _ProcessCommentHtml(model.Contents, _context); if (processedContents.TextWasDropped) { - return new RServiceResult(null, "بخشی از متن پیشنهادی شما به دلیل داشتن نشانه‌های HTML نامعتبر یا ناقص (مثلاً علامت‌های «کوچکتر از» یا «بزرگتر از» به‌تنهایی، یا برچسبی که بسته نشده) هنگام پاک‌سازی حذف شد. لطفاً متن را بررسی و اصلاح کنید و دوباره ارسال نمایید."); + return new RServiceResult(null, _BuildSanitizerDroppedTextError(processedContents.RemainingPlainText)); } model.Contents = processedContents.Html; @@ -223,7 +223,7 @@ namespace RMuseum.Services.Implementation var processedContents = await _ProcessCommentHtml(model.Contents, _context); if (processedContents.TextWasDropped) { - return new RServiceResult(false, "بخشی از متن به دلیل داشتن نشانه‌های HTML نامعتبر یا ناقص (مثلاً علامت‌های «کوچکتر از» یا «بزرگتر از» به‌تنهایی، یا برچسبی که بسته نشده) هنگام پاک‌سازی حذف شد. لطفاً متن را بررسی و اصلاح کنید و دوباره ثبت نمایید."); + return new RServiceResult(false, _BuildSanitizerDroppedTextError(processedContents.RemainingPlainText)); } model.Contents = processedContents.Html; diff --git a/RMuseum/Services/Implementation/GanjoorService.cs b/RMuseum/Services/Implementation/GanjoorService.cs index afd576c8..71614668 100644 --- a/RMuseum/Services/Implementation/GanjoorService.cs +++ b/RMuseum/Services/Implementation/GanjoorService.cs @@ -1044,7 +1044,7 @@ namespace RMuseum.Services.Implementation var processedComment = await _ProcessCommentHtml(content, _context); if (processedComment.TextWasDropped) { - return new RServiceResult(null, "بخشی از متن حاشیهٔ شما به دلیل داشتن نشانه‌های HTML نامعتبر یا ناقص (مثلاً علامت‌های «کوچکتر از» یا «بزرگتر از» به‌تنهایی، یا برچسبی که بسته نشده) هنگام پاک‌سازی حذف شد. لطفاً متن حاشیه را بررسی و اصلاح کنید و دوباره ارسال نمایید."); + return new RServiceResult(null, _BuildSanitizerDroppedTextError(processedComment.RemainingPlainText)); } content = processedComment.Html; @@ -1189,7 +1189,7 @@ namespace RMuseum.Services.Implementation var processedComment = await _ProcessCommentHtml(htmlComment, _context); if (processedComment.TextWasDropped) { - return new RServiceResult(false, "بخشی از متن حاشیهٔ شما به دلیل داشتن نشانه‌های HTML نامعتبر یا ناقص (مثلاً علامت‌های «کوچکتر از» یا «بزرگتر از» به‌تنهایی، یا برچسبی که بسته نشده) هنگام پاک‌سازی حذف شد. لطفاً متن حاشیه را بررسی و اصلاح کنید و دوباره ارسال نمایید."); + return new RServiceResult(false, _BuildSanitizerDroppedTextError(processedComment.RemainingPlainText)); } htmlComment = processedComment.Html;