Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / modules / ckeditor / src / Plugin / Editor / CKEditor.php
1 <?php
2
3 namespace Drupal\ckeditor\Plugin\Editor;
4
5 use Drupal\Core\Extension\ModuleHandlerInterface;
6 use Drupal\ckeditor\CKEditorPluginManager;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Language\LanguageManagerInterface;
9 use Drupal\Core\Render\Element;
10 use Drupal\Core\Render\RendererInterface;
11 use Drupal\editor\Plugin\EditorBase;
12 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
13 use Drupal\editor\Entity\Editor;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15
16 /**
17  * Defines a CKEditor-based text editor for Drupal.
18  *
19  * @Editor(
20  *   id = "ckeditor",
21  *   label = @Translation("CKEditor"),
22  *   supports_content_filtering = TRUE,
23  *   supports_inline_editing = TRUE,
24  *   is_xss_safe = FALSE,
25  *   supported_element_types = {
26  *     "textarea"
27  *   }
28  * )
29  */
30 class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
31
32   /**
33    * The module handler to invoke hooks on.
34    *
35    * @var \Drupal\Core\Extension\ModuleHandlerInterface
36    */
37   protected $moduleHandler;
38
39   /**
40    * The language manager.
41    *
42    * @var \Drupal\Core\Language\LanguageManagerInterface
43    */
44   protected $languageManager;
45
46   /**
47    * The CKEditor plugin manager.
48    *
49    * @var \Drupal\ckeditor\CKEditorPluginManager
50    */
51   protected $ckeditorPluginManager;
52
53   /**
54    * The renderer.
55    *
56    * @var \Drupal\Core\Render\RendererInterface
57    */
58   protected $renderer;
59
60   /**
61    * Constructs a \Drupal\ckeditor\Plugin\Editor\CKEditor object.
62    *
63    * @param array $configuration
64    *   A configuration array containing information about the plugin instance.
65    * @param string $plugin_id
66    *   The plugin_id for the plugin instance.
67    * @param mixed $plugin_definition
68    *   The plugin implementation definition.
69    * @param \Drupal\ckeditor\CKEditorPluginManager $ckeditor_plugin_manager
70    *   The CKEditor plugin manager.
71    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
72    *   The module handler to invoke hooks on.
73    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
74    *   The language manager.
75    * @param \Drupal\Core\Render\RendererInterface $renderer
76    *   The renderer.
77    */
78   public function __construct(array $configuration, $plugin_id, $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, RendererInterface $renderer) {
79     parent::__construct($configuration, $plugin_id, $plugin_definition);
80     $this->ckeditorPluginManager = $ckeditor_plugin_manager;
81     $this->moduleHandler = $module_handler;
82     $this->languageManager = $language_manager;
83     $this->renderer = $renderer;
84   }
85
86   /**
87    * {@inheritdoc}
88    */
89   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
90     return new static(
91       $configuration,
92       $plugin_id,
93       $plugin_definition,
94       $container->get('plugin.manager.ckeditor.plugin'),
95       $container->get('module_handler'),
96       $container->get('language_manager'),
97       $container->get('renderer')
98     );
99   }
100
101   /**
102    * {@inheritdoc}
103    */
104   public function getDefaultSettings() {
105     return [
106       'toolbar' => [
107         'rows' => [
108           // Button groups.
109           [
110             [
111               'name' => $this->t('Formatting'),
112               'items' => ['Bold', 'Italic'],
113             ],
114             [
115               'name' => $this->t('Links'),
116               'items' => ['DrupalLink', 'DrupalUnlink'],
117             ],
118             [
119               'name' => $this->t('Lists'),
120               'items' => ['BulletedList', 'NumberedList'],
121             ],
122             [
123               'name' => $this->t('Media'),
124               'items' => ['Blockquote', 'DrupalImage'],
125             ],
126             [
127               'name' => $this->t('Tools'),
128               'items' => ['Source'],
129             ],
130           ],
131         ],
132       ],
133       'plugins' => ['language' => ['language_list' => 'un']],
134     ];
135   }
136
137   /**
138    * {@inheritdoc}
139    */
140   public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
141     $settings = $editor->getSettings();
142
143     $ckeditor_settings_toolbar = [
144       '#theme' => 'ckeditor_settings_toolbar',
145       '#editor' => $editor,
146       '#plugins' => $this->ckeditorPluginManager->getButtons(),
147     ];
148     $form['toolbar'] = [
149       '#type' => 'container',
150       '#attached' => [
151         'library' => ['ckeditor/drupal.ckeditor.admin'],
152         'drupalSettings' => [
153           'ckeditor' => [
154             'toolbarAdmin' => (string) $this->renderer->renderPlain($ckeditor_settings_toolbar),
155           ],
156         ],
157       ],
158       '#attributes' => ['class' => ['ckeditor-toolbar-configuration']],
159     ];
160
161     $form['toolbar']['button_groups'] = [
162       '#type' => 'textarea',
163       '#title' => $this->t('Toolbar buttons'),
164       '#default_value' => json_encode($settings['toolbar']['rows']),
165       '#attributes' => ['class' => ['ckeditor-toolbar-textarea']],
166     ];
167
168     // CKEditor plugin settings, if any.
169     $form['plugin_settings'] = [
170       '#type' => 'vertical_tabs',
171       '#title' => $this->t('CKEditor plugin settings'),
172       '#attributes' => [
173         'id' => 'ckeditor-plugin-settings',
174       ],
175     ];
176     $this->ckeditorPluginManager->injectPluginSettingsForm($form, $form_state, $editor);
177     if (count(Element::children($form['plugins'])) === 0) {
178       unset($form['plugins']);
179       unset($form['plugin_settings']);
180     }
181
182     // Hidden CKEditor instance. We need a hidden CKEditor instance with all
183     // plugins enabled, so we can retrieve CKEditor's per-feature metadata (on
184     // which tags, attributes, styles and classes are enabled). This metadata is
185     // necessary for certain filters' (for instance, the html_filter filter)
186     // settings to be updated accordingly.
187     // Get a list of all external plugins and their corresponding files.
188     $plugins = array_keys($this->ckeditorPluginManager->getDefinitions());
189     $all_external_plugins = [];
190     foreach ($plugins as $plugin_id) {
191       $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
192       if (!$plugin->isInternal()) {
193         $all_external_plugins[$plugin_id] = $plugin->getFile();
194       }
195     }
196     // Get a list of all buttons that are provided by all plugins.
197     $all_buttons = array_reduce($this->ckeditorPluginManager->getButtons(), function ($result, $item) {
198       return array_merge($result, array_keys($item));
199     }, []);
200     // Build a fake Editor object, which we'll use to generate JavaScript
201     // settings for this fake Editor instance.
202     $fake_editor = Editor::create([
203       'format' => $editor->id(),
204       'editor' => 'ckeditor',
205       'settings' => [
206         // Single toolbar row, single button group, all existing buttons.
207         'toolbar' => [
208          'rows' => [
209            0 => [
210              0 => [
211                'name' => 'All existing buttons',
212                'items' => $all_buttons,
213              ],
214            ],
215          ],
216         ],
217         'plugins' => $settings['plugins'],
218       ],
219     ]);
220     $config = $this->getJSSettings($fake_editor);
221     // Remove the ACF configuration that is generated based on filter settings,
222     // because otherwise we cannot retrieve per-feature metadata.
223     unset($config['allowedContent']);
224     $form['hidden_ckeditor'] = [
225       '#markup' => '<div id="ckeditor-hidden" class="hidden"></div>',
226       '#attached' => [
227         'drupalSettings' => ['ckeditor' => ['hiddenCKEditorConfig' => $config]],
228       ],
229     ];
230
231     return $form;
232   }
233
234   /**
235    * {@inheritdoc}
236    */
237   public function settingsFormSubmit(array $form, FormStateInterface $form_state) {
238     // Modify the toolbar settings by reference. The values in
239     // $form_state->getValue(array('editor', 'settings')) will be saved directly
240     // by editor_form_filter_admin_format_submit().
241     $toolbar_settings = &$form_state->getValue(['editor', 'settings', 'toolbar']);
242
243     // The rows key is not built into the form structure, so decode the button
244     // groups data into this new key and remove the button_groups key.
245     $toolbar_settings['rows'] = json_decode($toolbar_settings['button_groups'], TRUE);
246     unset($toolbar_settings['button_groups']);
247
248     // Remove the plugin settings' vertical tabs state; no need to save that.
249     if ($form_state->hasValue(['editor', 'settings', 'plugins'])) {
250       $form_state->unsetValue(['editor', 'settings', 'plugin_settings']);
251     }
252   }
253
254   /**
255    * {@inheritdoc}
256    */
257   public function getJSSettings(Editor $editor) {
258     $settings = [];
259
260     // Get the settings for all enabled plugins, even the internal ones.
261     $enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor, TRUE));
262     foreach ($enabled_plugins as $plugin_id) {
263       $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
264       $settings += $plugin->getConfig($editor);
265     }
266
267     // Fall back on English if no matching language code was found.
268     $display_langcode = 'en';
269
270     // Map the interface language code to a CKEditor translation if interface
271     // translation is enabled.
272     if ($this->moduleHandler->moduleExists('locale')) {
273       $ckeditor_langcodes = $this->getLangcodes();
274       $language_interface = $this->languageManager->getCurrentLanguage();
275       if (isset($ckeditor_langcodes[$language_interface->getId()])) {
276         $display_langcode = $ckeditor_langcodes[$language_interface->getId()];
277       }
278     }
279
280     // Next, set the most fundamental CKEditor settings.
281     $external_plugin_files = $this->ckeditorPluginManager->getEnabledPluginFiles($editor);
282     $settings += [
283       'toolbar' => $this->buildToolbarJSSetting($editor),
284       'contentsCss' => $this->buildContentsCssJSSetting($editor),
285       'extraPlugins' => implode(',', array_keys($external_plugin_files)),
286       'language' => $display_langcode,
287       // Configure CKEditor to not load styles.js. The StylesCombo plugin will
288       // set stylesSet according to the user's settings, if the "Styles" button
289       // is enabled. We cannot get rid of this until CKEditor will stop loading
290       // styles.js by default.
291       // See http://dev.ckeditor.com/ticket/9992#comment:9.
292       'stylesSet' => FALSE,
293     ];
294
295     // Finally, set Drupal-specific CKEditor settings.
296     $root_relative_file_url = function ($uri) {
297       return file_url_transform_relative(file_create_url($uri));
298     };
299     $settings += [
300       'drupalExternalPlugins' => array_map($root_relative_file_url, $external_plugin_files),
301     ];
302
303     // Parse all CKEditor plugin JavaScript files for translations.
304     if ($this->moduleHandler->moduleExists('locale')) {
305       locale_js_translate(array_values($external_plugin_files));
306     }
307
308     ksort($settings);
309
310     return $settings;
311   }
312
313   /**
314    * Returns a list of language codes supported by CKEditor.
315    *
316    * @return array
317    *   An associative array keyed by language codes.
318    */
319   public function getLangcodes() {
320     // Cache the file system based language list calculation because this would
321     // be expensive to calculate all the time. The cache is cleared on core
322     // upgrades which is the only situation the CKEditor file listing should
323     // change.
324     $langcode_cache = \Drupal::cache()->get('ckeditor.langcodes');
325     if (!empty($langcode_cache)) {
326       $langcodes = $langcode_cache->data;
327     }
328     if (empty($langcodes)) {
329       $langcodes = [];
330       // Collect languages included with CKEditor based on file listing.
331       $files = scandir('core/assets/vendor/ckeditor/lang');
332       foreach ($files as $file) {
333         if ($file[0] !== '.' && preg_match('/\.js$/', $file)) {
334           $langcode = basename($file, '.js');
335           $langcodes[$langcode] = $langcode;
336         }
337       }
338       \Drupal::cache()->set('ckeditor.langcodes', $langcodes);
339     }
340
341     // Get language mapping if available to map to Drupal language codes.
342     // This is configurable in the user interface and not expensive to get, so
343     // we don't include it in the cached language list.
344     $language_mappings = $this->moduleHandler->moduleExists('language') ? language_get_browser_drupal_langcode_mappings() : [];
345     foreach ($langcodes as $langcode) {
346       // If this language code is available in a Drupal mapping, use that to
347       // compute a possibility for matching from the Drupal langcode to the
348       // CKEditor langcode.
349       // For instance, CKEditor uses the langcode 'no' for Norwegian, Drupal
350       // uses 'nb'. This would then remove the 'no' => 'no' mapping and replace
351       // it with 'nb' => 'no'. Now Drupal knows which CKEditor translation to
352       // load.
353       if (isset($language_mappings[$langcode]) && !isset($langcodes[$language_mappings[$langcode]])) {
354         $langcodes[$language_mappings[$langcode]] = $langcode;
355         unset($langcodes[$langcode]);
356       }
357     }
358
359     return $langcodes;
360   }
361
362   /**
363    * {@inheritdoc}
364    */
365   public function getLibraries(Editor $editor) {
366     $libraries = [
367       'ckeditor/drupal.ckeditor',
368     ];
369
370     // Get the required libraries for any enabled plugins.
371     $enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor));
372     foreach ($enabled_plugins as $plugin_id) {
373       $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
374       $additional_libraries = array_diff($plugin->getLibraries($editor), $libraries);
375       $libraries = array_merge($libraries, $additional_libraries);
376     }
377
378     return $libraries;
379   }
380
381   /**
382    * Builds the "toolbar" configuration part of the CKEditor JS settings.
383    *
384    * @see getJSSettings()
385    *
386    * @param \Drupal\editor\Entity\Editor $editor
387    *   A configured text editor object.
388    * @return array
389    *   An array containing the "toolbar" configuration.
390    */
391   public function buildToolbarJSSetting(Editor $editor) {
392     $toolbar = [];
393
394     $settings = $editor->getSettings();
395     foreach ($settings['toolbar']['rows'] as $row) {
396       foreach ($row as $group) {
397         $toolbar[] = $group;
398       }
399       $toolbar[] = '/';
400     }
401     return $toolbar;
402   }
403
404   /**
405    * Builds the "contentsCss" configuration part of the CKEditor JS settings.
406    *
407    * @see getJSSettings()
408    *
409    * @param \Drupal\editor\Entity\Editor $editor
410    *   A configured text editor object.
411    * @return array
412    *   An array containing the "contentsCss" configuration.
413    */
414   public function buildContentsCssJSSetting(Editor $editor) {
415     $css = [
416       drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css',
417       drupal_get_path('module', 'system') . '/css/components/align.module.css',
418     ];
419     $this->moduleHandler->alter('ckeditor_css', $css, $editor);
420     // Get a list of all enabled plugins' iframe instance CSS files.
421     $plugins_css = array_reduce($this->ckeditorPluginManager->getCssFiles($editor), function ($result, $item) {
422       return array_merge($result, array_values($item));
423     }, []);
424     $css = array_merge($css, $plugins_css);
425     $css = array_merge($css, _ckeditor_theme_css());
426     $css = array_map('file_create_url', $css);
427     $css = array_map('file_url_transform_relative', $css);
428
429     return array_values($css);
430   }
431
432 }