[BUGFIX] T3Editor getPlugins must write to response object
[Packages/TYPO3.CMS.git] / typo3 / sysext / t3editor / Classes / T3editor.php
1 <?php
2 namespace TYPO3\CMS\T3editor;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Core\Http\Response;
20 use TYPO3\CMS\Core\Page\PageRenderer;
21 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\PathUtility;
24
25 /**
26 * Provides a javascript-driven code editor with syntax highlighting for TS, HTML, CSS and more
27 */
28 class T3editor implements \TYPO3\CMS\Core\SingletonInterface
29 {
30 const MODE_TYPOSCRIPT = 'typoscript';
31 const MODE_JAVASCRIPT = 'javascript';
32 const MODE_CSS = 'css';
33 const MODE_XML = 'xml';
34 const MODE_HTML = 'html';
35 const MODE_PHP = 'php';
36 const MODE_SPARQL = 'sparql';
37 const MODE_MIXED = 'mixed';
38
39 /**
40 * @var string
41 */
42 protected $mode = '';
43
44 /**
45 * @var string
46 */
47 protected $ajaxSaveType = '';
48
49 /**
50 * Counts the editors on the current page
51 *
52 * @var int
53 */
54 protected $editorCounter = 0;
55
56 /**
57 * Path to EXT:t3editor
58 *
59 * @var string
60 */
61 protected $extPath = '';
62
63 /**
64 * Relative directory to codemirror
65 *
66 * @var string
67 */
68 protected $codemirrorPath = 'Resources/Public/JavaScript/Contrib/codemirror/js/';
69
70 /**
71 * RequireJS modules loaded for code completion
72 *
73 * @var array
74 */
75 protected $codeCompletionComponents = ['TsRef', 'CompletionResult', 'TsParser', 'TsCodeCompletion'];
76
77 /**
78 * sets the type of code to edit (::MODE_TYPOSCRIPT, ::MODE_JAVASCRIPT)
79 *
80 * @param $mode string Expects one of the predefined constants
81 * @return \TYPO3\CMS\T3editor\T3editor
82 */
83 public function setMode($mode)
84 {
85 $this->mode = $mode;
86 return $this;
87 }
88
89 /**
90 * Set the AJAX save type
91 *
92 * @param string $ajaxSaveType
93 * @return \TYPO3\CMS\T3editor\T3editor
94 */
95 public function setAjaxSaveType($ajaxSaveType)
96 {
97 $this->ajaxSaveType = $ajaxSaveType;
98 return $this;
99 }
100
101 /**
102 * Set mode by file
103 *
104 * @param string $file
105 * @return void
106 */
107 public function setModeByFile($file)
108 {
109 $fileInfo = GeneralUtility::split_fileref($file);
110 $this->setModeByType($fileInfo['fileext']);
111 }
112
113 /**
114 * Set mode by type
115 *
116 * @param string $type
117 * @return void
118 */
119 public function setModeByType($type)
120 {
121 switch ($type) {
122 case 'html':
123 case 'htm':
124 case 'tmpl':
125 $mode = self::MODE_HTML;
126 break;
127 case 'js':
128 $mode = self::MODE_JAVASCRIPT;
129 break;
130 case 'xml':
131 case 'svg':
132 $mode = self::MODE_XML;
133 break;
134 case 'css':
135 $mode = self::MODE_CSS;
136 break;
137 case 'ts':
138 $mode = self::MODE_TYPOSCRIPT;
139 break;
140 case 'sparql':
141 $mode = self::MODE_SPARQL;
142 break;
143 case 'php':
144 case 'phpsh':
145 case 'inc':
146 $mode = self::MODE_PHP;
147 break;
148 default:
149 $mode = self::MODE_MIXED;
150 }
151 $this->setMode($mode);
152 }
153
154 /**
155 * Get mode
156 *
157 * @return string
158 */
159 public function getMode()
160 {
161 return $this->mode;
162 }
163
164 /**
165 * Creates a new instance of the class
166 */
167 public function __construct()
168 {
169 $GLOBALS['LANG']->includeLLFile('EXT:t3editor/Resources/Private/Language/locallang.xlf');
170 // Disable pmktextarea to avoid conflicts (thanks Peter Klein for this suggestion)
171 $GLOBALS['BE_USER']->uc['disablePMKTextarea'] = 1;
172
173 $this->extPath = PathUtility::getAbsoluteWebPath(ExtensionManagementUtility::extPath('t3editor'));
174 $this->codemirrorPath = $this->extPath . $this->codemirrorPath;
175 }
176
177 /**
178 * Retrieves JavaScript code (header part) for editor
179 *
180 * @return string
181 */
182 public function getJavascriptCode()
183 {
184 /** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */
185 $pageRenderer = $this->getPageRenderer();
186 $pageRenderer->addCssFile($this->extPath . 'Resources/Public/Css/t3editor.css');
187 // Include editor-js-lib
188 $pageRenderer->addJsLibrary('codemirror', $this->codemirrorPath . 'codemirror.js');
189 if ($this->mode === self::MODE_TYPOSCRIPT) {
190 foreach ($this->codeCompletionComponents as $codeCompletionComponent) {
191 $pageRenderer->loadRequireJsModule('TYPO3/CMS/T3editor/Plugins/CodeCompletion/' . $codeCompletionComponent);
192 }
193 }
194 $pageRenderer->loadRequireJsModule('TYPO3/CMS/T3editor/T3editor');
195 return '';
196 }
197
198 /**
199 * Get the template code, prepared for javascript (no line breaks, quoted in single quotes)
200 *
201 * @return string The template code, prepared to use in javascript
202 */
203 protected function getPreparedTemplate()
204 {
205 $T3editor_template = file_get_contents(
206 GeneralUtility::getFileAbsFileName('EXT:t3editor/Resources/Private/Templates/t3editor.html')
207 );
208 return str_replace([CR, LF], '', $T3editor_template);
209 }
210
211 /**
212 * Determine the correct parser js file for given mode
213 *
214 * @param string $mode
215 * @return string Parser file name
216 */
217 protected function getParserfileByMode($mode)
218 {
219 switch ($mode) {
220 case self::MODE_TYPOSCRIPT:
221 $relPath = '../../../parse_typoscript/';
222 $parserfile = [$relPath . 'tokenizetyposcript.js', $relPath . 'parsetyposcript.js'];
223 break;
224 case self::MODE_JAVASCRIPT:
225 $parserfile = ['tokenizejavascript.js', 'parsejavascript.js'];
226 break;
227 case self::MODE_CSS:
228 $parserfile = ['parsecss.js'];
229 break;
230 case self::MODE_XML:
231 $parserfile = ['parsexml.js'];
232 break;
233 case self::MODE_SPARQL:
234 $parserfile = ['parsesparql.js'];
235 break;
236 case self::MODE_HTML:
237 $parserfile = ['tokenizejavascript.js', 'parsejavascript.js', 'parsecss.js', 'parsexml.js', 'parsehtmlmixed.js'];
238 break;
239 case self::MODE_PHP:
240 case self::MODE_MIXED:
241 $parserfile = ['tokenizejavascript.js', 'parsejavascript.js', 'parsecss.js', 'parsexml.js', '../contrib/php/js/tokenizephp.js', '../contrib/php/js/parsephp.js', '../contrib/php/js/parsephphtmlmixed.js'];
242 break;
243 default:
244 $parserfile = [];
245 }
246 return json_encode($parserfile);
247 }
248
249 /**
250 * Determine the correct css file for given mode
251 *
252 * @param string $mode
253 * @return string css file name
254 */
255 protected function getStylesheetByMode($mode)
256 {
257 switch ($mode) {
258 case self::MODE_TYPOSCRIPT:
259 $stylesheet = [$this->extPath . 'Resources/Public/Css/t3editor_typoscript_colors.css'];
260 break;
261 case self::MODE_JAVASCRIPT:
262 $stylesheet = [$this->codemirrorPath . '../css/jscolors.css'];
263 break;
264 case self::MODE_CSS:
265 $stylesheet = [$this->codemirrorPath . '../css/csscolors.css'];
266 break;
267 case self::MODE_XML:
268 $stylesheet = [$this->codemirrorPath . '../css/xmlcolors.css'];
269 break;
270 case self::MODE_HTML:
271 $stylesheet = [$this->codemirrorPath . '../css/xmlcolors.css', $this->codemirrorPath . '../css/jscolors.css', $this->codemirrorPath . '../css/csscolors.css'];
272 break;
273 case self::MODE_SPARQL:
274 $stylesheet = [$this->codemirrorPath . '../css/sparqlcolors.css'];
275 break;
276 case self::MODE_PHP:
277 $stylesheet = [$this->codemirrorPath . '../contrib/php/css/phpcolors.css'];
278 break;
279 case self::MODE_MIXED:
280 $stylesheet = [$this->codemirrorPath . '../css/xmlcolors.css', $this->codemirrorPath . '../css/jscolors.css', $this->codemirrorPath . '../css/csscolors.css', $this->codemirrorPath . '../contrib/php/css/phpcolors.css'];
281 break;
282 default:
283 $stylesheet = [];
284 }
285 $stylesheet[] = $this->extPath . 'Resources/Public/Css/t3editor_inner.css';
286 return json_encode($stylesheet);
287 }
288
289 /**
290 * Generates HTML with code editor
291 *
292 * @param string $name Name attribute of HTML tag
293 * @param string $class Class attribute of HTML tag
294 * @param string $content Content of the editor
295 * @param string $additionalParams Any additional editor parameters
296 * @param string $alt Alt attribute
297 * @param array $hiddenfields
298 * @return string Generated HTML code for editor
299 */
300 public function getCodeEditor($name, $class = '', $content = '', $additionalParams = '', $alt = '', array $hiddenfields = [])
301 {
302 $code = '';
303 $class .= ' t3editor';
304 $alt = trim($alt);
305 $code .=
306 '<div class="t3editor">'
307 . '<div class="t3e_wrap">'
308 . $this->getPreparedTemplate()
309 . '</div>'
310 . '<textarea '
311 . 'id="t3editor_' . (int)$this->editorCounter . '" '
312 . 'name="' . htmlspecialchars($name) . '" '
313 . 'class="' . htmlspecialchars($class) . '" '
314 . $additionalParams . ' '
315 . ($alt !== '' ? ' alt="' . htmlspecialchars($alt) . '"' : '')
316 . ' data-labels="' . htmlspecialchars(json_encode($GLOBALS['LANG']->getLabelsWithPrefix('js.', 'label_'))) . '"'
317 . ' data-instance-number="' . (int)$this->editorCounter . '"'
318 . ' data-editor-path="' . htmlspecialchars($this->extPath) . '"'
319 . ' data-codemirror-path="' . htmlspecialchars($this->codemirrorPath) . '"'
320 . ' data-ajaxsavetype="' . htmlspecialchars($this->ajaxSaveType) . '"'
321 . ' data-parserfile="' . htmlspecialchars($this->getParserfileByMode($this->mode)) . '"'
322 . ' data-stylesheet="' . htmlspecialchars($this->getStylesheetByMode($this->mode)) . '"'
323 . '>' . htmlspecialchars($content)
324 . '</textarea>'
325 . '</div>';
326 if (!empty($hiddenfields)) {
327 foreach ($hiddenfields as $name => $value) {
328 $code .= '<input type="hidden" ' . 'name="' . htmlspecialchars($name) . '" ' . 'value="' . htmlspecialchars($value) . '" />';
329 }
330 }
331 $this->editorCounter++;
332 return $code;
333 }
334
335 /**
336 * Save the content from t3editor retrieved via Ajax
337 *
338 * @param ServerRequestInterface $request
339 * @param ResponseInterface $response
340 * @return ResponseInterface
341 */
342 public function ajaxSaveCode(ServerRequestInterface $request, ResponseInterface $response)
343 {
344 // cancel if its not an Ajax request
345 if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX) {
346 $codeType = isset($request->getParsedBody()['t3editor_savetype']) ? $request->getParsedBody()['t3editor_savetype'] : $request->getQueryParams()['t3editor_savetype'];
347 $savingsuccess = false;
348 try {
349 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/t3editor/classes/class.tx_t3editor.php']['ajaxSaveCode'])) {
350 $_params = [
351 'pObj' => &$this,
352 'type' => $codeType,
353 'request' => $request,
354 'response' => $response
355 ];
356 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/t3editor/classes/class.tx_t3editor.php']['ajaxSaveCode'] as $key => $_funcRef) {
357 $savingsuccess = GeneralUtility::callUserFunction($_funcRef, $_params, $this) || $savingsuccess;
358 }
359 }
360 $responseContent = ['result' => $savingsuccess];
361 } catch (\Exception $e) {
362 $responseContent = [
363 'result' => false,
364 'exceptionMessage' => htmlspecialchars($e->getMessage()),
365 'exceptionCode' => $e->getCode()
366 ];
367 }
368 /** @var Response $response */
369 $response = GeneralUtility::makeInstance(Response::class);
370 $response->getBody()->write(json_encode($responseContent));
371 }
372
373 return $response;
374 }
375
376 /**
377 * Gets plugins that are defined at $TYPO3_CONF_VARS['EXTCONF']['t3editor']['plugins']
378 * Called by AjaxRequestHandler
379 *
380 * @param ServerRequestInterface $request
381 * @param ResponseInterface $response
382 * @return ResponseInterface
383 */
384 public function getPlugins(ServerRequestInterface $request, ResponseInterface $response)
385 {
386 $result = [];
387 $plugins = &$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3editor']['plugins'];
388 if (is_array($plugins)) {
389 $result = array_values($plugins);
390 }
391 $response->getBody()->write(json_encode($result));
392 return $response;
393 }
394
395 /**
396 * @return PageRenderer
397 */
398 protected function getPageRenderer()
399 {
400 return GeneralUtility::makeInstance(PageRenderer::class);
401 }
402 }