Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / editor / js / editor.es6.js
1 /**
2  * @file
3  * Attaches behavior for the Editor module.
4  */
5
6 (function ($, Drupal, drupalSettings) {
7   /**
8    * Finds the text area field associated with the given text format selector.
9    *
10    * @param {jQuery} $formatSelector
11    *   A text format selector DOM element.
12    *
13    * @return {HTMLElement}
14    *   The text area DOM element, if it was found.
15    */
16   function findFieldForFormatSelector($formatSelector) {
17     const field_id = $formatSelector.attr('data-editor-for');
18     // This selector will only find text areas in the top-level document. We do
19     // not support attaching editors on text areas within iframes.
20     return $(`#${field_id}`).get(0);
21   }
22
23   /**
24    * Changes the text editor on a text area.
25    *
26    * @param {HTMLElement} field
27    *   The text area DOM element.
28    * @param {string} newFormatID
29    *   The text format we're changing to; the text editor for the currently
30    *   active text format will be detached, and the text editor for the new text
31    *   format will be attached.
32    */
33   function changeTextEditor(field, newFormatID) {
34     const previousFormatID = field.getAttribute('data-editor-active-text-format');
35
36     // Detach the current editor (if any) and attach a new editor.
37     if (drupalSettings.editor.formats[previousFormatID]) {
38       Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]);
39     }
40     // When no text editor is currently active, stop tracking changes.
41     else {
42       $(field).off('.editor');
43     }
44
45     // Attach the new text editor (if any).
46     if (drupalSettings.editor.formats[newFormatID]) {
47       const format = drupalSettings.editor.formats[newFormatID];
48       filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach);
49     }
50
51     // Store the new active format.
52     field.setAttribute('data-editor-active-text-format', newFormatID);
53   }
54
55   /**
56    * Handles changes in text format.
57    *
58    * @param {jQuery.Event} event
59    *   The text format change event.
60    */
61   function onTextFormatChange(event) {
62     const $select = $(event.target);
63     const field = event.data.field;
64     const activeFormatID = field.getAttribute('data-editor-active-text-format');
65     const newFormatID = $select.val();
66
67     // Prevent double-attaching if the change event is triggered manually.
68     if (newFormatID === activeFormatID) {
69       return;
70     }
71
72     // When changing to a text format that has a text editor associated
73     // with it that supports content filtering, then first ask for
74     // confirmation, because switching text formats might cause certain
75     // markup to be stripped away.
76     const supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
77     // If there is no content yet, it's always safe to change the text format.
78     const hasContent = field.value !== '';
79     if (hasContent && supportContentFiltering) {
80       const message = Drupal.t('Changing the text format to %text_format will permanently remove content that is not allowed in that text format.<br><br>Save your changes before switching the text format to avoid losing data.', {
81         '%text_format': $select.find('option:selected').text(),
82       });
83       var confirmationDialog = Drupal.dialog(`<div>${message}</div>`, {
84         title: Drupal.t('Change text format?'),
85         dialogClass: 'editor-change-text-format-modal',
86         resizable: false,
87         buttons: [
88           {
89             text: Drupal.t('Continue'),
90             class: 'button button--primary',
91             click() {
92               changeTextEditor(field, newFormatID);
93               confirmationDialog.close();
94             },
95           },
96           {
97             text: Drupal.t('Cancel'),
98             class: 'button',
99             click() {
100               // Restore the active format ID: cancel changing text format. We
101               // cannot simply call event.preventDefault() because jQuery's
102               // change event is only triggered after the change has already
103               // been accepted.
104               $select.val(activeFormatID);
105               confirmationDialog.close();
106             },
107           },
108         ],
109         // Prevent this modal from being closed without the user making a choice
110         // as per http://stackoverflow.com/a/5438771.
111         closeOnEscape: false,
112         create() {
113           $(this).parent().find('.ui-dialog-titlebar-close').remove();
114         },
115         beforeClose: false,
116         close(event) {
117           // Automatically destroy the DOM element that was used for the dialog.
118           $(event.target).remove();
119         },
120       });
121
122       confirmationDialog.showModal();
123     }
124     else {
125       changeTextEditor(field, newFormatID);
126     }
127   }
128
129   /**
130    * Initialize an empty object for editors to place their attachment code.
131    *
132    * @namespace
133    */
134   Drupal.editors = {};
135
136   /**
137    * Enables editors on text_format elements.
138    *
139    * @type {Drupal~behavior}
140    *
141    * @prop {Drupal~behaviorAttach} attach
142    *   Attaches an editor to an input element.
143    * @prop {Drupal~behaviorDetach} detach
144    *   Detaches an editor from an input element.
145    */
146   Drupal.behaviors.editor = {
147     attach(context, settings) {
148       // If there are no editor settings, there are no editors to enable.
149       if (!settings.editor) {
150         return;
151       }
152
153       $(context).find('[data-editor-for]').once('editor').each(function () {
154         const $this = $(this);
155         const field = findFieldForFormatSelector($this);
156
157         // Opt-out if no supported text area was found.
158         if (!field) {
159           return;
160         }
161
162         // Store the current active format.
163         const activeFormatID = $this.val();
164         field.setAttribute('data-editor-active-text-format', activeFormatID);
165
166         // Directly attach this text editor, if the text format is enabled.
167         if (settings.editor.formats[activeFormatID]) {
168           // XSS protection for the current text format/editor is performed on
169           // the server side, so we don't need to do anything special here.
170           Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
171         }
172         // When there is no text editor for this text format, still track
173         // changes, because the user has the ability to switch to some text
174         // editor, otherwise this code would not be executed.
175         $(field).on('change.editor keypress.editor', () => {
176           field.setAttribute('data-editor-value-is-changed', 'true');
177           // Just knowing that the value was changed is enough, stop tracking.
178           $(field).off('.editor');
179         });
180
181         // Attach onChange handler to text format selector element.
182         if ($this.is('select')) {
183           $this.on('change.editorAttach', { field }, onTextFormatChange);
184         }
185         // Detach any editor when the containing form is submitted.
186         $this.parents('form').on('submit', (event) => {
187           // Do not detach if the event was canceled.
188           if (event.isDefaultPrevented()) {
189             return;
190           }
191           // Detach the current editor (if any).
192           if (settings.editor.formats[activeFormatID]) {
193             Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize');
194           }
195         });
196       });
197     },
198
199     detach(context, settings, trigger) {
200       let editors;
201       // The 'serialize' trigger indicates that we should simply update the
202       // underlying element with the new text, without destroying the editor.
203       if (trigger === 'serialize') {
204         // Removing the editor-processed class guarantees that the editor will
205         // be reattached. Only do this if we're planning to destroy the editor.
206         editors = $(context).find('[data-editor-for]').findOnce('editor');
207       }
208       else {
209         editors = $(context).find('[data-editor-for]').removeOnce('editor');
210       }
211
212       editors.each(function () {
213         const $this = $(this);
214         const activeFormatID = $this.val();
215         const field = findFieldForFormatSelector($this);
216         if (field && activeFormatID in settings.editor.formats) {
217           Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger);
218         }
219       });
220     },
221   };
222
223   /**
224    * Attaches editor behaviors to the field.
225    *
226    * @param {HTMLElement} field
227    *   The textarea DOM element.
228    * @param {object} format
229    *   The text format that's being activated, from
230    *   drupalSettings.editor.formats.
231    *
232    * @listens event:change
233    *
234    * @fires event:formUpdated
235    */
236   Drupal.editorAttach = function (field, format) {
237     if (format.editor) {
238       // Attach the text editor.
239       Drupal.editors[format.editor].attach(field, format);
240
241       // Ensures form.js' 'formUpdated' event is triggered even for changes that
242       // happen within the text editor.
243       Drupal.editors[format.editor].onChange(field, () => {
244         $(field).trigger('formUpdated');
245
246         // Keep track of changes, so we know what to do when switching text
247         // formats and guaranteeing XSS protection.
248         field.setAttribute('data-editor-value-is-changed', 'true');
249       });
250     }
251   };
252
253   /**
254    * Detaches editor behaviors from the field.
255    *
256    * @param {HTMLElement} field
257    *   The textarea DOM element.
258    * @param {object} format
259    *   The text format that's being activated, from
260    *   drupalSettings.editor.formats.
261    * @param {string} trigger
262    *   Trigger value from the detach behavior.
263    */
264   Drupal.editorDetach = function (field, format, trigger) {
265     if (format.editor) {
266       Drupal.editors[format.editor].detach(field, format, trigger);
267
268       // Restore the original value if the user didn't make any changes yet.
269       if (field.getAttribute('data-editor-value-is-changed') === 'false') {
270         field.value = field.getAttribute('data-editor-value-original');
271       }
272     }
273   };
274
275   /**
276    * Filter away XSS attack vectors when switching text formats.
277    *
278    * @param {HTMLElement} field
279    *   The textarea DOM element.
280    * @param {object} format
281    *   The text format that's being activated, from
282    *   drupalSettings.editor.formats.
283    * @param {string} originalFormatID
284    *   The text format ID of the original text format.
285    * @param {function} callback
286    *   A callback to be called (with no parameters) after the field's value has
287    *   been XSS filtered.
288    */
289   function filterXssWhenSwitching(field, format, originalFormatID, callback) {
290     // A text editor that already is XSS-safe needs no additional measures.
291     if (format.editor.isXssSafe) {
292       callback(field, format);
293     }
294     // Otherwise, ensure XSS safety: let the server XSS filter this value.
295     else {
296       $.ajax({
297         url: Drupal.url(`editor/filter_xss/${format.format}`),
298         type: 'POST',
299         data: {
300           value: field.value,
301           original_format_id: originalFormatID,
302         },
303         dataType: 'json',
304         success(xssFilteredValue) {
305           // If the server returns false, then no XSS filtering is needed.
306           if (xssFilteredValue !== false) {
307             field.value = xssFilteredValue;
308           }
309           callback(field, format);
310         },
311       });
312     }
313   }
314 }(jQuery, Drupal, drupalSettings));