annotate core/modules/editor/editor.module @ 19:fa3358dc1485 tip

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