@richscripts/richtexteditor 2.11.1 → 2.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/richtexteditor/all_plugins.js +158 -47
- package/richtexteditor/all_plugins.min.js +55 -55
- package/richtexteditor/plugins/aitoolkit.js +27 -4
- package/richtexteditor/plugins/all_plugins.js +158 -47
- package/richtexteditor/plugins/all_plugins.min.js +55 -55
- package/richtexteditor/plugins/insertgallery.js +108 -42
- package/richtexteditor/plugins/sanitizer.js +23 -1
- package/richtexteditor/rte-config.js +6 -0
- package/richtexteditor/rte.js +8 -2
|
@@ -2538,6 +2538,29 @@ function RTE_Plugin_AIToolkit() {
|
|
|
2538
2538
|
};
|
|
2539
2539
|
}
|
|
2540
2540
|
|
|
2541
|
+
// HTML that arrives from a ledger or a persisted document came from someone else - another
|
|
2542
|
+
// reviewer, a shared store, an AI endpoint that can be prompt-injected. It is written straight
|
|
2543
|
+
// into the document on accept/reject, so clean it here, at the boundary, instead of relying on
|
|
2544
|
+
// whatever runs against the live DOM later. Script vectors go through the content sanitizer when
|
|
2545
|
+
// it is loaded; position:fixed/sticky is dropped either way, because a full-viewport overlay from
|
|
2546
|
+
// remote content survived every other pass (UI redress, measured 2026-09-18).
|
|
2547
|
+
function cleanRemoteHtml(html) {
|
|
2548
|
+
if (!html) return "";
|
|
2549
|
+
html = String(html);
|
|
2550
|
+
if (typeof editor.sanitizeHtml === "function") html = editor.sanitizeHtml(html);
|
|
2551
|
+
var doc = new DOMParser().parseFromString("<body>" + html + "</body>", "text/html");
|
|
2552
|
+
var styled = doc.body.querySelectorAll("[style]");
|
|
2553
|
+
for (var i = 0; i < styled.length; i++) {
|
|
2554
|
+
var st = styled[i].style;
|
|
2555
|
+
if (/^(fixed|sticky)$/i.test(st.position)) {
|
|
2556
|
+
st.removeProperty("position");
|
|
2557
|
+
st.removeProperty("z-index");
|
|
2558
|
+
if (!styled[i].getAttribute("style")) styled[i].removeAttribute("style");
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return doc.body.innerHTML;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2541
2564
|
function normalizeLedgerEntry(raw) {
|
|
2542
2565
|
if (!raw || !raw.id) return null;
|
|
2543
2566
|
var changeType = raw.changeType || "ai-preview";
|
|
@@ -2555,10 +2578,10 @@ function RTE_Plugin_AIToolkit() {
|
|
|
2555
2578
|
name: author.name || author.id || "User",
|
|
2556
2579
|
color: author.color || "#2563eb"
|
|
2557
2580
|
},
|
|
2558
|
-
originalHtml: raw.originalHtml
|
|
2581
|
+
originalHtml: cleanRemoteHtml(raw.originalHtml),
|
|
2559
2582
|
originalText: raw.originalText || "",
|
|
2560
2583
|
resultText: raw.resultText || "",
|
|
2561
|
-
resultHtml: raw.resultHtml
|
|
2584
|
+
resultHtml: cleanRemoteHtml(raw.resultHtml),
|
|
2562
2585
|
reason: raw.reason || "",
|
|
2563
2586
|
suggestionType: raw.suggestionType || "",
|
|
2564
2587
|
language: raw.language || "",
|
|
@@ -2595,10 +2618,10 @@ function RTE_Plugin_AIToolkit() {
|
|
|
2595
2618
|
name: author.name || author.id || "User",
|
|
2596
2619
|
color: author.color || "#2563eb"
|
|
2597
2620
|
},
|
|
2598
|
-
originalHtml: raw.originalHtml || textToInlineHtml(raw.originalText || ""),
|
|
2621
|
+
originalHtml: cleanRemoteHtml(raw.originalHtml) || textToInlineHtml(raw.originalText || ""),
|
|
2599
2622
|
originalText: normalizeText(raw.originalText || ""),
|
|
2600
2623
|
resultText: normalizeText(raw.resultText || ""),
|
|
2601
|
-
resultHtml: raw.resultHtml || textToInlineHtml(raw.resultText || ""),
|
|
2624
|
+
resultHtml: cleanRemoteHtml(raw.resultHtml) || textToInlineHtml(raw.resultText || ""),
|
|
2602
2625
|
reason: normalizeText(raw.reason || ""),
|
|
2603
2626
|
suggestionType: getSuggestionTypeValue(raw.suggestionType || ""),
|
|
2604
2627
|
language: raw.language || "",
|
|
@@ -3748,6 +3748,29 @@ function RTE_Plugin_AIToolkit() {
|
|
|
3748
3748
|
};
|
|
3749
3749
|
}
|
|
3750
3750
|
|
|
3751
|
+
// HTML that arrives from a ledger or a persisted document came from someone else - another
|
|
3752
|
+
// reviewer, a shared store, an AI endpoint that can be prompt-injected. It is written straight
|
|
3753
|
+
// into the document on accept/reject, so clean it here, at the boundary, instead of relying on
|
|
3754
|
+
// whatever runs against the live DOM later. Script vectors go through the content sanitizer when
|
|
3755
|
+
// it is loaded; position:fixed/sticky is dropped either way, because a full-viewport overlay from
|
|
3756
|
+
// remote content survived every other pass (UI redress, measured 2026-09-18).
|
|
3757
|
+
function cleanRemoteHtml(html) {
|
|
3758
|
+
if (!html) return "";
|
|
3759
|
+
html = String(html);
|
|
3760
|
+
if (typeof editor.sanitizeHtml === "function") html = editor.sanitizeHtml(html);
|
|
3761
|
+
var doc = new DOMParser().parseFromString("<body>" + html + "</body>", "text/html");
|
|
3762
|
+
var styled = doc.body.querySelectorAll("[style]");
|
|
3763
|
+
for (var i = 0; i < styled.length; i++) {
|
|
3764
|
+
var st = styled[i].style;
|
|
3765
|
+
if (/^(fixed|sticky)$/i.test(st.position)) {
|
|
3766
|
+
st.removeProperty("position");
|
|
3767
|
+
st.removeProperty("z-index");
|
|
3768
|
+
if (!styled[i].getAttribute("style")) styled[i].removeAttribute("style");
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
return doc.body.innerHTML;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3751
3774
|
function normalizeLedgerEntry(raw) {
|
|
3752
3775
|
if (!raw || !raw.id) return null;
|
|
3753
3776
|
var changeType = raw.changeType || "ai-preview";
|
|
@@ -3765,10 +3788,10 @@ function RTE_Plugin_AIToolkit() {
|
|
|
3765
3788
|
name: author.name || author.id || "User",
|
|
3766
3789
|
color: author.color || "#2563eb"
|
|
3767
3790
|
},
|
|
3768
|
-
originalHtml: raw.originalHtml
|
|
3791
|
+
originalHtml: cleanRemoteHtml(raw.originalHtml),
|
|
3769
3792
|
originalText: raw.originalText || "",
|
|
3770
3793
|
resultText: raw.resultText || "",
|
|
3771
|
-
resultHtml: raw.resultHtml
|
|
3794
|
+
resultHtml: cleanRemoteHtml(raw.resultHtml),
|
|
3772
3795
|
reason: raw.reason || "",
|
|
3773
3796
|
suggestionType: raw.suggestionType || "",
|
|
3774
3797
|
language: raw.language || "",
|
|
@@ -3805,10 +3828,10 @@ function RTE_Plugin_AIToolkit() {
|
|
|
3805
3828
|
name: author.name || author.id || "User",
|
|
3806
3829
|
color: author.color || "#2563eb"
|
|
3807
3830
|
},
|
|
3808
|
-
originalHtml: raw.originalHtml || textToInlineHtml(raw.originalText || ""),
|
|
3831
|
+
originalHtml: cleanRemoteHtml(raw.originalHtml) || textToInlineHtml(raw.originalText || ""),
|
|
3809
3832
|
originalText: normalizeText(raw.originalText || ""),
|
|
3810
3833
|
resultText: normalizeText(raw.resultText || ""),
|
|
3811
|
-
resultHtml: raw.resultHtml || textToInlineHtml(raw.resultText || ""),
|
|
3834
|
+
resultHtml: cleanRemoteHtml(raw.resultHtml) || textToInlineHtml(raw.resultText || ""),
|
|
3812
3835
|
reason: normalizeText(raw.reason || ""),
|
|
3813
3836
|
suggestionType: getSuggestionTypeValue(raw.suggestionType || ""),
|
|
3814
3837
|
language: raw.language || "",
|
|
@@ -30812,10 +30835,12 @@ function RTE_Plugin_InsertEmoji() {
|
|
|
30812
30835
|
//
|
|
30813
30836
|
// config.galleryEndpoint URL returning { currentFolder, currentFolderDisplay,
|
|
30814
30837
|
// parentFolder, folders: [{folder,name}],
|
|
30815
|
-
// images: [{url,name,folder,size,source}] }.
|
|
30838
|
+
// images: [{url,name,folder,size,source,alt}] }.
|
|
30816
30839
|
// Absent -> local mode: presets only, no request.
|
|
30817
30840
|
// config.galleryImages Preset images. String, [url, text], or an object
|
|
30818
|
-
// with url/src/href plus optional thumbnail/name/meta.
|
|
30841
|
+
// with url/src/href plus optional thumbnail/name/meta/alt.
|
|
30842
|
+
// alt (presets and server images) becomes the inserted
|
|
30843
|
+
// image's alt text; without it the attribute stays absent.
|
|
30819
30844
|
//
|
|
30820
30845
|
// Uploads use window.richTextBoxUploadFile when present (it accepts a target
|
|
30821
30846
|
// folder), otherwise the generic window.rte_file_upload_handler.
|
|
@@ -30946,7 +30971,8 @@ function RTE_Plugin_InsertGallery() {
|
|
|
30946
30971
|
url: url,
|
|
30947
30972
|
thumbnail: item.thumbnail || item.thumb || item.preview || url,
|
|
30948
30973
|
name: item.name || item.text || item.title || getFileName(url),
|
|
30949
|
-
meta: item.meta || item.description || item.alt || ""
|
|
30974
|
+
meta: item.meta || item.description || item.alt || "",
|
|
30975
|
+
alt: typeof item.alt === "string" ? item.alt : undefined
|
|
30950
30976
|
};
|
|
30951
30977
|
}
|
|
30952
30978
|
|
|
@@ -30986,6 +31012,7 @@ function RTE_Plugin_InsertGallery() {
|
|
|
30986
31012
|
folder: item.folder || "",
|
|
30987
31013
|
source: item.source || "upload",
|
|
30988
31014
|
size: item.size || 0,
|
|
31015
|
+
alt: typeof item.alt === "string" ? item.alt : undefined,
|
|
30989
31016
|
searchText: (name + " " + item.url).toLowerCase()
|
|
30990
31017
|
};
|
|
30991
31018
|
}
|
|
@@ -31179,7 +31206,9 @@ function RTE_Plugin_InsertGallery() {
|
|
|
31179
31206
|
return;
|
|
31180
31207
|
}
|
|
31181
31208
|
|
|
31182
|
-
|
|
31209
|
+
// Only an alt the host supplied (preset or server listing). The file name is
|
|
31210
|
+
// not alt text; leaving it absent lets the accessibility checker flag it.
|
|
31211
|
+
editor.insertImageByUrl(selected.url, selected.alt);
|
|
31183
31212
|
closeDialog();
|
|
31184
31213
|
editor.focus();
|
|
31185
31214
|
}
|
|
@@ -31410,6 +31439,10 @@ function RTE_Plugin_InsertGallery() {
|
|
|
31410
31439
|
render();
|
|
31411
31440
|
}
|
|
31412
31441
|
|
|
31442
|
+
// Upload failures to show once the folder has been re-listed (applyResponse
|
|
31443
|
+
// clears the error, so it would otherwise vanish).
|
|
31444
|
+
var pendingError = "";
|
|
31445
|
+
|
|
31413
31446
|
function loadFolder(folder, selectAfterLoad) {
|
|
31414
31447
|
if (!serverMode) {
|
|
31415
31448
|
state.images = presetImages.slice(0);
|
|
@@ -31423,6 +31456,7 @@ function RTE_Plugin_InsertGallery() {
|
|
|
31423
31456
|
|
|
31424
31457
|
requestJson("GET", withFolder(endpoint, folder), null, function (payload, errorCode) {
|
|
31425
31458
|
if (errorCode) {
|
|
31459
|
+
pendingError = "";
|
|
31426
31460
|
loadFallback(errorCode);
|
|
31427
31461
|
return;
|
|
31428
31462
|
}
|
|
@@ -31431,6 +31465,8 @@ function RTE_Plugin_InsertGallery() {
|
|
|
31431
31465
|
if (selectAfterLoad) {
|
|
31432
31466
|
state.selectedUrl = selectAfterLoad;
|
|
31433
31467
|
}
|
|
31468
|
+
state.error = pendingError;
|
|
31469
|
+
pendingError = "";
|
|
31434
31470
|
state.loading = false;
|
|
31435
31471
|
render();
|
|
31436
31472
|
});
|
|
@@ -31465,67 +31501,120 @@ function RTE_Plugin_InsertGallery() {
|
|
|
31465
31501
|
});
|
|
31466
31502
|
}
|
|
31467
31503
|
|
|
31504
|
+
// config.maxUploadFileSize is enforced by the core only on its own insert
|
|
31505
|
+
// paths. The gallery calls the upload handler directly, so the limit a
|
|
31506
|
+
// customer set to protect their database was skipped for every image
|
|
31507
|
+
// uploaded through this dialog. The accept list is only a hint to the file
|
|
31508
|
+
// picker ("All files" bypasses it), so the format is checked here too.
|
|
31509
|
+
function rejectReason(file) {
|
|
31510
|
+
var name = String(file.name || "").toLowerCase();
|
|
31511
|
+
var dot = name.lastIndexOf(".");
|
|
31512
|
+
if (dot < 0 || GALLERY_ACCEPT.indexOf(name.substring(dot)) < 0) {
|
|
31513
|
+
return file.name + " is not an accepted image format.";
|
|
31514
|
+
}
|
|
31515
|
+
var max = parseInt(config.maxUploadFileSize, 10);
|
|
31516
|
+
if (max > 0 && file.size > max) {
|
|
31517
|
+
return file.name + " is " + humanSize(file.size) + ". The maximum allowed size is " + humanSize(max) + ".";
|
|
31518
|
+
}
|
|
31519
|
+
return "";
|
|
31520
|
+
}
|
|
31521
|
+
|
|
31468
31522
|
function uploadFiles(fileList) {
|
|
31469
31523
|
var files = [];
|
|
31524
|
+
var problems = [];
|
|
31470
31525
|
var index;
|
|
31471
31526
|
for (index = 0; index < fileList.length; index++) {
|
|
31472
|
-
|
|
31527
|
+
var reason = rejectReason(fileList[index]);
|
|
31528
|
+
if (reason) {
|
|
31529
|
+
problems.push(reason);
|
|
31530
|
+
} else {
|
|
31531
|
+
files.push(fileList[index]);
|
|
31532
|
+
}
|
|
31473
31533
|
}
|
|
31474
31534
|
|
|
31475
|
-
if (!
|
|
31535
|
+
if (!canUpload) {
|
|
31536
|
+
return;
|
|
31537
|
+
}
|
|
31538
|
+
if (!files.length) {
|
|
31539
|
+
if (problems.length) {
|
|
31540
|
+
state.error = problems.join(" ");
|
|
31541
|
+
render();
|
|
31542
|
+
}
|
|
31476
31543
|
return;
|
|
31477
31544
|
}
|
|
31478
31545
|
|
|
31479
|
-
|
|
31546
|
+
// One failed file used to stop the batch, and in local mode only the
|
|
31547
|
+
// LAST success was added to the list - uploading three images showed
|
|
31548
|
+
// one. Now every file is attempted, every success is kept, and the
|
|
31549
|
+
// failures are reported together at the end.
|
|
31550
|
+
var uploadedUrls = [];
|
|
31480
31551
|
|
|
31481
|
-
function
|
|
31482
|
-
|
|
31483
|
-
if (serverMode) {
|
|
31484
|
-
loadFolder(state.currentFolder, lastUploadedUrl);
|
|
31485
|
-
return;
|
|
31486
|
-
}
|
|
31552
|
+
function finish() {
|
|
31553
|
+
var lastUploadedUrl = uploadedUrls.length ? uploadedUrls[uploadedUrls.length - 1] : "";
|
|
31487
31554
|
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
31495
|
-
|
|
31496
|
-
|
|
31497
|
-
|
|
31555
|
+
if (serverMode) {
|
|
31556
|
+
pendingError = problems.join(" ");
|
|
31557
|
+
loadFolder(state.currentFolder, lastUploadedUrl || state.selectedUrl);
|
|
31558
|
+
return;
|
|
31559
|
+
}
|
|
31560
|
+
|
|
31561
|
+
// No server to re-list from, so fold the uploads into the preset
|
|
31562
|
+
// list directly, newest first, and select the last one.
|
|
31563
|
+
for (var u = 0; u < uploadedUrls.length; u++) {
|
|
31564
|
+
var uploaded = normalizePreset(uploadedUrls[u]);
|
|
31565
|
+
if (uploaded) {
|
|
31566
|
+
uploaded.source = "upload";
|
|
31567
|
+
presetImages.unshift(uploaded);
|
|
31568
|
+
config.galleryImages.unshift(uploadedUrls[u]);
|
|
31498
31569
|
}
|
|
31499
|
-
|
|
31500
|
-
|
|
31501
|
-
|
|
31570
|
+
}
|
|
31571
|
+
if (lastUploadedUrl) {
|
|
31572
|
+
state.selectedUrl = lastUploadedUrl;
|
|
31573
|
+
}
|
|
31574
|
+
state.images = presetImages.slice(0);
|
|
31575
|
+
state.loading = false;
|
|
31576
|
+
state.error = problems.join(" ");
|
|
31577
|
+
render();
|
|
31578
|
+
}
|
|
31579
|
+
|
|
31580
|
+
function uploadNext(nextIndex) {
|
|
31581
|
+
if (nextIndex >= files.length) {
|
|
31582
|
+
finish();
|
|
31502
31583
|
return;
|
|
31503
31584
|
}
|
|
31504
31585
|
|
|
31505
31586
|
var file = files[nextIndex];
|
|
31587
|
+
var settled = false;
|
|
31506
31588
|
|
|
31507
|
-
function done(url,
|
|
31508
|
-
|
|
31509
|
-
|
|
31510
|
-
state.loading = false;
|
|
31511
|
-
render();
|
|
31589
|
+
function done(url, errorText) {
|
|
31590
|
+
// a handler that calls back twice must not upload the rest twice
|
|
31591
|
+
if (settled) {
|
|
31512
31592
|
return;
|
|
31513
31593
|
}
|
|
31514
|
-
|
|
31515
|
-
|
|
31594
|
+
settled = true;
|
|
31595
|
+
if (url) {
|
|
31596
|
+
uploadedUrls.push(url);
|
|
31597
|
+
} else {
|
|
31598
|
+
problems.push(typeof errorText === "string" && errorText ? errorText : ("Upload failed for " + file.name + "."));
|
|
31599
|
+
}
|
|
31516
31600
|
uploadNext(nextIndex + 1);
|
|
31517
31601
|
}
|
|
31518
31602
|
|
|
31519
|
-
|
|
31520
|
-
|
|
31521
|
-
|
|
31522
|
-
|
|
31523
|
-
|
|
31524
|
-
|
|
31603
|
+
try {
|
|
31604
|
+
if (uploadToFolder) {
|
|
31605
|
+
window.richTextBoxUploadFile(file, function (url, errorCode) {
|
|
31606
|
+
done(errorCode ? "" : url, errorCode ? ("Upload failed for " + file.name + ".") : "");
|
|
31607
|
+
}, { folder: state.currentFolder }, nextIndex, files);
|
|
31608
|
+
return;
|
|
31609
|
+
}
|
|
31525
31610
|
|
|
31526
|
-
|
|
31527
|
-
|
|
31528
|
-
|
|
31611
|
+
window.rte_file_upload_handler(file, function (url, error) {
|
|
31612
|
+
done(url, error);
|
|
31613
|
+
}, nextIndex, files);
|
|
31614
|
+
} catch (ex) {
|
|
31615
|
+
// a handler that throws must not leave the dialog stuck on "Loading"
|
|
31616
|
+
done("", "Upload failed for " + file.name + ".");
|
|
31617
|
+
}
|
|
31529
31618
|
}
|
|
31530
31619
|
|
|
31531
31620
|
state.loading = true;
|
|
@@ -39806,13 +39895,35 @@ function RTE_Plugin_Sanitizer() {
|
|
|
39806
39895
|
// sanitizerAllowedIframeHosts is still the stronger control.
|
|
39807
39896
|
var DEFAULT_IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-presentation";
|
|
39808
39897
|
|
|
39898
|
+
// 2026-09-21 Two holes in the default sandbox, both closed here:
|
|
39899
|
+
// - it was only ADDED when the iframe had none, so injected markup that
|
|
39900
|
+
// brought its own sandbox="allow-top-navigation allow-popups allow-forms"
|
|
39901
|
+
// kept every permission the comment above says is withheld. An existing
|
|
39902
|
+
// sandbox is now narrowed to the default set, never trusted.
|
|
39903
|
+
// - allow-scripts + allow-same-origin isolates nothing when the frame is
|
|
39904
|
+
// served from the page's OWN origin: the framed page can reach the parent
|
|
39905
|
+
// and remove its own sandbox. Same-origin frames lose allow-same-origin.
|
|
39906
|
+
function applyDefaultSandbox(el, src) {
|
|
39907
|
+
var allowed = DEFAULT_IFRAME_SANDBOX.split(" ");
|
|
39908
|
+
var sameOrigin = false;
|
|
39909
|
+
try { sameOrigin = new URL(src, location.href).origin === location.origin; } catch (e) {}
|
|
39910
|
+
if (sameOrigin) allowed = allowed.filter(function (t) { return t !== "allow-same-origin"; });
|
|
39911
|
+
if (el.hasAttribute("sandbox")) {
|
|
39912
|
+
// keep only what the frame asked for AND the default permits; an empty
|
|
39913
|
+
// sandbox (the strictest) stays empty
|
|
39914
|
+
var asked = (el.getAttribute("sandbox") || "").toLowerCase().split(/\s+/);
|
|
39915
|
+
allowed = allowed.filter(function (t) { return asked.indexOf(t) >= 0; });
|
|
39916
|
+
}
|
|
39917
|
+
el.setAttribute("sandbox", allowed.join(" "));
|
|
39918
|
+
}
|
|
39919
|
+
|
|
39809
39920
|
function iframeAllowed(el) {
|
|
39810
39921
|
if (config.sanitizerAllowIframes === false) return false;
|
|
39811
39922
|
var src = el.getAttribute("src") || "";
|
|
39812
39923
|
if (!/^https?:/i.test(src)) return false;
|
|
39813
39924
|
var hosts = config.sanitizerAllowedIframeHosts;
|
|
39814
39925
|
if (!hosts || !hosts.length) {
|
|
39815
|
-
|
|
39926
|
+
applyDefaultSandbox(el, src);
|
|
39816
39927
|
return true;
|
|
39817
39928
|
}
|
|
39818
39929
|
try {
|