* @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Editors\TinyMCE\PluginTraits; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Session\Session; use Joomla\CMS\Uri\Uri; use Joomla\Registry\Registry; use stdClass; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Handles the onDisplay event for the TinyMCE editor. * * @since 4.1.0 */ trait DisplayTrait { use GlobalFilters; use KnownButtons; use ResolveFiles; use ToolbarPresets; use XTDButtons; /** * Display the editor area. * * @param string $name The name of the editor area. * @param string $content The content of the field. * @param string $width The width of the editor area. * @param string $height The height of the editor area. * @param int $col The number of columns for the editor area. * @param int $row The number of rows for the editor area. * @param boolean $buttons True and the editor buttons will be displayed. * @param string $id An optional ID for the textarea. If not supplied the name is used. * @param string $asset The object asset * @param object $author The author. * @param array $params Associative array of editor parameters. * * @return string */ public function onDisplay( $name, $content, $width, $height, $col, $row, $buttons = true, $id = null, $asset = null, $author = null, $params = [] ) { $id = empty($id) ? $name : $id; $user = $this->app->getIdentity(); $language = $this->app->getLanguage(); $doc = $this->app->getDocument(); $id = preg_replace('/(\s|[^A-Za-z0-9_])+/', '_', $id); $nameGroup = explode('[', preg_replace('/\[\]|\]/', '', $name)); $fieldName = end($nameGroup); $scriptOptions = []; $externalPlugins = []; $options = $doc->getScriptOptions('plg_editor_tinymce'); $theme = 'silver'; // Data object for the layout $textarea = new stdClass(); $textarea->name = $name; $textarea->id = $id; $textarea->class = 'mce_editable joomla-editor-tinymce'; $textarea->cols = $col; $textarea->rows = $row; $textarea->width = is_numeric($width) ? $width . 'px' : $width; $textarea->height = is_numeric($height) ? $height . 'px' : $height; $textarea->content = $content; $textarea->readonly = !empty($params['readonly']); // Render Editor markup $editor = '
'; $editor .= LayoutHelper::render('joomla.tinymce.textarea', $textarea); $editor .= !$this->app->client->mobile ? LayoutHelper::render('joomla.tinymce.togglebutton') : ''; $editor .= '
'; // Prepare the instance specific options if (empty($options['tinyMCE'][$fieldName])) { $options['tinyMCE'][$fieldName] = []; } // Width and height if ($width && empty($options['tinyMCE'][$fieldName]['width'])) { $options['tinyMCE'][$fieldName]['width'] = $width; } if ($height && empty($options['tinyMCE'][$fieldName]['height'])) { $options['tinyMCE'][$fieldName]['height'] = $height; } // Set editor to readonly mode if (!empty($params['readonly'])) { $options['tinyMCE'][$fieldName]['readonly'] = 1; } // The ext-buttons if (empty($options['tinyMCE'][$fieldName]['joomlaExtButtons'])) { $btns = $this->tinyButtons($id, $buttons); $options['tinyMCE'][$fieldName]['joomlaMergeDefaults'] = true; $options['tinyMCE'][$fieldName]['joomlaExtButtons'] = $btns; } $doc->addScriptOptions('plg_editor_tinymce', $options, false); // Setup Default (common) options for the Editor script // Check whether we already have them if (!empty($options['tinyMCE']['default'])) { return $editor; } $ugroups = array_combine($user->getAuthorisedGroups(), $user->getAuthorisedGroups()); // Prepare the parameters $levelParams = new Registry(); $extraOptions = new stdClass(); $toolbarParams = new stdClass(); $extraOptionsAll = (array) $this->params->get('configuration.setoptions', []); $toolbarParamsAll = (array) $this->params->get('configuration.toolbars', []); // Sort the array in reverse, so the items with lowest access level goes first krsort($extraOptionsAll); // Get configuration depend from User group foreach ($extraOptionsAll as $set => $val) { $val = (object) $val; $val->access = empty($val->access) ? [] : $val->access; // Check whether User in one of allowed group foreach ($val->access as $group) { if (isset($ugroups[$group])) { $extraOptions = $val; $toolbarParams = (object) $toolbarParamsAll[$set]; } } } // load external plugins if (isset($extraOptions->external_plugins) && $extraOptions->external_plugins) { foreach (json_decode(json_encode($extraOptions->external_plugins), true) as $external) { // get the path for readability $path = $external['path']; // if we have a name and path, add it to the list if ($external['name'] != '' && $path != '') { $externalPlugins[$external['name']] = substr($path, 0, 1) == '/' ? Uri::root() . substr($path, 1) : $path; } } } // Merge the params $levelParams->loadObject($toolbarParams); $levelParams->loadObject($extraOptions); // Set the selected skin $skin = $levelParams->get($this->app->isClient('administrator') ? 'skin_admin' : 'skin', 'oxide'); // Check that selected skin exists. $skin = Folder::exists(JPATH_ROOT . '/media/vendor/tinymce/skins/ui/' . $skin) ? $skin : 'oxide'; if (!$levelParams->get('lang_mode', 1)) { // Admin selected language $langPrefix = $levelParams->get('lang_code', 'en'); } else { // Reflect the current language if (file_exists(JPATH_ROOT . '/media/vendor/tinymce/langs/' . $language->getTag() . '.js')) { $langPrefix = $language->getTag(); } elseif (file_exists(JPATH_ROOT . '/media/vendor/tinymce/langs/' . substr($language->getTag(), 0, strpos($language->getTag(), '-')) . '.js')) { $langPrefix = substr($language->getTag(), 0, strpos($language->getTag(), '-')); } else { $langPrefix = 'en'; } } $use_content_css = $levelParams->get('content_css', 1); $content_css_custom = $levelParams->get('content_css_custom', ''); $content_css = null; // Loading of css file for 'styles' dropdown if ($content_css_custom) { /** * If URL, just pass it to $content_css * else, assume it is a file name in the current template folder */ $content_css = strpos($content_css_custom, 'http') !== false ? $content_css_custom : $this->includeRelativeFiles('css', $content_css_custom); } else { // Process when use_content_css is Yes and no custom file given $content_css = $use_content_css ? $this->includeRelativeFiles('css', 'editor' . (JDEBUG ? '' : '.min') . '.css') : $content_css; } $ignore_filter = false; // Text filtering if ($levelParams->get('use_config_textfilters', 0)) { // Use filters from com_config $filter = static::getGlobalFilters($user); $ignore_filter = $filter === false; $blockedTags = !empty($filter->blockedTags) ? $filter->blockedTags : []; $blockedAttributes = !empty($filter->blockedAttributes) ? $filter->blockedAttributes : []; $tagArray = !empty($filter->tagsArray) ? $filter->tagsArray : []; $attrArray = !empty($filter->attrArray) ? $filter->attrArray : []; $invalid_elements = implode(',', array_merge($blockedTags, $blockedAttributes, $tagArray, $attrArray)); // Valid elements are all entries listed as allowed in com_config, which are now missing in the filter blocked properties $default_filter = InputFilter::getInstance(); $valid_elements = implode(',', array_diff($default_filter->blockedTags, $blockedTags)); $extended_elements = ''; } else { // Use filters from TinyMCE params $invalid_elements = trim($levelParams->get('invalid_elements', 'script,applet,iframe')); $extended_elements = trim($levelParams->get('extended_elements', '')); $valid_elements = trim($levelParams->get('valid_elements', '')); } // The param is true for vertical resizing only, false or both $resizing = (bool) $levelParams->get('resizing', true); $resize_horizontal = (bool) $levelParams->get('resize_horizontal', true); if ($resizing && $resize_horizontal) { $resizing = 'both'; } // Set of always available plugins $plugins = [ 'autolink', 'lists', 'importcss', 'quickbars', ]; // Allowed elements $elements = [ 'hr[id|title|alt|class|width|size|noshade]', ]; $elements = $extended_elements ? array_merge($elements, explode(',', $extended_elements)) : $elements; // Prepare the toolbar/menubar $knownButtons = static::getKnownButtons(); // Check if there no value at all if (!$levelParams->get('menu') && !$levelParams->get('toolbar1') && !$levelParams->get('toolbar2')) { // Get from preset $presets = static::getToolbarPreset(); /** * Predefine group as: * Set 0: for Administrator, Editor, Super Users (4,7,8) * Set 1: for Registered, Manager (2,6), all else are public */ switch (true) { case isset($ugroups[4]) || isset($ugroups[7]) || isset($ugroups[8]): $preset = $presets['advanced']; break; case isset($ugroups[2]) || isset($ugroups[6]): $preset = $presets['medium']; break; default: $preset = $presets['simple']; } $levelParams->loadArray($preset); } $menubar = (array) $levelParams->get('menu', []); $toolbar1 = (array) $levelParams->get('toolbar1', []); $toolbar2 = (array) $levelParams->get('toolbar2', []); // Make an easy way to check which button is enabled $allButtons = array_merge($toolbar1, $toolbar2); $allButtons = array_combine($allButtons, $allButtons); // Check for button-specific plugins foreach ($allButtons as $btnName) { if (!empty($knownButtons[$btnName]['plugin'])) { $plugins[] = $knownButtons[$btnName]['plugin']; } } // Template $templates = []; if (!empty($allButtons['template'])) { // Do we have a custom content_template_path $template_path = $levelParams->get('content_template_path'); $template_path = $template_path ? '/templates/' . $template_path : '/media/vendor/tinymce/templates'; $filepaths = Folder::exists(JPATH_ROOT . $template_path) ? Folder::files(JPATH_ROOT . $template_path, '\.(html|txt)$', false, true) : []; foreach ($filepaths as $filepath) { $fileinfo = pathinfo($filepath); $filename = $fileinfo['filename']; $full_filename = $fileinfo['basename']; if ($filename === 'index') { continue; } $title = $filename; $title_upper = strtoupper($filename); $description = ' '; if ($language->hasKey('PLG_TINY_TEMPLATE_' . $title_upper . '_TITLE')) { $title = Text::_('PLG_TINY_TEMPLATE_' . $title_upper . '_TITLE'); } if ($language->hasKey('PLG_TINY_TEMPLATE_' . $title_upper . '_DESC')) { $description = Text::_('PLG_TINY_TEMPLATE_' . $title_upper . '_DESC'); } $templates[] = [ 'title' => $title, 'description' => $description, 'url' => Uri::root(true) . $template_path . '/' . $full_filename, ]; } } // Check for extra plugins, from the setoptions form foreach (['wordcount' => 1, 'advlist' => 1, 'autosave' => 1, 'textpattern' => 0] as $pName => $def) { if ($levelParams->get($pName, $def)) { $plugins[] = $pName; } } // Use CodeMirror in the code view instead of plain text to provide syntax highlighting if ($levelParams->get('sourcecode', 1)) { $externalPlugins['highlightPlus'] = HTMLHelper::_('script', 'plg_editors_tinymce/plugins/highlighter/plugin-es5.min.js', ['relative' => true, 'version' => 'auto', 'pathOnly' => true]); } $dragdrop = $levelParams->get('drag_drop', 1); if ($dragdrop && $user->authorise('core.create', 'com_media')) { $externalPlugins['jdragndrop'] = HTMLHelper::_('script', 'plg_editors_tinymce/plugins/dragdrop/plugin.min.js', ['relative' => true, 'version' => 'auto', 'pathOnly' => true]); $uploadUrl = Uri::base(false) . 'index.php?option=com_media&format=json&url=1&task=api.files'; $uploadUrl = $this->app->isClient('site') ? htmlentities($uploadUrl, ENT_NOQUOTES, 'UTF-8', false) : $uploadUrl; Text::script('PLG_TINY_ERR_UNSUPPORTEDBROWSER'); Text::script('ERROR'); Text::script('PLG_TINY_DND_ADDITIONALDATA'); Text::script('PLG_TINY_DND_ALTTEXT'); Text::script('PLG_TINY_DND_LAZYLOADED'); Text::script('PLG_TINY_DND_EMPTY_ALT'); $scriptOptions['parentUploadFolder'] = $levelParams->get('path', ''); $scriptOptions['csrfToken'] = Session::getFormToken(); $scriptOptions['uploadUri'] = $uploadUrl; // @TODO have a way to select the adapter, similar to $levelParams->get('path', ''); $scriptOptions['comMediaAdapter'] = 'local-images:'; } // Convert pt to px in dropdown $scriptOptions['fontsize_formats'] = '8px 10px 12px 14px 18px 24px 36px'; // select the languages for the "language of parts" menu if (isset($extraOptions->content_languages) && $extraOptions->content_languages) { foreach (json_decode(json_encode($extraOptions->content_languages), true) as $content_language) { // if we have a language name and a language code then add to the menu if ($content_language['content_language_name'] != '' && $content_language['content_language_code'] != '') { $ctemp[] = array('title' => $content_language['content_language_name'], 'code' => $content_language['content_language_code']); } } $scriptOptions['content_langs'] = array_merge($ctemp); } // User custom plugins and buttons $custom_plugin = trim($levelParams->get('custom_plugin', '')); $custom_button = trim($levelParams->get('custom_button', '')); if ($custom_plugin) { $plugins = array_merge($plugins, explode(strpos($custom_plugin, ',') !== false ? ',' : ' ', $custom_plugin)); } if ($custom_button) { $toolbar1 = array_merge($toolbar1, explode(strpos($custom_button, ',') !== false ? ',' : ' ', $custom_button)); } // Merge the two toolbars for backwards compatibility $toolbar = array_merge($toolbar1, $toolbar2); // Build the final options set $scriptOptions = array_merge( $scriptOptions, [ 'deprecation_warnings' => JDEBUG ? true : false, 'suffix' => JDEBUG ? '' : '.min', 'baseURL' => Uri::root(true) . '/media/vendor/tinymce', 'directionality' => $language->isRtl() ? 'rtl' : 'ltr', 'language' => $langPrefix, 'autosave_restore_when_empty' => false, 'skin' => $skin, 'theme' => $theme, 'schema' => 'html5', // Toolbars 'menubar' => empty($menubar) ? false : implode(' ', array_unique($menubar)), 'toolbar' => empty($toolbar) ? null : 'jxtdbuttons ' . implode(' ', $toolbar), 'plugins' => implode(',', array_unique($plugins)), // Quickbars 'quickbars_image_toolbar' => false, 'quickbars_insert_toolbar' => false, 'quickbars_selection_toolbar' => 'bold italic underline | H2 H3 | link blockquote', // Cleanup/Output 'browser_spellcheck' => true, 'entity_encoding' => $levelParams->get('entity_encoding', 'raw'), 'verify_html' => !$ignore_filter, 'paste_as_text' => (bool) $levelParams->get('paste_as_text', false), 'valid_elements' => $valid_elements, 'extended_valid_elements' => implode(',', $elements), 'invalid_elements' => $invalid_elements, // URL 'relative_urls' => (bool) $levelParams->get('relative_urls', true), 'remove_script_host' => false, // Drag and drop Images always FALSE, reverting this allows for inlining the images 'paste_data_images' => false, // Layout 'content_css' => $content_css, 'document_base_url' => Uri::root(true) . '/', 'image_caption' => true, 'importcss_append' => true, 'height' => $this->params->get('html_height', '550px'), 'width' => $this->params->get('html_width', ''), 'elementpath' => (bool) $levelParams->get('element_path', true), 'resize' => $resizing, 'templates' => $templates, 'external_plugins' => empty($externalPlugins) ? null : $externalPlugins, 'contextmenu' => (bool) $levelParams->get('contextmenu', true) ? null : false, 'toolbar_sticky' => true, 'toolbar_mode' => $levelParams->get('toolbar_mode', 'sliding'), // Image plugin options 'a11y_advanced_options' => true, 'image_advtab' => (bool) $levelParams->get('image_advtab', false), 'image_title' => true, // Drag and drop specific 'dndEnabled' => $dragdrop, // Disable TinyMCE Branding 'branding' => false, ] ); if ($levelParams->get('newlines')) { // Break $scriptOptions['force_br_newlines'] = true; $scriptOptions['forced_root_block'] = ''; } else { // Paragraph $scriptOptions['force_br_newlines'] = false; $scriptOptions['forced_root_block'] = 'p'; } $scriptOptions['rel_list'] = [ ['title' => 'None', 'value' => ''], ['title' => 'Alternate', 'value' => 'alternate'], ['title' => 'Author', 'value' => 'author'], ['title' => 'Bookmark', 'value' => 'bookmark'], ['title' => 'Help', 'value' => 'help'], ['title' => 'License', 'value' => 'license'], ['title' => 'Lightbox', 'value' => 'lightbox'], ['title' => 'Next', 'value' => 'next'], ['title' => 'No Follow', 'value' => 'nofollow'], ['title' => 'No Referrer', 'value' => 'noreferrer'], ['title' => 'Prefetch', 'value' => 'prefetch'], ['title' => 'Prev', 'value' => 'prev'], ['title' => 'Search', 'value' => 'search'], ['title' => 'Tag', 'value' => 'tag'], ]; $scriptOptions['style_formats'] = [ [ 'title' => Text::_('PLG_TINY_MENU_CONTAINER'), 'items' => [ ['title' => 'article', 'block' => 'article', 'wrapper' => true, 'merge_siblings' => false], ['title' => 'aside', 'block' => 'aside', 'wrapper' => true, 'merge_siblings' => false], ['title' => 'section', 'block' => 'section', 'wrapper' => true, 'merge_siblings' => false], ], ], ]; $scriptOptions['style_formats_merge'] = true; $options['tinyMCE']['default'] = $scriptOptions; $doc->addScriptOptions('plg_editor_tinymce', $options); return $editor; } }