@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.
@@ -14,10 +14,12 @@
14
14
  //
15
15
  // config.galleryEndpoint URL returning { currentFolder, currentFolderDisplay,
16
16
  // parentFolder, folders: [{folder,name}],
17
- // images: [{url,name,folder,size,source}] }.
17
+ // images: [{url,name,folder,size,source,alt}] }.
18
18
  // Absent -> local mode: presets only, no request.
19
19
  // config.galleryImages Preset images. String, [url, text], or an object
20
- // with url/src/href plus optional thumbnail/name/meta.
20
+ // with url/src/href plus optional thumbnail/name/meta/alt.
21
+ // alt (presets and server images) becomes the inserted
22
+ // image's alt text; without it the attribute stays absent.
21
23
  //
22
24
  // Uploads use window.richTextBoxUploadFile when present (it accepts a target
23
25
  // folder), otherwise the generic window.rte_file_upload_handler.
@@ -148,7 +150,8 @@ function RTE_Plugin_InsertGallery() {
148
150
  url: url,
149
151
  thumbnail: item.thumbnail || item.thumb || item.preview || url,
150
152
  name: item.name || item.text || item.title || getFileName(url),
151
- meta: item.meta || item.description || item.alt || ""
153
+ meta: item.meta || item.description || item.alt || "",
154
+ alt: typeof item.alt === "string" ? item.alt : undefined
152
155
  };
153
156
  }
154
157
 
@@ -188,6 +191,7 @@ function RTE_Plugin_InsertGallery() {
188
191
  folder: item.folder || "",
189
192
  source: item.source || "upload",
190
193
  size: item.size || 0,
194
+ alt: typeof item.alt === "string" ? item.alt : undefined,
191
195
  searchText: (name + " " + item.url).toLowerCase()
192
196
  };
193
197
  }
@@ -381,7 +385,9 @@ function RTE_Plugin_InsertGallery() {
381
385
  return;
382
386
  }
383
387
 
384
- editor.insertImageByUrl(selected.url);
388
+ // Only an alt the host supplied (preset or server listing). The file name is
389
+ // not alt text; leaving it absent lets the accessibility checker flag it.
390
+ editor.insertImageByUrl(selected.url, selected.alt);
385
391
  closeDialog();
386
392
  editor.focus();
387
393
  }
@@ -612,6 +618,10 @@ function RTE_Plugin_InsertGallery() {
612
618
  render();
613
619
  }
614
620
 
621
+ // Upload failures to show once the folder has been re-listed (applyResponse
622
+ // clears the error, so it would otherwise vanish).
623
+ var pendingError = "";
624
+
615
625
  function loadFolder(folder, selectAfterLoad) {
616
626
  if (!serverMode) {
617
627
  state.images = presetImages.slice(0);
@@ -625,6 +635,7 @@ function RTE_Plugin_InsertGallery() {
625
635
 
626
636
  requestJson("GET", withFolder(endpoint, folder), null, function (payload, errorCode) {
627
637
  if (errorCode) {
638
+ pendingError = "";
628
639
  loadFallback(errorCode);
629
640
  return;
630
641
  }
@@ -633,6 +644,8 @@ function RTE_Plugin_InsertGallery() {
633
644
  if (selectAfterLoad) {
634
645
  state.selectedUrl = selectAfterLoad;
635
646
  }
647
+ state.error = pendingError;
648
+ pendingError = "";
636
649
  state.loading = false;
637
650
  render();
638
651
  });
@@ -667,67 +680,120 @@ function RTE_Plugin_InsertGallery() {
667
680
  });
668
681
  }
669
682
 
683
+ // config.maxUploadFileSize is enforced by the core only on its own insert
684
+ // paths. The gallery calls the upload handler directly, so the limit a
685
+ // customer set to protect their database was skipped for every image
686
+ // uploaded through this dialog. The accept list is only a hint to the file
687
+ // picker ("All files" bypasses it), so the format is checked here too.
688
+ function rejectReason(file) {
689
+ var name = String(file.name || "").toLowerCase();
690
+ var dot = name.lastIndexOf(".");
691
+ if (dot < 0 || GALLERY_ACCEPT.indexOf(name.substring(dot)) < 0) {
692
+ return file.name + " is not an accepted image format.";
693
+ }
694
+ var max = parseInt(config.maxUploadFileSize, 10);
695
+ if (max > 0 && file.size > max) {
696
+ return file.name + " is " + humanSize(file.size) + ". The maximum allowed size is " + humanSize(max) + ".";
697
+ }
698
+ return "";
699
+ }
700
+
670
701
  function uploadFiles(fileList) {
671
702
  var files = [];
703
+ var problems = [];
672
704
  var index;
673
705
  for (index = 0; index < fileList.length; index++) {
674
- files.push(fileList[index]);
706
+ var reason = rejectReason(fileList[index]);
707
+ if (reason) {
708
+ problems.push(reason);
709
+ } else {
710
+ files.push(fileList[index]);
711
+ }
675
712
  }
676
713
 
677
- if (!files.length || !canUpload) {
714
+ if (!canUpload) {
715
+ return;
716
+ }
717
+ if (!files.length) {
718
+ if (problems.length) {
719
+ state.error = problems.join(" ");
720
+ render();
721
+ }
678
722
  return;
679
723
  }
680
724
 
681
- var lastUploadedUrl = "";
725
+ // One failed file used to stop the batch, and in local mode only the
726
+ // LAST success was added to the list - uploading three images showed
727
+ // one. Now every file is attempted, every success is kept, and the
728
+ // failures are reported together at the end.
729
+ var uploadedUrls = [];
682
730
 
683
- function uploadNext(nextIndex) {
684
- if (nextIndex >= files.length) {
685
- if (serverMode) {
686
- loadFolder(state.currentFolder, lastUploadedUrl);
687
- return;
688
- }
731
+ function finish() {
732
+ var lastUploadedUrl = uploadedUrls.length ? uploadedUrls[uploadedUrls.length - 1] : "";
733
+
734
+ if (serverMode) {
735
+ pendingError = problems.join(" ");
736
+ loadFolder(state.currentFolder, lastUploadedUrl || state.selectedUrl);
737
+ return;
738
+ }
689
739
 
690
- // No server to re-list from, so fold the upload into the
691
- // preset list directly and keep it selected.
692
- if (lastUploadedUrl) {
693
- var uploaded = normalizePreset(lastUploadedUrl);
694
- if (uploaded) {
695
- uploaded.source = "upload";
696
- presetImages.unshift(uploaded);
697
- config.galleryImages.unshift(lastUploadedUrl);
698
- }
699
- state.selectedUrl = lastUploadedUrl;
740
+ // No server to re-list from, so fold the uploads into the preset
741
+ // list directly, newest first, and select the last one.
742
+ for (var u = 0; u < uploadedUrls.length; u++) {
743
+ var uploaded = normalizePreset(uploadedUrls[u]);
744
+ if (uploaded) {
745
+ uploaded.source = "upload";
746
+ presetImages.unshift(uploaded);
747
+ config.galleryImages.unshift(uploadedUrls[u]);
700
748
  }
701
- state.images = presetImages.slice(0);
702
- state.loading = false;
703
- render();
749
+ }
750
+ if (lastUploadedUrl) {
751
+ state.selectedUrl = lastUploadedUrl;
752
+ }
753
+ state.images = presetImages.slice(0);
754
+ state.loading = false;
755
+ state.error = problems.join(" ");
756
+ render();
757
+ }
758
+
759
+ function uploadNext(nextIndex) {
760
+ if (nextIndex >= files.length) {
761
+ finish();
704
762
  return;
705
763
  }
706
764
 
707
765
  var file = files[nextIndex];
766
+ var settled = false;
708
767
 
709
- function done(url, errorCode) {
710
- if (!url) {
711
- state.error = errorCode || ("Upload failed for " + file.name + ".");
712
- state.loading = false;
713
- render();
768
+ function done(url, errorText) {
769
+ // a handler that calls back twice must not upload the rest twice
770
+ if (settled) {
714
771
  return;
715
772
  }
716
-
717
- lastUploadedUrl = url;
773
+ settled = true;
774
+ if (url) {
775
+ uploadedUrls.push(url);
776
+ } else {
777
+ problems.push(typeof errorText === "string" && errorText ? errorText : ("Upload failed for " + file.name + "."));
778
+ }
718
779
  uploadNext(nextIndex + 1);
719
780
  }
720
781
 
721
- if (uploadToFolder) {
722
- window.richTextBoxUploadFile(file, function (url, errorCode) {
723
- done(errorCode ? "" : url, errorCode ? ("Upload failed for " + file.name + ".") : "");
724
- }, { folder: state.currentFolder }, nextIndex, files);
725
- return;
726
- }
782
+ try {
783
+ if (uploadToFolder) {
784
+ window.richTextBoxUploadFile(file, function (url, errorCode) {
785
+ done(errorCode ? "" : url, errorCode ? ("Upload failed for " + file.name + ".") : "");
786
+ }, { folder: state.currentFolder }, nextIndex, files);
787
+ return;
788
+ }
727
789
 
728
- window.rte_file_upload_handler(file, function (url, error) {
729
- done(url, error);
730
- }, nextIndex, files);
790
+ window.rte_file_upload_handler(file, function (url, error) {
791
+ done(url, error);
792
+ }, nextIndex, files);
793
+ } catch (ex) {
794
+ // a handler that throws must not leave the dialog stuck on "Loading"
795
+ done("", "Upload failed for " + file.name + ".");
796
+ }
731
797
  }
732
798
 
733
799
  state.loading = true;
@@ -203,13 +203,35 @@ function RTE_Plugin_Sanitizer() {
203
203
  // sanitizerAllowedIframeHosts is still the stronger control.
204
204
  var DEFAULT_IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-presentation";
205
205
 
206
+ // 2026-09-21 Two holes in the default sandbox, both closed here:
207
+ // - it was only ADDED when the iframe had none, so injected markup that
208
+ // brought its own sandbox="allow-top-navigation allow-popups allow-forms"
209
+ // kept every permission the comment above says is withheld. An existing
210
+ // sandbox is now narrowed to the default set, never trusted.
211
+ // - allow-scripts + allow-same-origin isolates nothing when the frame is
212
+ // served from the page's OWN origin: the framed page can reach the parent
213
+ // and remove its own sandbox. Same-origin frames lose allow-same-origin.
214
+ function applyDefaultSandbox(el, src) {
215
+ var allowed = DEFAULT_IFRAME_SANDBOX.split(" ");
216
+ var sameOrigin = false;
217
+ try { sameOrigin = new URL(src, location.href).origin === location.origin; } catch (e) {}
218
+ if (sameOrigin) allowed = allowed.filter(function (t) { return t !== "allow-same-origin"; });
219
+ if (el.hasAttribute("sandbox")) {
220
+ // keep only what the frame asked for AND the default permits; an empty
221
+ // sandbox (the strictest) stays empty
222
+ var asked = (el.getAttribute("sandbox") || "").toLowerCase().split(/\s+/);
223
+ allowed = allowed.filter(function (t) { return asked.indexOf(t) >= 0; });
224
+ }
225
+ el.setAttribute("sandbox", allowed.join(" "));
226
+ }
227
+
206
228
  function iframeAllowed(el) {
207
229
  if (config.sanitizerAllowIframes === false) return false;
208
230
  var src = el.getAttribute("src") || "";
209
231
  if (!/^https?:/i.test(src)) return false;
210
232
  var hosts = config.sanitizerAllowedIframeHosts;
211
233
  if (!hosts || !hosts.length) {
212
- if (!el.hasAttribute("sandbox")) el.setAttribute("sandbox", DEFAULT_IFRAME_SANDBOX);
234
+ applyDefaultSandbox(el, src);
213
235
  return true;
214
236
  }
215
237
  try {
@@ -68,6 +68,10 @@ RTE_DefaultConfig.inlineMath = true; // When true, editor.applyMathMarkup() wrap
68
68
  RTE_DefaultConfig.imageLazyLoad = true; // Newly-inserted <img> tags get loading="lazy" + decoding="async" so big documents stay responsive.
69
69
  RTE_DefaultConfig.documentUploadAccept = ".txt,.text,.rtf,.md,.markdown,.html,.htm,.pdf,.doc,.docx,.odt,.xls,.xlsx,.csv,.tsv,.json,.xml,.zip,.rar"; // Default Insert Document chooser filter.
70
70
  RTE_DefaultConfig.maxUploadFileSize = 0; // Largest file the editor will insert or upload, in BYTES. 0 = no limit. Applies to the image and document dialogs, paste and drag-and-drop. Over the limit the file is refused before it is read, so nothing is embedded or sent.
71
+ RTE_DefaultConfig.allowBase64Images = true; // Without file_upload_handler an inserted image is embedded in the HTML as base64. false = never embed; an upload handler is then required.
72
+ RTE_DefaultConfig.base64ImageMaxSize = 204800; // Largest base64 image, in BYTES, after shrinking (200 KB). Larger images are refused with a message. 0 = no cap (the behaviour before core 2.6.5).
73
+ RTE_DefaultConfig.base64ImageShrink = true; // Resize and re-encode photos in the browser before embedding them as base64. GIF and SVG are never re-encoded.
74
+ RTE_DefaultConfig.base64ImageMaxDimension = 1600; // Longest side, in pixels, of an image shrunk for base64 embedding.
71
75
  RTE_DefaultConfig.fileUploadLocalObjectUrl = true; // Without file_upload_handler, local document files insert as temporary blob: links instead of failing.
72
76
  // Reading-mode persistence key. Default is session-only so refresh restores the toolbar.
73
77
  // Set to true for per-editor localStorage persistence, or to a custom string to share state across editors.
@@ -936,6 +940,8 @@ RTE_DefaultConfig.text_inserttemplatetitle = "@inserttemplate";
936
940
  RTE_DefaultConfig.text_uploadfailed = "The file could not be uploaded."; // Shown when the upload handler reports an error; the error code follows in brackets.
937
941
  RTE_DefaultConfig.text_uploadnothandler = "This file cannot be attached, because file uploads are not set up on this site."; // Shown when no file_upload_handler is configured and fileUploadLocalObjectUrl is false.
938
942
  RTE_DefaultConfig.text_uploadtoobig = "This file is {0}. The maximum allowed size is {1}."; // {0} = the file size, {1} = maxUploadFileSize. Shown when a file is refused; handle the "uploadtoobig" customdialog hook for your own dialog.
943
+ RTE_DefaultConfig.text_base64toobig = "This image is {0} even after resizing. Images larger than {1} need an image upload handler - see the upload documentation."; // {0} = embedded size, {1} = base64ImageMaxSize. customdialog hook: "base64toobig".
944
+ RTE_DefaultConfig.text_base64disabled = "Images cannot be embedded in this editor. An image upload handler must be configured (file_upload_handler)."; // Shown when allowBase64Images is false and no handler is set. customdialog hook: "base64disabled".
939
945
  RTE_DefaultConfig.text_reachmaxlength = "The text to be added has reached the character limit for this field.";
940
946
 
941
947
  RTE_DefaultConfig.translation = RTE_DefaultConfig.translation || {