f3bb1b5314185eb096385b83936dc1c63f4b8db3
[yaffs-website] / web / core / modules / editor / editor.module
1 <?php
2
3 /**
4  * @file
5  * Adds bindings for client-side "text editors" to text formats.
6  */
7
8 use Drupal\Component\Utility\Html;
9 use Drupal\editor\Entity\Editor;
10 use Drupal\Core\Entity\FieldableEntityInterface;
11 use Drupal\Core\Field\FieldDefinitionInterface;
12 use Drupal\Core\Form\FormStateInterface;
13 use Drupal\Core\Render\Element;
14 use Drupal\Core\Routing\RouteMatchInterface;
15 use Drupal\Core\StringTranslation\TranslatableMarkup;
16 use Drupal\Core\Entity\EntityInterface;
17 use Drupal\filter\FilterFormatInterface;
18 use Drupal\filter\Plugin\FilterInterface;
19
20 /**
21  * Implements hook_help().
22  */
23 function editor_help($route_name, RouteMatchInterface $route_match) {
24   switch ($route_name) {
25     case 'help.page.editor':
26       $output = '';
27       $output .= '<h3>' . t('About') . '</h3>';
28       $output .= '<p>' . t('The Text Editor module provides a framework that other modules (such as <a href=":ckeditor">CKEditor module</a>) can use to provide toolbars and other functionality that allow users to format text more easily than typing HTML tags directly. For more information, see the <a href=":documentation">online documentation for the Text Editor module</a>.', [':documentation' => 'https://www.drupal.org/documentation/modules/editor', ':ckeditor' => (\Drupal::moduleHandler()->moduleExists('ckeditor')) ? \Drupal::url('help.page', ['name' => 'ckeditor']) : '#']) . '</p>';
29       $output .= '<h3>' . t('Uses') . '</h3>';
30       $output .= '<dl>';
31       $output .= '<dt>' . t('Installing text editors') . '</dt>';
32       $output .= '<dd>' . t('The Text Editor module provides a framework for managing editors. To use it, you also need to enable a text editor. This can either be the core <a href=":ckeditor">CKEditor module</a>, which can be enabled on the <a href=":extend">Extend page</a>, or a contributed module for any other text editor. When installing a contributed text editor module, be sure to check the installation instructions, because you will most likely need to download and install an external library as well as the Drupal module.', [':ckeditor' => (\Drupal::moduleHandler()->moduleExists('ckeditor')) ? \Drupal::url('help.page', ['name' => 'ckeditor']) : '#', ':extend' => \Drupal::url('system.modules_list')]) . '</dd>';
33       $output .= '<dt>' . t('Enabling a text editor for a text format') . '</dt>';
34       $output .= '<dd>' . t('On the <a href=":formats">Text formats and editors page</a> you can see which text editor is associated with each text format. You can change this by clicking on the <em>Configure</em> link, and then choosing a text editor or <em>none</em> from the <em>Text editor</em> drop-down list. The text editor will then be displayed with any text field for which this text format is chosen.', [':formats' => \Drupal::url('filter.admin_overview')]) . '</dd>';
35       $output .= '<dt>' . t('Configuring a text editor') . '</dt>';
36       $output .= '<dd>' . t('Once a text editor is associated with a text format, you can configure it by clicking on the <em>Configure</em> link for this format. Depending on the specific text editor, you can configure it for example by adding buttons to its toolbar. Typically these buttons provide formatting or editing tools, and they often insert HTML tags into the field source. For details, see the help page of the specific text editor.') . '</dd>';
37       $output .= '<dt>' . t('Using different text editors and formats') . '</dt>';
38       $output .= '<dd>' . t('If you change the text format on a text field, the text editor will change as well because the text editor configuration is associated with the individual text format. This allows the use of the same text editor with different options for different text formats. It also allows users to choose between text formats with different text editors if they are installed.') . '</dd>';
39       $output .= '</dl>';
40       return $output;
41   }
42 }
43
44 /**
45  * Implements hook_menu_links_discovered_alter().
46  *
47  * Rewrites the menu entries for filter module that relate to the configuration
48  * of text editors.
49  */
50 function editor_menu_links_discovered_alter(array &$links) {
51   $links['filter.admin_overview']['title'] = new TranslatableMarkup('Text formats and editors');
52   $links['filter.admin_overview']['description'] = new TranslatableMarkup('Select and configure text editors, and how content is filtered when displayed.');
53 }
54
55 /**
56  * Implements hook_element_info_alter().
57  *
58  * Extends the functionality of text_format elements (provided by Filter
59  * module), so that selecting a text format notifies a client-side text editor
60  * when it should be enabled or disabled.
61  *
62  * @see \Drupal\filter\Element\TextFormat
63  */
64 function editor_element_info_alter(&$types) {
65   $types['text_format']['#pre_render'][] = 'element.editor:preRenderTextFormat';
66 }
67
68 /**
69  * Implements hook_form_FORM_ID_alter() for \Drupal\filter\FilterFormatListBuilder.
70  *
71  * Implements hook_field_formatter_info_alter().
72  *
73  * @see quickedit_field_formatter_info_alter()
74  */
75 function editor_field_formatter_info_alter(&$info) {
76   // Update \Drupal\text\Plugin\Field\FieldFormatter\TextDefaultFormatter's
77   // annotation to indicate that it supports the 'editor' in-place editor
78   // provided by this module.
79   $info['text_default']['quickedit'] = ['editor' => 'editor'];
80 }
81
82 /**
83  * Implements hook_form_FORM_ID_alter().
84  */
85 function editor_form_filter_admin_overview_alter(&$form, FormStateInterface $form_state) {
86   // @todo Cleanup column injection: https://www.drupal.org/node/1876718.
87   // Splice in the column for "Text editor" into the header.
88   $position = array_search('name', $form['formats']['#header']) + 1;
89   $start = array_splice($form['formats']['#header'], 0, $position, ['editor' => t('Text editor')]);
90   $form['formats']['#header'] = array_merge($start, $form['formats']['#header']);
91
92   // Then splice in the name of each text editor for each text format.
93   $editors = \Drupal::service('plugin.manager.editor')->getDefinitions();
94   foreach (Element::children($form['formats']) as $format_id) {
95     $editor = editor_load($format_id);
96     $editor_name = ($editor && isset($editors[$editor->getEditor()])) ? $editors[$editor->getEditor()]['label'] : '—';
97     $editor_column['editor'] = ['#markup' => $editor_name];
98     $position = array_search('name', array_keys($form['formats'][$format_id])) + 1;
99     $start = array_splice($form['formats'][$format_id], 0, $position, $editor_column);
100     $form['formats'][$format_id] = array_merge($start, $form['formats'][$format_id]);
101   }
102 }
103
104 /**
105  * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\filter\FilterFormatEditForm.
106  */
107 function editor_form_filter_format_form_alter(&$form, FormStateInterface $form_state) {
108   $editor = $form_state->get('editor');
109   if ($editor === NULL) {
110     $format = $form_state->getFormObject()->getEntity();
111     $format_id = $format->isNew() ? NULL : $format->id();
112     $editor = editor_load($format_id);
113     $form_state->set('editor', $editor);
114   }
115
116   // Associate a text editor with this text format.
117   $manager = \Drupal::service('plugin.manager.editor');
118   $editor_options = $manager->listOptions();
119   $form['editor'] = [
120     // Position the editor selection before the filter settings (weight of 0),
121     // but after the filter label and name (weight of -20).
122     '#weight' => -9,
123   ];
124   $form['editor']['editor'] = [
125     '#type' => 'select',
126     '#title' => t('Text editor'),
127     '#options' => $editor_options,
128     '#empty_option' => t('None'),
129     '#default_value' => $editor ? $editor->getEditor() : '',
130     '#ajax' => [
131       'trigger_as' => ['name' => 'editor_configure'],
132       'callback' => 'editor_form_filter_admin_form_ajax',
133       'wrapper' => 'editor-settings-wrapper',
134     ],
135     '#weight' => -10,
136   ];
137   $form['editor']['configure'] = [
138     '#type' => 'submit',
139     '#name' => 'editor_configure',
140     '#value' => t('Configure'),
141     '#limit_validation_errors' => [['editor']],
142     '#submit' => ['editor_form_filter_admin_format_editor_configure'],
143     '#ajax' => [
144       'callback' => 'editor_form_filter_admin_form_ajax',
145       'wrapper' => 'editor-settings-wrapper',
146     ],
147     '#weight' => -10,
148     '#attributes' => ['class' => ['js-hide']],
149   ];
150
151   // If there aren't any options (other than "None"), disable the select list.
152   if (empty($editor_options)) {
153     $form['editor']['editor']['#disabled'] = TRUE;
154     $form['editor']['editor']['#description'] = t('This option is disabled because no modules that provide a text editor are currently enabled.');
155   }
156
157   $form['editor']['settings'] = [
158     '#tree' => TRUE,
159     '#weight' => -8,
160     '#type' => 'container',
161     '#id' => 'editor-settings-wrapper',
162     '#attached' => [
163       'library' => [
164         'editor/drupal.editor.admin',
165       ],
166     ],
167   ];
168
169   // Add editor-specific validation and submit handlers.
170   if ($editor) {
171     /** @var $plugin \Drupal\editor\Plugin\EditorPluginInterface */
172     $plugin = $manager->createInstance($editor->getEditor());
173     $settings_form = [];
174     $settings_form['#element_validate'][] = [$plugin, 'validateConfigurationForm'];
175     $form['editor']['settings']['subform'] = $plugin->buildConfigurationForm($settings_form, $form_state);
176     $form['editor']['settings']['subform']['#parents'] = ['editor', 'settings'];
177     $form['actions']['submit']['#submit'][] = [$plugin, 'submitConfigurationForm'];
178   }
179
180   $form['#validate'][] = 'editor_form_filter_admin_format_validate';
181   $form['actions']['submit']['#submit'][] = 'editor_form_filter_admin_format_submit';
182 }
183
184 /**
185  * Button submit handler for filter_format_form()'s 'editor_configure' button.
186  */
187 function editor_form_filter_admin_format_editor_configure($form, FormStateInterface $form_state) {
188   $editor = $form_state->get('editor');
189   $editor_value = $form_state->getValue(['editor', 'editor']);
190   if ($editor_value !== NULL) {
191     if ($editor_value === '') {
192       $form_state->set('editor', FALSE);
193     }
194     elseif (empty($editor) || $editor_value !== $editor->getEditor()) {
195       $format = $form_state->getFormObject()->getEntity();
196       $editor = Editor::create([
197         'format' => $format->isNew() ? NULL : $format->id(),
198         'editor' => $editor_value,
199       ]);
200       $form_state->set('editor', $editor);
201     }
202   }
203   $form_state->setRebuild();
204 }
205
206 /**
207  * AJAX callback handler for filter_format_form().
208  */
209 function editor_form_filter_admin_form_ajax($form, FormStateInterface $form_state) {
210   return $form['editor']['settings'];
211 }
212
213 /**
214  * Additional validate handler for filter_format_form().
215  */
216 function editor_form_filter_admin_format_validate($form, FormStateInterface $form_state) {
217   // This validate handler is not applicable when using the 'Configure' button.
218   if ($form_state->getTriggeringElement()['#name'] === 'editor_configure') {
219     return;
220   }
221
222   // When using this form with JavaScript disabled in the browser, the
223   // 'Configure' button won't be clicked automatically. So, when the user has
224   // selected a text editor and has then clicked 'Save configuration', we should
225   // point out that the user must still configure the text editor.
226   if ($form_state->getValue(['editor', 'editor']) !== '' && !$form_state->get('editor')) {
227     $form_state->setErrorByName('editor][editor', t('You must configure the selected text editor.'));
228   }
229 }
230
231 /**
232  * Additional submit handler for filter_format_form().
233  */
234 function editor_form_filter_admin_format_submit($form, FormStateInterface $form_state) {
235   // Delete the existing editor if disabling or switching between editors.
236   $format = $form_state->getFormObject()->getEntity();
237   $format_id = $format->isNew() ? NULL : $format->id();
238   $original_editor = editor_load($format_id);
239   if ($original_editor && $original_editor->getEditor() != $form_state->getValue(['editor', 'editor'])) {
240     $original_editor->delete();
241   }
242
243   // Create a new editor or update the existing editor.
244   if ($editor = $form_state->get('editor')) {
245     // Ensure the text format is set: when creating a new text format, this
246     // would equal the empty string.
247     $editor->set('format', $format_id);
248     if ($settings = $form_state->getValue(['editor', 'settings'])) {
249       $editor->setSettings($settings);
250     }
251     $editor->save();
252   }
253 }
254
255 /**
256  * Loads an individual configured text editor based on text format ID.
257  *
258  * @param int $format_id
259  *   A text format ID.
260  *
261  * @return \Drupal\editor\Entity\Editor|null
262  *   A text editor object, or NULL.
263  */
264 function editor_load($format_id) {
265   // Load all the editors at once here, assuming that either no editors or more
266   // than one editor will be needed on a page (such as having multiple text
267   // formats for administrators). Loading a small number of editors all at once
268   // is more efficient than loading multiple editors individually.
269   $editors = Editor::loadMultiple();
270   return isset($editors[$format_id]) ? $editors[$format_id] : NULL;
271 }
272
273 /**
274  * Applies text editor XSS filtering.
275  *
276  * @param string $html
277  *   The HTML string that will be passed to the text editor.
278  * @param \Drupal\filter\FilterFormatInterface|null $format
279  *   The text format whose text editor will be used or NULL if the previously
280  *   defined text format is now disabled.
281  * @param \Drupal\filter\FilterFormatInterface|null $original_format
282  *   (optional) The original text format (i.e. when switching text formats,
283  *   $format is the text format that is going to be used, $original_format is
284  *   the one that was being used initially, the one that is stored in the
285  *   database when editing).
286  *
287  * @return string|false
288  *   The XSS filtered string or FALSE when no XSS filtering needs to be applied,
289  *   because one of the next conditions might occur:
290  *   - No text editor is associated with the text format,
291  *   - The previously defined text format is now disabled,
292  *   - The text editor is safe from XSS,
293  *   - The text format does not use any XSS protection filters.
294  *
295  * @see https://www.drupal.org/node/2099741
296  */
297 function editor_filter_xss($html, FilterFormatInterface $format = NULL, FilterFormatInterface $original_format = NULL) {
298   $editor = $format ? editor_load($format->id()) : NULL;
299
300   // If no text editor is associated with this text format or the previously
301   // defined text format is now disabled, then we don't need text editor XSS
302   // filtering either.
303   if (!isset($editor)) {
304     return FALSE;
305   }
306
307   // If the text editor associated with this text format guarantees security,
308   // then we also don't need text editor XSS filtering.
309   $definition = \Drupal::service('plugin.manager.editor')->getDefinition($editor->getEditor());
310   if ($definition['is_xss_safe'] === TRUE) {
311     return FALSE;
312   }
313
314   // If there is no filter preventing XSS attacks in the text format being used,
315   // then no text editor XSS filtering is needed either. (Because then the
316   // editing user can already be attacked by merely viewing the content.)
317   // e.g.: an admin user creates content in Full HTML and then edits it, no text
318   // format switching happens; in this case, no text editor XSS filtering is
319   // desirable, because it would strip style attributes, amongst others.
320   $current_filter_types = $format->getFilterTypes();
321   if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $current_filter_types, TRUE)) {
322     if ($original_format === NULL) {
323       return FALSE;
324     }
325     // Unless we are switching from another text format, in which case we must
326     // first check whether a filter preventing XSS attacks is used in that text
327     // format, and if so, we must still apply XSS filtering.
328     // e.g.: an anonymous user creates content in Restricted HTML, an admin user
329     // edits it (then no XSS filtering is applied because no text editor is
330     // used), and switches to Full HTML (for which a text editor is used). Then
331     // we must apply XSS filtering to protect the admin user.
332     else {
333       $original_filter_types = $original_format->getFilterTypes();
334       if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $original_filter_types, TRUE)) {
335         return FALSE;
336       }
337     }
338   }
339
340   // Otherwise, apply the text editor XSS filter. We use the default one unless
341   // a module tells us to use a different one.
342   $editor_xss_filter_class = '\Drupal\editor\EditorXssFilter\Standard';
343   \Drupal::moduleHandler()->alter('editor_xss_filter', $editor_xss_filter_class, $format, $original_format);
344
345   return call_user_func($editor_xss_filter_class . '::filterXss', $html, $format, $original_format);
346 }
347
348 /**
349  * Implements hook_entity_insert().
350  */
351 function editor_entity_insert(EntityInterface $entity) {
352   // Only act on content entities.
353   if (!($entity instanceof FieldableEntityInterface)) {
354     return;
355   }
356   $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
357   foreach ($referenced_files_by_field as $field => $uuids) {
358     _editor_record_file_usage($uuids, $entity);
359   }
360 }
361
362 /**
363  * Implements hook_entity_update().
364  */
365 function editor_entity_update(EntityInterface $entity) {
366   // Only act on content entities.
367   if (!($entity instanceof FieldableEntityInterface)) {
368     return;
369   }
370
371   // On new revisions, all files are considered to be a new usage and no
372   // deletion of previous file usages are necessary.
373   if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) {
374     $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
375     foreach ($referenced_files_by_field as $field => $uuids) {
376       _editor_record_file_usage($uuids, $entity);
377     }
378   }
379   // On modified revisions, detect which file references have been added (and
380   // record their usage) and which ones have been removed (delete their usage).
381   // File references that existed both in the previous version of the revision
382   // and in the new one don't need their usage to be updated.
383   else {
384     $original_uuids_by_field = _editor_get_file_uuids_by_field($entity->original);
385     $uuids_by_field = _editor_get_file_uuids_by_field($entity);
386
387     // Detect file usages that should be incremented.
388     foreach ($uuids_by_field as $field => $uuids) {
389       $added_files = array_diff($uuids_by_field[$field], $original_uuids_by_field[$field]);
390       _editor_record_file_usage($added_files, $entity);
391     }
392
393     // Detect file usages that should be decremented.
394     foreach ($original_uuids_by_field as $field => $uuids) {
395       $removed_files = array_diff($original_uuids_by_field[$field], $uuids_by_field[$field]);
396       _editor_delete_file_usage($removed_files, $entity, 1);
397     }
398   }
399 }
400
401 /**
402  * Implements hook_entity_delete().
403  */
404 function editor_entity_delete(EntityInterface $entity) {
405   // Only act on content entities.
406   if (!($entity instanceof FieldableEntityInterface)) {
407     return;
408   }
409   $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
410   foreach ($referenced_files_by_field as $field => $uuids) {
411     _editor_delete_file_usage($uuids, $entity, 0);
412   }
413 }
414
415 /**
416  * Implements hook_entity_revision_delete().
417  */
418 function editor_entity_revision_delete(EntityInterface $entity) {
419   // Only act on content entities.
420   if (!($entity instanceof FieldableEntityInterface)) {
421     return;
422   }
423   $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
424   foreach ($referenced_files_by_field as $field => $uuids) {
425     _editor_delete_file_usage($uuids, $entity, 1);
426   }
427 }
428
429 /**
430  * Records file usage of files referenced by formatted text fields.
431  *
432  * Every referenced file that does not yet have the FILE_STATUS_PERMANENT state,
433  * will be given that state.
434  *
435  * @param array $uuids
436  *   An array of file entity UUIDs.
437  * @param \Drupal\Core\Entity\EntityInterface $entity
438  *   An entity whose fields to inspect for file references.
439  */
440 function _editor_record_file_usage(array $uuids, EntityInterface $entity) {
441   foreach ($uuids as $uuid) {
442     if ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid)) {
443       if ($file->status !== FILE_STATUS_PERMANENT) {
444         $file->status = FILE_STATUS_PERMANENT;
445         $file->save();
446       }
447       \Drupal::service('file.usage')->add($file, 'editor', $entity->getEntityTypeId(), $entity->id());
448     }
449   }
450 }
451
452 /**
453  * Deletes file usage of files referenced by formatted text fields.
454  *
455  * @param array $uuids
456  *   An array of file entity UUIDs.
457  * @param \Drupal\Core\Entity\EntityInterface $entity
458  *   An entity whose fields to inspect for file references.
459  * @param $count
460  *   The number of references to delete. Should be 1 when deleting a single
461  *   revision and 0 when deleting an entity entirely.
462  *
463  * @see \Drupal\file\FileUsage\FileUsageInterface::delete()
464  */
465 function _editor_delete_file_usage(array $uuids, EntityInterface $entity, $count) {
466   foreach ($uuids as $uuid) {
467     if ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid)) {
468       \Drupal::service('file.usage')->delete($file, 'editor', $entity->getEntityTypeId(), $entity->id(), $count);
469     }
470   }
471 }
472
473 /**
474  * Implements hook_file_download().
475  *
476  * @see file_file_download()
477  * @see file_get_file_references()
478  */
479 function editor_file_download($uri) {
480   // Get the file record based on the URI. If not in the database just return.
481   /** @var \Drupal\file\FileInterface[] $files */
482   $files = \Drupal::entityTypeManager()
483     ->getStorage('file')
484     ->loadByProperties(['uri' => $uri]);
485   if (count($files)) {
486     foreach ($files as $item) {
487       // Since some database servers sometimes use a case-insensitive comparison
488       // by default, double check that the filename is an exact match.
489       if ($item->getFileUri() === $uri) {
490         $file = $item;
491         break;
492       }
493     }
494   }
495   if (!isset($file)) {
496     return;
497   }
498
499   // Temporary files are handled by file_file_download(), so nothing to do here
500   // about them.
501   // @see file_file_download()
502
503   // Find out if any editor-backed field contains the file.
504   $usage_list = \Drupal::service('file.usage')->listUsage($file);
505
506   // Stop processing if there are no references in order to avoid returning
507   // headers for files controlled by other modules. Make an exception for
508   // temporary files where the host entity has not yet been saved (for example,
509   // an image preview on a node creation form) in which case, allow download by
510   // the file's owner.
511   if (empty($usage_list['editor']) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
512     return;
513   }
514
515   // Editor.module MUST NOT call $file->access() here (like file_file_download()
516   // does) as checking the 'download' access to a file entity would end up in
517   // FileAccessControlHandler->checkAccess() and ->getFileReferences(), which
518   // calls file_get_file_references(). This latter one would allow downloading
519   // files only handled by the file.module, which is exactly not the case right
520   // here. So instead we must check if the current user is allowed to view any
521   // of the entities that reference the image using the 'editor' module.
522   if ($file->isPermanent()) {
523     $referencing_entity_is_accessible = FALSE;
524     $references = empty($usage_list['editor']) ? [] : $usage_list['editor'];
525     foreach ($references as $entity_type => $entity_ids_usage_count) {
526       $referencing_entities = entity_load_multiple($entity_type, array_keys($entity_ids_usage_count));
527       /** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */
528       foreach ($referencing_entities as $referencing_entity) {
529         if ($referencing_entity->access('view', NULL, TRUE)->isAllowed()) {
530           $referencing_entity_is_accessible = TRUE;
531           break 2;
532         }
533       }
534     }
535     if (!$referencing_entity_is_accessible) {
536       return -1;
537     }
538   }
539
540   // Access is granted.
541   $headers = file_get_content_headers($file);
542   return $headers;
543 }
544
545 /**
546  * Finds all files referenced (data-entity-uuid) by formatted text fields.
547  *
548  * @param \Drupal\Core\Entity\EntityInterface $entity
549  *   An entity whose fields to analyze.
550  *
551  * @return array
552  *   An array of file entity UUIDs.
553  */
554 function _editor_get_file_uuids_by_field(EntityInterface $entity) {
555   $uuids = [];
556
557   $formatted_text_fields = _editor_get_formatted_text_fields($entity);
558   foreach ($formatted_text_fields as $formatted_text_field) {
559     $text = '';
560     $field_items = $entity->get($formatted_text_field);
561     foreach ($field_items as $field_item) {
562       $text .= $field_item->value;
563       if ($field_item->getFieldDefinition()->getType() == 'text_with_summary') {
564         $text .= $field_item->summary;
565       }
566     }
567     $uuids[$formatted_text_field] = _editor_parse_file_uuids($text);
568   }
569   return $uuids;
570 }
571
572 /**
573  * Determines the formatted text fields on an entity.
574  *
575  * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
576  *   An entity whose fields to analyze.
577  *
578  * @return array
579  *   The names of the fields on this entity that support formatted text.
580  */
581 function _editor_get_formatted_text_fields(FieldableEntityInterface $entity) {
582   $field_definitions = $entity->getFieldDefinitions();
583   if (empty($field_definitions)) {
584     return [];
585   }
586
587   // Only return formatted text fields.
588   return array_keys(array_filter($field_definitions, function (FieldDefinitionInterface $definition) {
589     return in_array($definition->getType(), ['text', 'text_long', 'text_with_summary'], TRUE);
590   }));
591 }
592
593 /**
594  * Parse an HTML snippet for any linked file with data-entity-uuid attributes.
595  *
596  * @param string $text
597  *   The partial (X)HTML snippet to load. Invalid markup will be corrected on
598  *   import.
599  *
600  * @return array
601  *   An array of all found UUIDs.
602  */
603 function _editor_parse_file_uuids($text) {
604   $dom = Html::load($text);
605   $xpath = new \DOMXPath($dom);
606   $uuids = [];
607   foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
608     $uuids[] = $node->getAttribute('data-entity-uuid');
609   }
610   return $uuids;
611 }
612
613 /**
614  * Implements hook_ENTITY_TYPE_presave().
615  *
616  * Synchronizes the editor status to its paired text format status.
617  */
618 function editor_filter_format_presave(FilterFormatInterface $format) {
619   // The text format being created cannot have a text editor yet.
620   if ($format->isNew()) {
621     return;
622   }
623
624   /** @var \Drupal\filter\FilterFormatInterface $original */
625   $original = \Drupal::entityManager()
626     ->getStorage('filter_format')
627     ->loadUnchanged($format->getOriginalId());
628
629   // If the text format status is the same, return early.
630   if (($status = $format->status()) === $original->status()) {
631     return;
632   }
633
634   /** @var \Drupal\editor\EditorInterface $editor */
635   if ($editor = Editor::load($format->id())) {
636     $editor->setStatus($status)->save();
637   }
638 }