editor.module

Same filename in other branches
  1. 9 core/modules/editor/editor.module
  2. 8.9.x core/modules/editor/editor.module
  3. 10 core/modules/editor/editor.module

File

core/modules/editor/editor.module

View source
<?php


/**
 * @file
 */
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\SubformState;
use Drupal\editor\Entity\Editor;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\filter\FilterFormatInterface;
use Drupal\filter\Plugin\FilterInterface;
use Drupal\text\Plugin\Field\FieldType\TextItemBase;

/**
 * Button submit handler for filter_format_form()'s 'editor_configure' button.
 */
function editor_form_filter_admin_format_editor_configure($form, FormStateInterface $form_state) {
    $editor = $form_state->get('editor');
    $editor_value = $form_state->getValue([
        'editor',
        'editor',
    ]);
    if ($editor_value !== NULL) {
        if ($editor_value === '') {
            $form_state->set('editor', FALSE);
            $form_state->set('editor_plugin', NULL);
        }
        elseif (empty($editor) || $editor_value !== $editor->getEditor()) {
            $format = $form_state->getFormObject()
                ->getEntity();
            $editor = Editor::create([
                'format' => $format->isNew() ? NULL : $format->id(),
                'editor' => $editor_value,
                'image_upload' => [
                    'status' => FALSE,
                ],
            ]);
            $form_state->set('editor', $editor);
        }
    }
    $form_state->setRebuild();
}

/**
 * AJAX callback handler for filter_format_form().
 */
function editor_form_filter_admin_form_ajax($form, FormStateInterface $form_state) {
    return $form['editor']['settings'];
}

/**
 * Additional validate handler for filter_format_form().
 */
function editor_form_filter_admin_format_validate($form, FormStateInterface $form_state) {
    $editor_set = $form_state->getValue([
        'editor',
        'editor',
    ]) !== "";
    $subform_array_exists = !empty($form['editor']['settings']['subform']) && is_array($form['editor']['settings']['subform']);
    if ($editor_set && $subform_array_exists && ($editor_plugin = $form_state->get('editor_plugin'))) {
        $subform_state = SubformState::createForSubform($form['editor']['settings']['subform'], $form, $form_state);
        $editor_plugin->validateConfigurationForm($form['editor']['settings']['subform'], $subform_state);
    }
    // This validate handler is not applicable when using the 'Configure' button.
    if ($form_state->getTriggeringElement()['#name'] === 'editor_configure') {
        return;
    }
    // When using this form with JavaScript disabled in the browser, the
    // 'Configure' button won't be clicked automatically. So, when the user has
    // selected a text editor and has then clicked 'Save configuration', we should
    // point out that the user must still configure the text editor.
    if ($form_state->getValue([
        'editor',
        'editor',
    ]) !== '' && !$form_state->get('editor')) {
        $form_state->setErrorByName('editor][editor', t('You must configure the selected text editor.'));
    }
}

/**
 * Additional submit handler for filter_format_form().
 */
function editor_form_filter_admin_format_submit($form, FormStateInterface $form_state) {
    // Delete the existing editor if disabling or switching between editors.
    $format = $form_state->getFormObject()
        ->getEntity();
    $format_id = $format->isNew() ? NULL : $format->id();
    $original_editor = editor_load($format_id);
    if ($original_editor && $original_editor->getEditor() != $form_state->getValue([
        'editor',
        'editor',
    ])) {
        $original_editor->delete();
    }
    $editor_set = $form_state->getValue([
        'editor',
        'editor',
    ]) !== "";
    $subform_array_exists = !empty($form['editor']['settings']['subform']) && is_array($form['editor']['settings']['subform']);
    if (($editor_plugin = $form_state->get('editor_plugin')) && $editor_set && $subform_array_exists) {
        $subform_state = SubformState::createForSubform($form['editor']['settings']['subform'], $form, $form_state);
        $editor_plugin->submitConfigurationForm($form['editor']['settings']['subform'], $subform_state);
    }
    // Create a new editor or update the existing editor.
    if ($editor = $form_state->get('editor')) {
        // Ensure the text format is set: when creating a new text format, this
        // would equal the empty string.
        $editor->set('format', $format_id);
        if ($settings = $form_state->getValue([
            'editor',
            'settings',
        ])) {
            $editor->setSettings($settings);
        }
        // When image uploads are disabled (status = FALSE), the schema for image
        // upload settings does not allow other keys to be present.
        // @see editor.image_upload_settings.*
        // @see editor.image_upload_settings.1
        // @see editor.schema.yml
        $image_upload_settings = $editor->getImageUploadSettings();
        if (!$image_upload_settings['status']) {
            $editor->setImageUploadSettings([
                'status' => FALSE,
            ]);
        }
        $editor->save();
    }
}

/**
 * Loads an individual configured text editor based on text format ID.
 *
 * @param int $format_id
 *   A text format ID.
 *
 * @return \Drupal\editor\Entity\Editor|null
 *   A text editor object, or NULL.
 */
function editor_load($format_id) {
    // Load all the editors at once here, assuming that either no editors or more
    // than one editor will be needed on a page (such as having multiple text
    // formats for administrators). Loading a small number of editors all at once
    // is more efficient than loading multiple editors individually.
    $editors = Editor::loadMultiple();
    return $editors[$format_id] ?? NULL;
}

/**
 * Applies text editor XSS filtering.
 *
 * @param string $html
 *   The HTML string that will be passed to the text editor.
 * @param \Drupal\filter\FilterFormatInterface|null $format
 *   The text format whose text editor will be used or NULL if the previously
 *   defined text format is now disabled.
 * @param \Drupal\filter\FilterFormatInterface|null $original_format
 *   (optional) The original text format (i.e. when switching text formats,
 *   $format is the text format that is going to be used, $original_format is
 *   the one that was being used initially, the one that is stored in the
 *   database when editing).
 *
 * @return string|false
 *   The XSS filtered string or FALSE when no XSS filtering needs to be applied,
 *   because one of the next conditions might occur:
 *   - No text editor is associated with the text format,
 *   - The previously defined text format is now disabled,
 *   - The text editor is safe from XSS,
 *   - The text format does not use any XSS protection filters.
 *
 * @see https://www.drupal.org/node/2099741
 */
function editor_filter_xss($html, ?FilterFormatInterface $format = NULL, ?FilterFormatInterface $original_format = NULL) {
    $editor = $format ? editor_load($format->id()) : NULL;
    // If no text editor is associated with this text format or the previously
    // defined text format is now disabled, then we don't need text editor XSS
    // filtering either.
    if (!isset($editor)) {
        return FALSE;
    }
    // If the text editor associated with this text format guarantees security,
    // then we also don't need text editor XSS filtering.
    $definition = \Drupal::service('plugin.manager.editor')->getDefinition($editor->getEditor());
    if ($definition['is_xss_safe'] === TRUE) {
        return FALSE;
    }
    // If there is no filter preventing XSS attacks in the text format being used,
    // then no text editor XSS filtering is needed either. (Because then the
    // editing user can already be attacked by merely viewing the content.)
    // e.g.: an admin user creates content in Full HTML and then edits it, no text
    // format switching happens; in this case, no text editor XSS filtering is
    // desirable, because it would strip style attributes, amongst others.
    $current_filter_types = $format->getFilterTypes();
    if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $current_filter_types, TRUE)) {
        if ($original_format === NULL) {
            return FALSE;
        }
        else {
            $original_filter_types = $original_format->getFilterTypes();
            if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $original_filter_types, TRUE)) {
                return FALSE;
            }
        }
    }
    // Otherwise, apply the text editor XSS filter. We use the default one unless
    // a module tells us to use a different one.
    $editor_xss_filter_class = '\\Drupal\\editor\\EditorXssFilter\\Standard';
    \Drupal::moduleHandler()->alter('editor_xss_filter', $editor_xss_filter_class, $format, $original_format);
    return call_user_func($editor_xss_filter_class . '::filterXss', $html, $format, $original_format);
}

/**
 * Records file usage of files referenced by formatted text fields.
 *
 * Every referenced file that is temporally saved will be resaved as permanent.
 *
 * @param array $uuids
 *   An array of file entity UUIDs.
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   An entity whose fields to inspect for file references.
 */
function _editor_record_file_usage(array $uuids, EntityInterface $entity) {
    foreach ($uuids as $uuid) {
        if ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid)) {
            
            /** @var \Drupal\file\FileInterface $file */
            if ($file->isTemporary()) {
                $file->setPermanent();
                $file->save();
            }
            \Drupal::service('file.usage')->add($file, 'editor', $entity->getEntityTypeId(), $entity->id());
        }
    }
}

/**
 * Deletes file usage of files referenced by formatted text fields.
 *
 * @param array $uuids
 *   An array of file entity UUIDs.
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   An entity whose fields to inspect for file references.
 * @param int $count
 *   The number of references to delete. Should be 1 when deleting a single
 *   revision and 0 when deleting an entity entirely.
 *
 * @see \Drupal\file\FileUsage\FileUsageInterface::delete()
 */
function _editor_delete_file_usage(array $uuids, EntityInterface $entity, $count) {
    foreach ($uuids as $uuid) {
        if ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid)) {
            \Drupal::service('file.usage')->delete($file, 'editor', $entity->getEntityTypeId(), $entity->id(), $count);
        }
    }
}

/**
 * Finds all files referenced (data-entity-uuid) by formatted text fields.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   An entity whose fields to analyze.
 *
 * @return array
 *   An array of file entity UUIDs.
 */
function _editor_get_file_uuids_by_field(EntityInterface $entity) {
    $uuids = [];
    $formatted_text_fields = _editor_get_formatted_text_fields($entity);
    foreach ($formatted_text_fields as $formatted_text_field) {
        $text = '';
        $field_items = $entity->get($formatted_text_field);
        foreach ($field_items as $field_item) {
            $text .= $field_item->value;
            if ($field_item->getFieldDefinition()
                ->getType() == 'text_with_summary') {
                $text .= $field_item->summary;
            }
        }
        $uuids[$formatted_text_field] = _editor_parse_file_uuids($text);
    }
    return $uuids;
}

/**
 * Determines the formatted text fields on an entity.
 *
 * A field type is considered to provide formatted text if its class is a
 * subclass of Drupal\text\Plugin\Field\FieldType\TextItemBase.
 *
 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
 *   An entity whose fields to analyze.
 *
 * @return array
 *   The names of the fields on this entity that support formatted text.
 */
function _editor_get_formatted_text_fields(FieldableEntityInterface $entity) {
    $field_definitions = $entity->getFieldDefinitions();
    if (empty($field_definitions)) {
        return [];
    }
    // Only return formatted text fields.
    // @todo improve as part of https://www.drupal.org/node/2732429
    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
    return array_keys(array_filter($field_definitions, function (FieldDefinitionInterface $definition) use ($field_type_manager) {
        $type = $definition->getType();
        $plugin_class = $field_type_manager->getPluginClass($type);
        return is_subclass_of($plugin_class, TextItemBase::class);
    }));
}

/**
 * Parse an HTML snippet for any linked file with data-entity-uuid attributes.
 *
 * @param string $text
 *   The partial (X)HTML snippet to load. Invalid markup will be corrected on
 *   import.
 *
 * @return array
 *   An array of all found UUIDs.
 */
function _editor_parse_file_uuids($text) {
    $dom = Html::load($text);
    $xpath = new \DOMXPath($dom);
    $uuids = [];
    foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
        $uuids[] = $node->getAttribute('data-entity-uuid');
    }
    return $uuids;
}

Functions

Title Deprecated Summary
editor_filter_xss Applies text editor XSS filtering.
editor_form_filter_admin_format_editor_configure Button submit handler for filter_format_form()'s 'editor_configure' button.
editor_form_filter_admin_format_submit Additional submit handler for filter_format_form().
editor_form_filter_admin_format_validate Additional validate handler for filter_format_form().
editor_form_filter_admin_form_ajax AJAX callback handler for filter_format_form().
editor_load Loads an individual configured text editor based on text format ID.
_editor_delete_file_usage Deletes file usage of files referenced by formatted text fields.
_editor_get_file_uuids_by_field Finds all files referenced (data-entity-uuid) by formatted text fields.
_editor_get_formatted_text_fields Determines the formatted text fields on an entity.
_editor_parse_file_uuids Parse an HTML snippet for any linked file with data-entity-uuid attributes.
_editor_record_file_usage Records file usage of files referenced by formatted text fields.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.