<?php namespace SGalinski\Tinymce; /*************************************************************** * Copyright notice * * (c) sgalinski Internet Services (https://www.sgalinski.de) * * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is * free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * The GNU General Public License can be found at * http://www.gnu.org/copyleft/gpl.html. * * This script is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Lang\LanguageService; use TYPO3\CMS\Core\Localization\Locales; /** * tinyMCE initialisation class * * Usage: * * // @var Loader $tinyMceLoader * $tinyMceLoader = GeneralUtility::makeInstance('SGalinski\Tinymce\Loader'); * $tinyMceLoader->loadConfiguration($configurationFile) * $tinyMceLoader->loadJsViaPageRenderer($GLOBALS['TSFE']->getPageRenderer()); * * Basic Configuration: (= content of $configurationFile) * * tinymce.init({ * selector: 'textarea' * }); */ class Loader { /** * TinyMCE configuration * * @var array */ protected $tinymceConfiguration = []; /** * Initialization flag * * @var bool */ static protected $init = FALSE; /** * That's a map, which contains the differences in relation to the tinyMce languages. * * @var array */ protected $typo3LanguagesDifferencesToTinyMceLanguagesMap = [ 'bg' => 'bg_BG', 'fr' => 'fr_FR', 'fr_CA' => 'fr_FR', 'kl' => 'gl', 'he' => 'he_IL', 'hi' => 'hi_IN', 'hu' => 'hu_HU', 'is' => 'is_IS', 'km' => 'km_KH', 'no' => 'nb_NO', 'pt' => 'pt_PT', 'sl' => 'sl_SI', 'sv' => 'sv_SE', 'th' => 'th_TH', 'zh' => 'zh_TW', ]; /** * @param string $configuration file reference or configuration string (defaults to basic configuration) * @param boolean $forceLanguage set this to true if you want to force your language set by the configuration * * @return void * @throws \InvalidArgumentException */ public function loadConfiguration($configuration = '', $forceLanguage = FALSE) { self::$init = FALSE; $this->tinymceConfiguration = $this->prepareTinyMCEConfiguration($configuration); if (!$forceLanguage) { $this->setLanguage(); } } /** * Calculates and sets the current language * * @return void * @throws \InvalidArgumentException */ protected function setLanguage() { /** @var $languageInstance LanguageService */ $languageInstance = (TYPO3_MODE === 'FE' ? $GLOBALS['TSFE'] : $GLOBALS['LANG']); $languageKey = $languageInstance->lang; if (TYPO3_MODE === 'BE') { $groupOrUserProps = BackendUtility::getModTSconfig('', 'tx_tinyMCE'); if (trim($groupOrUserProps['properties']['prefLang']) !== '') { $languageKey = $groupOrUserProps['properties']['prefLang']; } } // language conversion from TLD to iso631 $locales = GeneralUtility::makeInstance(Locales::class); Locales::initialize(); $isoArray = (array) $locales->getIsoMapping(); if (array_key_exists($languageKey, $isoArray)) { $languageKey = $isoArray[$languageKey]; } if (array_key_exists($languageKey, $this->typo3LanguagesDifferencesToTinyMceLanguagesMap)) { $languageKey = $this->typo3LanguagesDifferencesToTinyMceLanguagesMap[$languageKey]; } $languageFile = PATH_site . ExtensionManagementUtility::siteRelPath('tinymce') . 'tinymce_node_modules/tinymce/langs/' . $languageKey . '.js'; if (!is_file($languageFile)) { $languageKey = 'en'; } $this->addConfigurationOption('language', $languageKey); } /** * Returns a file that contains the tinyMCE configuration * * Note: The load dom event cannot be used, because e.g. IRRE adds the javascript * later on. This leads to code that is never executed. The interval timer hack fixes this * issue. * * @return string * @throws \UnexpectedValueException */ protected function getConfiguration(): string { $configuration = $this->tinymceConfiguration['preJS']; $configuration .= ' var SG = SG || {}; SG.domIsReady = (function(domIsReady) { var isBrowserIeOrNot = function() { return (!document.attachEvent || typeof document.attachEvent === "undefined" ? "not-ie" : "ie"); } domIsReady = function(callback) { if(callback && typeof callback === "function"){ if(isBrowserIeOrNot() !== "ie") { document.addEventListener("DOMContentLoaded", function() { return callback(); }); } else { document.attachEvent("onreadystatechange", function() { if(document.readyState === "complete") { return callback(); } }); } } else { console.error("The callback is not a function!"); } } return domIsReady; })(SG.domIsReady || {}); SG.initTinyMceLoadFunction = function() { if (SG.initializedTinyMceLoaderInstance) { if (SG.initTinyMceLoadInterval) { clearInterval(SG.initTinyMceLoadInterval); } return; } if (SG.TinyMceLoader && window.tinymce && window.tinymce.init) { SG.initializedTinyMceLoaderInstance = new SG.TinyMceLoader(window.tinymce, { ' . $this->replaceTypo3Paths($this->tinymceConfiguration['configurationData']) . ' }); if (SG.initTinyMceLoadInterval) { clearInterval(SG.initTinyMceLoadInterval); } } }; SG.domIsReady(function() { SG.initTinyMceLoadFunction(); }); // the content ready event is not thrown if RTE fields are loaded via IRRE // so we need to check at least after some time if the function was really called SG.initTinyMceLoadInterval = window.setInterval(SG.initTinyMceLoadFunction, 1500); '; $configuration .= $this->tinymceConfiguration['postJS']; $filename = 'tinymceConfiguration' . sha1($configuration) . '.js'; $file = PATH_site . 'typo3temp/' . $filename; if (!is_file($file)) { file_put_contents($file, $configuration); GeneralUtility::fixPermissions($file); } return $this->getPath($file, TRUE); } /** * Returns the needed javascript inclusion code * * Note: This function can only be called once. * * @return string */ public function getJS(): string { $output = ''; if (!self::$init) { self::$init = TRUE; $pathToTinyMceExtension = ExtensionManagementUtility::extRelPath('tinymce'); $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'tinymce_node_modules/tinymce/tinymce.min.js'; $output = '<script type="text/javascript" src="' . $script . '"></script>'; $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/WeakMap/WeakMap.js'; $output .= '<script type="text/javascript" src="' . $script . '"></script>'; $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/MutationObserver/MutationObserver.js'; $output .= '<script type="text/javascript" src="' . $script . '"></script>'; $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Resources/Public/JavaScript/Loader.js'; $output .= '<script type="text/javascript" src="' . $script . '"></script>'; $script = $this->getConfiguration(); $output .= '<script type="text/javascript" src="' . $script . '"></script>'; } return $output; } /** * Loads the required javascript via the given page renderer instance * * Note: This function can only be called once. * * @param PageRenderer $pageRenderer * @return void */ public function loadJsViaPageRenderer(PageRenderer $pageRenderer) { if (self::$init) { return; } self::$init = TRUE; $pathToTinyMceExtension = ExtensionManagementUtility::extRelPath('tinymce'); $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'tinymce_node_modules/tinymce/tinymce.min.js'; $pageRenderer->addJsLibrary('tinymce', $script, 'text/javascript', FALSE, TRUE, '', TRUE); $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/MutationObserver/MutationObserver.js'; $pageRenderer->addJsLibrary('MutationObserver', $script, 'text/javascript', FALSE, TRUE, '', TRUE); $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/WeakMap/WeakMap.js'; $pageRenderer->addJsLibrary('WeakMap', $script, 'text/javascript', FALSE, TRUE, '', TRUE); $script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Resources/Public/JavaScript/Loader.js'; $pageRenderer->addJsFile($script, 'text/javascript', FALSE, TRUE, '', TRUE); $script = $this->getConfiguration(); $pageRenderer->addJsFile($script, 'text/javascript', FALSE, TRUE, '', TRUE); } /** * Loads the required javascript via the require.js * * @return array * @throws \BadFunctionCallException * @throws \UnexpectedValueException * @see \SGalinski\Tinymce4Rte\Form\Element\RichTextElement->loadRequireModulesForRTE */ public function loadJsViaRequireJS(): array { if (self::$init) { return []; } self::$init = TRUE; $pathToTinyMceExtension = ExtensionManagementUtility::extPath('tinymce'); $tinymceSource = $pathToTinyMceExtension . 'tinymce_node_modules/tinymce/tinymce.min.js'; $configuration = $this->tinymceConfiguration['preJS']; $configuration .= ' var $ = jQuery = window.TYPO3.jQuery; var RTEarea = RTEarea || window.RTEarea; define([\'TYPO3/CMS/Tinymce/../../../../typo3conf/ext/tinymce/tinymce_node_modules/tinymce/jquery.tinymce.min.js\'], function () { $(\'.tinymce4_rte#RTEarea' . strtr($this->tinymceConfiguration['configurationDataArray']['editornumber'], array('.' => '\\\\.', '\'' => '')) . '\').tinymce({ script_url : \'' . $this->getPath($tinymceSource, TRUE) . '\', ' . $this->replaceTypo3Paths($this->tinymceConfiguration['configurationData']) . ', selector: \'.tinymce4_rte#RTEarea' . strtr($this->tinymceConfiguration['configurationDataArray']['editornumber'], array('.' => '\\\\.', '\'' => '')) . '\' }); $(\'#t3js-ui-block\').remove(); }); '; $configuration .= $this->tinymceConfiguration['postJS']; $filename = 'tinymceConfiguration' . sha1($configuration) . '.js'; $file = PATH_site . 'typo3temp/' . $filename; if (!is_file($file)) { file_put_contents($file, $configuration); GeneralUtility::fixPermissions($file); } return [$this->getPath($file, TRUE)]; } /** * Parses and processes the tinyMCE configuration * * @param string $configuration file reference or configuration string * @return array */ protected function prepareTinyMCEConfiguration($configuration): array { $configurationArray = []; // try to resolve a potential TYPO3 file path $configurationFile = GeneralUtility::getFileAbsFileName($configuration); if (is_file($configurationFile)) { $configuration = file_get_contents($configurationFile); } // first try to find the configuration via the "subpart" ###TINYMCE_INIT### $pattern = '/(.*)?tinymce\.init\s*\(\s*\{\s*\/\*\s?###TINYMCE_INIT###.*?\*\/(.*)\/\*\s*###TINYMCE_INIT###.*?\*\/\s*\}\s*\);*(.*)?/is'; if (@preg_match($pattern, $configuration, $matches)) { // fine :) } else { // if nothing is found, try it the legacy way (note: this may cause problems with a complex setups, since parenthesis-matching is not perfect here) $pattern = '/(.*)tinymce\.init\s*\(\s*\{(.*?)\}\s*\)\s*;?(.*)/is'; preg_match($pattern, $configuration, $matches); } // add preJS and postJS $configurationArray['preJS'] = trim($matches[1]); $configurationArray['configurationData'] = trim($matches[2]); $configurationArray['postJS'] = trim($matches[3]); return $configurationArray; } /** * Adds a basic configuration value to the parsed configuration * * @param string $key * @param mixed $value */ public function addConfigurationOption($key, $value) { if (is_numeric($value)) { if (strpos($value, '.')) { $value = (float) $value; } else { $value = (int) $value; } } elseif (strpos(trim($value), '[') === FALSE && strpos(trim($value), '{') === FALSE && strpos(trim($value), 'function') === FALSE ) { $value = '\'' . $value . '\''; } if ($this->tinymceConfiguration['configurationData'] !== '') { $this->tinymceConfiguration['configurationData'] .= "\n,"; } $this->tinymceConfiguration['configurationData'] .= $key . ': ' . $value; $this->tinymceConfiguration['configurationDataArray'][$key] = $value; } /** * Replaces any TYPO3 extension path with the domain prefixed one. * * @param string $configuration * @return string */ protected function replaceTypo3Paths($configuration): string { $replacementFunction = function ($value) { // getPath should be used, but this causes a php exception with PHP 5.3 as $this isn't set there return '\'' . GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . str_replace(PATH_site, '', GeneralUtility::getFileAbsFileName($value[1])) . '\''; }; return preg_replace_callback('/["\'](EXT:[^"\']*)["\']/is', $replacementFunction, $configuration); } /** * Resolves a relative path like EXT:tinymce/... into an absolute one that contains either the * current host or the path to the file in the file system. * * @param string $relativePath * @param bool $returnWithDomain * * @return string * @throws \UnexpectedValueException */ protected function getPath($relativePath, $returnWithDomain = FALSE): string { $finalPath = $absolutePath = GeneralUtility::getFileAbsFileName($relativePath); if ($returnWithDomain) { $finalPath = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . str_replace(PATH_site, '', $absolutePath); } return $finalPath; } }