<?php

/**
 * @file
 * Provides integration with the CKEditor WYSIWYG editor.
 */

use Drupal\Core\Url;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ckeditor\CKEditorPluginButtonsInterface;
use Drupal\ckeditor\CKEditorPluginContextualInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\editor\Entity\Editor;
use Drupal\editor\EditorInterface;

/**
 * Implements hook_help().
 */
function ckeditor_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.ckeditor':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The CKEditor module provides a highly-accessible, highly-usable visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href=":text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see the <a href=":doc_url">online documentation for the CKEditor module</a> and the <a href=":cke_url">CKEditor website</a>.', [':doc_url' => 'https://www.drupal.org/documentation/modules/ckeditor', ':cke_url' => 'http://ckeditor.com', ':text_editor' => Url::fromRoute('help.page', ['name' => 'editor'])->toString()]) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Enabling CKEditor for individual text formats') . '</dt>';
      $output .= '<dd>' . t('CKEditor has to be enabled and configured separately for individual text formats from the <a href=":formats">Text formats and editors page</a> because the filter settings for each text format can be different. For more information, see the <a href=":text_editor">Text Editor help page</a> and <a href=":filter">Filter help page</a>.', [':formats' => Url::fromRoute('filter.admin_overview')->toString(), ':text_editor' => Url::fromRoute('help.page', ['name' => 'editor'])->toString(), ':filter' => Url::fromRoute('help.page', ['name' => 'filter'])->toString()]) . '</dd>';
      $output .= '<dt>' . t('Configuring the toolbar') . '</dt>';
      $output .= '<dd>' . t('When CKEditor is chosen from the <em>Text editor</em> drop-down menu, its toolbar configuration is displayed. You can add and remove buttons from the <em>Active toolbar</em> by dragging and dropping them, and additional rows can be added to organize the buttons.') . '</dd>';
      $output .= '<dt>' . t('Formatting content') . '</dt>';
      $output .= '<dd>' . t('CKEditor only allow users to format content in accordance with the filter configuration of the specific text format. If a text format excludes certain HTML tags, the corresponding toolbar buttons are not displayed to users when they edit a text field in this format. For more information see the <a href=":filter">Filter help page</a>.', [':filter' => Url::fromRoute('help.page', ['name' => 'filter'])->toString()]) . '</dd>';
      $output .= '<dt>' . t('Toggling between formatted text and HTML source') . '</dt>';
      $output .= '<dd>' . t('If the <em>Source</em> button is available in the toolbar, users can click this button to disable the visual editor and edit the HTML source directly. After toggling back, the visual editor uses the allowed HTML tags to format the text — independent of whether buttons for these tags are available in the toolbar. If the text format is set to <em>limit the use of HTML tags</em>, then all excluded tags will be stripped out of the HTML source when the user toggles back to the text editor.') . '</dd>';
      $output .= '<dt>' . t('Check my spelling as I type') . '</dt>';
      $output .= '<dd>' . t("By default, CKEditor is configured to leverage your browser's spell check capability. Make sure your browser's spell checker is enabled in your browser's settings. To access suggested corrections for misspelled words, it may be necessary to hold the <em>Control</em> or <em>command</em> (Mac) key while right-clicking the misspelling.") . '</dd>';
      $output .= '<dt>' . t('Accessibility features') . '</dt>';
      $output .= '<dd>' . t('The built in WYSIWYG editor (CKEditor) comes with a number of <a href=":features">accessibility features</a>. CKEditor comes with built in <a href=":shortcuts">keyboard shortcuts</a>, which can be beneficial for both power users and keyboard only users.', [':features' => 'http://docs.ckeditor.com/#!/guide/dev_a11y', ':shortcuts' => 'http://docs.ckeditor.com/#!/guide/dev_shortcuts']) . '</dd>';
      $output .= '<dt>' . t('Generating accessible content') . '</dt>';
      $output .= '<dd>';
      $output .= '<ul>';
      $output .= '<li>' . t('HTML tables can be created with table headers and caption/summary elements.') . '</li>';
      $output .= '<li>' . t('Alt text is required by default on images added through CKEditor (note that this can be overridden).') . '</li>';
      $output .= '<li>' . t('Semantic HTML5 figure/figcaption are available to add captions to images.') . '</li>';
      $output .= '<li>' . t('To support multilingual page content, CKEditor can be configured to include a language button in the toolbar.') . '</li>';
      $output .= '</ul>';
      $output .= '</dd>';
      $output .= '</dl>';
      return $output;
  }
}

/**
 * Implements hook_theme().
 */
function ckeditor_theme() {
  return [
    'ckeditor_settings_toolbar' => [
      'file' => 'ckeditor.admin.inc',
      'variables' => ['editor' => NULL, 'plugins' => NULL],
    ],
  ];
}

/**
 * Implements hook_ckeditor_css_alter().
 */
function ckeditor_ckeditor_css_alter(array &$css, Editor $editor) {
  if (!$editor->hasAssociatedFilterFormat()) {
    return;
  }

  // Add the filter caption CSS if the text format associated with this text
  // editor uses the filter_caption filter. This is used by the included
  // CKEditor DrupalImageCaption plugin.
  if ($editor->getFilterFormat()->filters('filter_caption')->status) {
    $css[] = \Drupal::service('extension.list.module')->getPath('filter') . '/css/filter.caption.css';
  }
}

/**
 * Retrieves the default theme's CKEditor stylesheets.
 *
 * Themes may specify iframe-specific CSS files for use with CKEditor by
 * including a "ckeditor_stylesheets" key in their .info.yml file.
 *
 * @code
 * ckeditor_stylesheets:
 *   - css/ckeditor-iframe.css
 * @endcode
 */
function _ckeditor_theme_css($theme = NULL) {
  $css = [];
  if (!isset($theme)) {
    $theme = \Drupal::config('system.theme')->get('default');
  }
  /** @var \Drupal\Core\Extension\ThemeExtensionList $theme_list */
  $theme_list = \Drupal::service('extension.list.theme');
  if (isset($theme) && $theme_path = $theme_list->getPath($theme)) {
    $info = $theme_list->getExtensionInfo($theme);
    if (isset($info['ckeditor_stylesheets'])) {
      $css = $info['ckeditor_stylesheets'];
      foreach ($css as $key => $url) {
        // CSS url is external.
        if (UrlHelper::isExternal($url)) {
          $css[$key] = $url;
        }
        // CSS url is relative to Drupal root.
        elseif ($url[0] === '/') {
          $css[$key] = substr($url, 1);
        }
        // CSS url is relative to theme.
        else {
          $css[$key] = $theme_path . '/' . $url;
        }
      }
    }
    if (isset($info['base theme'])) {
      $css = array_merge(_ckeditor_theme_css($info['base theme']), $css);
    }
  }
  return $css;
}

/**
 * Gets all enabled CKEditor 4 plugins.
 *
 * @param \Drupal\editor\EditorInterface $editor
 *   A text editor config entity configured to use CKEditor 4.
 *
 * @return string[]
 *   The enabled CKEditor 4 plugin IDs.
 *
 * @internal
 */
function _ckeditor_get_enabled_plugins(EditorInterface $editor): array {
  assert($editor->getEditor() === 'ckeditor');

  $cke4_plugin_manager = \Drupal::service('plugin.manager.ckeditor.plugin');

  // This is largely copied from the CKEditor 4 plugin manager, because it
  // unfortunately does not provide the API this needs.
  // @see \Drupal\ckeditor\CKEditorPluginManager::getEnabledPluginFiles()
  $plugins = array_keys($cke4_plugin_manager->getDefinitions());
  $toolbar_buttons = $cke4_plugin_manager->getEnabledButtons($editor);
  $enabled_plugins = [];
  $additional_plugins = [];
  foreach ($plugins as $plugin_id) {
    $plugin = $cke4_plugin_manager->createInstance($plugin_id);

    $enabled = FALSE;
    // Plugin is enabled if it provides a button that has been enabled.
    if ($plugin instanceof CKEditorPluginButtonsInterface) {
      $plugin_buttons = array_keys($plugin->getButtons());
      $enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0);
    }
    // Otherwise plugin is enabled if it declares itself as enabled.
    if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) {
      $enabled = $plugin->isEnabled($editor);
    }

    if ($enabled) {
      $enabled_plugins[$plugin_id] = $plugin_id;
      // Check if this plugin has dependencies that should be considered
      // enabled.
      $additional_plugins = array_merge($additional_plugins, array_diff($plugin->getDependencies($editor), $additional_plugins));
    }
  }

  // Add the list of dependent plugins.
  foreach ($additional_plugins as $plugin_id) {
    $enabled_plugins[$plugin_id] = $plugin_id;
  }

  return $enabled_plugins;
}

/**
 * Implements hook_library_info_alter().
 */
function ckeditor_library_info_alter(&$libraries, $extension) {
  // Pass Drupal's JS cache-busting string via settings along to CKEditor.
  // @see http://docs.ckeditor.com/#!/api/CKEDITOR-property-timestamp
  if ($extension === 'ckeditor' && isset($libraries['drupal.ckeditor'])) {
    $query_string = \Drupal::state()->get('system.css_js_query_string', '0');
    $libraries['drupal.ckeditor']['drupalSettings']['ckeditor']['timestamp'] = $query_string;
  }
  // Add an alias to keep "core/ckeditor" working if it doesn't exist already.
  if ($extension === 'core' && empty($libraries['ckeditor'])) {
    $libraries['ckeditor'] = [
      'dependencies' => ['ckeditor/ckeditor'],
    ];
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function ckeditor_form_filter_format_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  // Add an additional validate callback so we can ensure the media_embed filter
  // is enabled when the DrupalMediaLibrary button is enabled.
  $form['#validate'][] = 'ckeditor_filter_format_edit_form_validate';
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function ckeditor_form_filter_format_add_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  // Add an additional validate callback so we can ensure the media_embed filter
  // is enabled when the DrupalMediaLibrary button is enabled.
  $form['#validate'][] = 'ckeditor_filter_format_edit_form_validate';
}

/**
 * Validate callback to ensure the DrupalMediaLibrary button can work correctly.
 */
function ckeditor_filter_format_edit_form_validate($form, FormStateInterface $form_state) {
  if ($form_state->getTriggeringElement()['#name'] !== 'op') {
    return;
  }

  // The "DrupalMediaLibrary" button is for the CKEditor text editor.
  if ($form_state->getValue(['editor', 'editor']) !== 'ckeditor') {
    return;
  }

  $button_group_path = [
    'editor',
    'settings',
    'toolbar',
    'button_groups',
  ];

  if ($button_groups = $form_state->getValue($button_group_path)) {
    $buttons = [];
    $button_groups = Json::decode($button_groups);

    foreach ($button_groups as $button_row) {
      foreach ($button_row as $button_group) {
        $buttons = array_merge($buttons, array_values($button_group['items']));
      }
    }

    $get_filter_label = function ($filter_plugin_id) use ($form) {
      return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup'];
    };

    if (in_array('DrupalMediaLibrary', $buttons, TRUE)) {
      $media_embed_enabled = $form_state->getValue([
        'filters',
        'media_embed',
        'status',
      ]);

      if (!$media_embed_enabled) {
        $error_message = new TranslatableMarkup('The %media-embed-filter-label filter must be enabled to use the %drupal-media-library-button button.', [
          '%media-embed-filter-label' => $get_filter_label('media_embed'),
          '%drupal-media-library-button' => new TranslatableMarkup('Insert from Media Library'),
        ]);
        $form_state->setErrorByName('filters', $error_message);
      }
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function ckeditor_editor_presave(EditorInterface $editor) {
  // Only try to update editors using CKEditor 4.
  if ($editor->getEditor() !== 'ckeditor') {
    return FALSE;
  }

  $enabled_plugins = _ckeditor_get_enabled_plugins($editor);

  // Only update if the editor has plugin settings for disabled plugins.
  $needs_update = FALSE;
  $settings = $editor->getSettings();

  // Updates are not needed if plugin settings are not defined for the editor.
  if (!isset($settings['plugins'])) {
    return;
  }

  foreach (array_keys($settings['plugins']) as $plugin_id) {
    if (!in_array($plugin_id, $enabled_plugins, TRUE)) {
      unset($settings['plugins'][$plugin_id]);
      $needs_update = TRUE;
    }
  }
  if ($needs_update) {
    $editor->setSettings($settings);
  }
}
