Added Feature #12066: Added constants throughout the core for LF, CR, TAB (Thanks...
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / pi1 / class.tx_rtehtmlarea_pi1.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2003-2010 Stanislas Rolland <typo3(arobas)sjbr.ca>
6 * All rights reserved
7 *
8 * This script is part of the Typo3 project. The Typo3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24 /**
25 * Spell checking plugin 'tx_rtehtmlarea_pi1' for the htmlArea RTE extension.
26 *
27 * @author Stanislas Rolland <typo3(arobas)sjbr.ca>
28 *
29 * TYPO3 SVN ID: $Id$
30 *
31 */
32
33 class tx_rtehtmlarea_pi1 {
34
35 protected $csConvObj;
36 var $extKey = 'rtehtmlarea'; // The extension key.
37 var $siteUrl;
38 var $charset = 'utf-8';
39 var $parserCharset = 'utf-8';
40 var $defaultAspellEncoding = 'utf-8';
41 var $aspellEncoding;
42 var $result;
43 var $text;
44 var $misspelled = array();
45 var $suggestedWords;
46 var $wordCount = 0;
47 var $suggestionCount = 0;
48 var $suggestedWordCount = 0;
49 var $pspell_link;
50 var $pspellMode = 'normal';
51 var $dictionary;
52 var $AspellDirectory;
53 var $pspell_is_available;
54 var $forceCommandMode = 0;
55 var $filePrefix = 'rtehtmlarea_';
56 var $uploadFolder = 'uploads/tx_rtehtmlarea/';
57 var $userUid;
58 var $personalDictsArg = '';
59 var $xmlCharacterData = '';
60
61 /**
62 * Main class of Spell Checker plugin for Typo3 CMS
63 *
64 * @return string content produced by the plugin
65 */
66 function main() {
67
68 $this->csConvObj = t3lib_div::makeInstance('t3lib_cs');
69
70 // Setting start time
71 $time_start = microtime(true);
72 $this->pspell_is_available = in_array('pspell', get_loaded_extensions());
73 $this->AspellDirectory = trim($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['plugins']['SpellChecker']['AspellDirectory'])? trim($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['plugins']['SpellChecker']['AspellDirectory']) : '/usr/bin/aspell';
74 $this->forceCommandMode = (trim($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['plugins']['SpellChecker']['forceCommandMode']))? trim($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['plugins']['SpellChecker']['forceCommandMode']) : 0;
75 $safe_mode_is_enabled = ini_get('safe_mode');
76 if($safe_mode_is_enabled && !$this->pspell_is_available ) echo('Configuration problem: Spell checking cannot be performed');
77 if($safe_mode_is_enabled && $this->forceCommandMode) echo('Configuration problem: Spell checking cannot be performed in command mode');
78 if(!$safe_mode_is_enabled && (!$this->pspell_is_available || $this->forceCommandMode)) {
79 $AspellVersionString = explode('Aspell', shell_exec( $this->AspellDirectory.' -v'));
80 $AspellVersion = substr( $AspellVersionString[1], 0, 4);
81 if( doubleval($AspellVersion) < doubleval('0.5') && (!$this->pspell_is_available || $this->forceCommandMode)) echo('Configuration problem: Aspell version ' . $AspellVersion . ' too old. Spell checking cannot be performed in command mode');
82 $this->defaultAspellEncoding = trim(shell_exec($this->AspellDirectory.' config encoding'));
83 }
84
85 // Setting the list of dictionaries
86 if (!$safe_mode_is_enabled && (!$this->pspell_is_available || $this->forceCommandMode)) {
87 $dictionaryList = shell_exec( $this->AspellDirectory.' dump dicts');
88 $dictionaryList = implode(',', t3lib_div::trimExplode(LF, $dictionaryList, 1));
89 }
90 if (empty($dictionaryList)) {
91 $dictionaryList = t3lib_div::_POST('showDictionaries');
92 // Applying EM variable DEPRECATED as of TYPO3 4.3.0
93 $dictionaryList = $dictionaryList ? $dictionaryList : trim($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['plugins']['SpellChecker']['dictionaryList']);
94 }
95 $dictionaryArray = t3lib_div::trimExplode(',', $dictionaryList, 1);
96 $restrictToDictionaries = t3lib_div::_POST('restrictToDictionaries');
97 if ($restrictToDictionaries) {
98 $dictionaryArray = array_intersect($dictionaryArray, t3lib_div::trimExplode(',', $restrictToDictionaries, 1));
99 }
100 if (!count($dictionaryArray)) {
101 $dictionaryArray[] = 'en';
102 }
103 $this->dictionary = t3lib_div::_POST('dictionary');
104 // Applying EM variable DEPRECATED as of TYPO3 4.3.0
105 $defaultDictionary = $this->dictionary ? $this->dictionary : trim($TYPO3_CONF_VARS['EXTCONF'][$this->extKey]['plugins']['SpellChecker']['defaultDictionary']);
106 if (!$defaultDictionary || !in_array($defaultDictionary, $dictionaryArray)) {
107 $defaultDictionary = 'en';
108 }
109 uasort($dictionaryArray, 'strcoll');
110 $dictionaryList = implode(',', $dictionaryArray);
111 // Setting the dictionary
112 if (empty($this->dictionary) || !in_array($this->dictionary, $dictionaryArray)) {
113 $this->dictionary = 'en';
114 }
115 // Setting the pspell suggestion mode
116 $this->pspellMode = t3lib_div::_POST('pspell_mode')?t3lib_div::_POST('pspell_mode'): $this->pspellMode;
117 // Now sanitize $this->pspellMode
118 $this->pspellMode = t3lib_div::inList('ultra,fast,normal,bad-spellers',$this->pspellMode)?$this->pspellMode:'normal';
119 switch($this->pspellMode) {
120 case 'ultra':
121 case 'fast':
122 $pspellModeFlag = PSPELL_FAST;
123 break;
124 case 'bad-spellers':
125 $pspellModeFlag = PSPELL_BAD_SPELLERS;
126 break;
127 case 'normal':
128 default:
129 $pspellModeFlag = PSPELL_NORMAL;
130 break;
131 }
132
133 // Setting the charset
134 if (t3lib_div::_POST('pspell_charset')) {
135 $this->charset = trim(t3lib_div::_POST('pspell_charset'));
136 }
137 if (strtolower($this->charset) == 'iso-8859-1') {
138 $this->parserCharset = strtolower($this->charset);
139 }
140
141 // In some configurations, Aspell uses 'iso8859-1' instead of 'iso-8859-1'
142 $this->aspellEncoding = $this->parserCharset;
143 if ($this->parserCharset == 'iso-8859-1' && strstr($this->defaultAspellEncoding, '8859-1')) {
144 $this->aspellEncoding = $this->defaultAspellEncoding;
145 }
146
147 // However, we are going to work only in the parser charset
148 if($this->pspell_is_available && !$this->forceCommandMode) {
149 $this->pspell_link = pspell_new($this->dictionary, '', '', $this->parserCharset, $pspellModeFlag);
150 }
151
152 // Setting the path to user personal dicts, if any
153 if (t3lib_div::_POST('enablePersonalDicts') == 'true' && TYPO3_MODE == 'BE' && is_object($GLOBALS['BE_USER'])) {
154 $this->userUid = 'BE_' . $GLOBALS['BE_USER']->user['uid'];
155 if ($this->userUid) {
156 $this->personalDictPath = t3lib_div::getFileAbsFileName($this->uploadFolder . $this->userUid);
157 if (!is_dir($this->personalDictPath)) {
158 t3lib_div::mkdir($this->personalDictPath);
159 }
160 // escape here for later use
161 $this->personalDictsArg = ' --home-dir=' . escapeshellarg($this->personalDictPath);
162 }
163 }
164
165 $cmd = t3lib_div::_POST('cmd');
166 if ($cmd == 'learn' && !$safe_mode_is_enabled) {
167 // Only availble for BE_USERS, die silently if someone has gotten here by accident
168 if (TYPO3_MODE !='BE' || !is_object($GLOBALS['BE_USER'])) die('');
169 // Updating the personal word list
170 $to_p_dict = t3lib_div::_POST('to_p_dict');
171 $to_p_dict = $to_p_dict ? $to_p_dict : array();
172 $to_r_list = t3lib_div::_POST('to_r_list');
173 $to_r_list = $to_r_list ? $to_r_list : array();
174 header('Content-Type: text/plain; charset=' . strtoupper($this->parserCharset));
175 header('Pragma: no-cache');
176 if($to_p_dict || $to_r_list) {
177 $tmpFileName = t3lib_div::tempnam($this->filePrefix);
178 if($filehandle = fopen($tmpFileName,'wb')) {
179 foreach ($to_p_dict as $personal_word) {
180 $cmd = '&' . $personal_word . LF;
181 echo $cmd;
182 fwrite($filehandle, $cmd, strlen($cmd));
183 }
184 foreach ($to_r_list as $replace_pair) {
185 $cmd = '$$ra ' . $replace_pair[0] . ' , ' . $replace_pair[1] . LF;
186 echo $cmd;
187 fwrite($filehandle, $cmd, strlen($cmd));
188 }
189 $cmd = "#\n";
190 echo $cmd;
191 fwrite($filehandle, $cmd, strlen($cmd));
192 fclose($filehandle);
193 // $this->personalDictsArg has already been escapeshellarg()'ed above, it is an optional paramter and might be empty here
194 $AspellCommand = 'cat ' . escapeshellarg($tmpFileName) . ' | ' . $this->AspellDirectory . ' -a --mode=none' . $this->personalDictsArg . ' --lang=' . escapeshellarg($this->dictionary) . ' --encoding=' . escapeshellarg($this->aspellEncoding) . ' 2>&1';
195 print $AspellCommand . LF;
196 print shell_exec($AspellCommand);
197 t3lib_div::unlink_tempfile($tmpFileName);
198 echo('Personal word list was updated.');
199 } else {
200 echo('SpellChecker tempfile open error.');
201 }
202 } else {
203 echo('Nothing to add to the personal word list.');
204 }
205 flush();
206 exit();
207 } else {
208 // Check spelling content
209 // Initialize output
210 $this->result = '<?xml version="1.0" encoding="' . $this->parserCharset . '"?>
211 <!DOCTYPE html
212 PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
213 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
214 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="' . substr($this->dictionary, 0, 2) . '" lang="' . substr($this->dictionary, 0, 2) . '">
215 <head>
216 <meta http-equiv="Content-Type" content="text/html; charset=' . $this->parserCharset . '" />
217 <link rel="stylesheet" type="text/css" media="all" href="' . (TYPO3_MODE == 'BE' ? '../' : '') . t3lib_extMgm::siteRelPath($this->extKey) . '/htmlarea/plugins/SpellChecker/spell-check-style.css" />
218 <script type="text/javascript">
219 /*<![CDATA[*/
220 <!--
221 ';
222
223 // Getting the input content
224 $content = t3lib_div::_POST('content');
225
226 // Parsing the input HTML
227 $parser = xml_parser_create(strtoupper($this->parserCharset));
228 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
229 xml_set_object($parser, &$this);
230 if (!xml_set_element_handler($parser, 'startHandler', 'endHandler')) echo('Bad xml handler setting');
231 if (!xml_set_character_data_handler($parser, 'collectDataHandler')) echo('Bad xml handler setting');
232 if (!xml_set_default_handler($parser, 'defaultHandler')) echo('Bad xml handler setting');
233 if (!xml_parse($parser,'<?xml version="1.0" encoding="' . $this->parserCharset . '"?><spellchecker> ' . preg_replace('/&nbsp;/'.(($this->parserCharset == 'utf-8')?'u':''), ' ', $content) . ' </spellchecker>')) echo('Bad parsing');
234 if (xml_get_error_code($parser)) {
235 die('Line '.xml_get_current_line_number($parser).': '.xml_error_string(xml_get_error_code($parser)));
236 }
237 xml_parser_free($parser);
238 if ($this->pspell_is_available && !$this->forceCommandMode) {
239 pspell_clear_session ($this->pspell_link);
240 }
241 $this->result .= 'var suggestedWords = {' . $this->suggestedWords . '};
242 var dictionaries = "' . $dictionaryList . '";
243 var selectedDictionary = "' . $this->dictionary . '";
244 ';
245
246 // Calculating parsing and spell checkting time
247 $time = number_format(microtime(true) - $time_start, 2, ',', ' ');
248
249 // Insert spellcheck info
250 $this->result .= 'var spellcheckInfo = { "Total words":"'.$this->wordCount.'","Misspelled words":"'.sizeof($this->misspelled).'","Total suggestions":"'.$this->suggestionCount.'","Total words suggested":"'.$this->suggestedWordCount.'","Spelling checked in":"'.$time.'" };
251 // -->
252 /*]]>*/
253 </script>
254 </head>
255 ';
256 $this->result .= '<body onload="window.parent.RTEarea[\'' . t3lib_div::_POST('editorId') . '\'].editor.getPlugin(\'SpellChecker\').spellCheckComplete();">';
257 $this->result .= preg_replace('/'.preg_quote('<?xml').'.*'.preg_quote('?>').'['.preg_quote(LF.CR.chr(32)).']*/'.(($this->parserCharset == 'utf-8')?'u':''), '', $this->text);
258 $this->result .= '<div style="display: none;">'.$dictionaries.'</div>';
259
260 // Closing
261 $this->result .= '
262 </body></html>';
263
264 // Outputting
265 header('Content-Type: text/html; charset=' . strtoupper($this->parserCharset));
266 echo $this->result;
267 }
268
269 } // end of function main
270
271 function startHandler($xml_parser, $tag, $attributes) {
272
273 if (strlen($this->xmlCharacterData)) {
274 $this->spellCheckHandler($xml_parser, $this->xmlCharacterData);
275 $this->xmlCharacterData = '';
276 }
277
278 switch($tag) {
279 case 'spellchecker':
280 break;
281 case 'br':
282 case 'BR':
283 case 'img':
284 case 'IMG':
285 case 'hr':
286 case 'HR':
287 case 'area':
288 case 'AREA':
289 $this->text .= '<'. $this->csConvObj->conv_case($this->parserCharset, $tag, 'toLower') . ' ';
290 foreach( $attributes as $key => $val) {
291 $this->text .= $key . '="' . $val . '" ';
292 }
293 $this->text .= ' />';
294 break;
295 default:
296 $this->text .= '<'. $this->csConvObj->conv_case($this->parserCharset, $tag, 'toLower') . ' ';
297 foreach( $attributes as $key => $val) {
298 $this->text .= $key . '="' . $val . '" ';
299 }
300 $this->text .= '>';
301 break;
302 }
303 return;
304 }
305
306 function endHandler($xml_parser, $tag) {
307 if (strlen($this->xmlCharacterData)) {
308 $this->spellCheckHandler($xml_parser, $this->xmlCharacterData);
309 $this->xmlCharacterData = '';
310 }
311
312 switch($tag) {
313 case 'spellchecker':
314 break;
315 case 'br':
316 case 'BR':
317 case 'img':
318 case 'IMG':
319 case 'hr':
320 case 'HR':
321 case 'input':
322 case 'INPUT':
323 case 'area':
324 case 'AREA':
325 break;
326 default:
327 $this->text .= '</' . $tag . '>';
328 break;
329 }
330 return;
331 }
332
333 function spellCheckHandler($xml_parser, $string) {
334 $incurrent=array();
335 $stringText = $string;
336 $words = preg_split((($this->parserCharset == 'utf-8')?'/\P{L}+/u':'/\W+/'), $stringText);
337 while( list(,$word) = each($words) ) {
338 $word = preg_replace('/ /'.(($this->parserCharset == 'utf-8')?'u':''), '', $word);
339 if( $word && !is_numeric($word)) {
340 if($this->pspell_is_available && !$this->forceCommandMode) {
341 if (!pspell_check($this->pspell_link, $word)) {
342 if(!in_array($word, $this->misspelled)) {
343 if(sizeof($this->misspelled) != 0 ) {
344 $this->suggestedWords .= ',';
345 }
346 $suggest = array();
347 $suggest = pspell_suggest($this->pspell_link, $word);
348 if(sizeof($suggest) != 0 ) {
349 $this->suggestionCount++;
350 $this->suggestedWordCount += sizeof($suggest);
351 }
352 $this->suggestedWords .= '"'.$word.'":"'.implode(',',$suggest).'"';
353 $this->misspelled[] = $word;
354 unset($suggest);
355 }
356 if( !in_array($word, $incurrent) ) {
357 $stringText = preg_replace('/\b'.$word.'\b/'.(($this->parserCharset == 'utf-8')?'u':''), '<span class="htmlarea-spellcheck-error">'.$word.'</span>', $stringText);
358 $incurrent[] = $word;
359 }
360 }
361 } else {
362 $tmpFileName = t3lib_div::tempnam($this->filePrefix);
363 if(!$filehandle = fopen($tmpFileName,'wb')) echo('SpellChecker tempfile open error');
364 if(!fwrite($filehandle, $word)) echo('SpellChecker tempfile write error');
365 if(!fclose($filehandle)) echo('SpellChecker tempfile close error');
366 $AspellCommand = 'cat ' . escapeshellarg($tmpFileName) . ' | ' . $this->AspellDirectory . ' -a check --mode=none --sug-mode=' . escapeshellarg($this->pspellMode) . $this->personalDictsArg . ' --lang=' . escapeshellarg($this->dictionary) . ' --encoding=' . escapeshellarg($this->aspellEncoding) . ' 2>&1';
367 $AspellAnswer = shell_exec($AspellCommand);
368 $AspellResultLines = array();
369 $AspellResultLines = t3lib_div::trimExplode(LF, $AspellAnswer, 1);
370 if(substr($AspellResultLines[0],0,6) == 'Error:') echo("{$AspellAnswer}");
371 t3lib_div::unlink_tempfile($tmpFileName);
372 if(substr($AspellResultLines['1'],0,1) != '*') {
373 if(!in_array($word, $this->misspelled)) {
374 if(sizeof($this->misspelled) != 0 ) {
375 $this->suggestedWords .= ',';
376 }
377 $suggest = array();
378 $suggestions = array();
379 if (substr($AspellResultLines['1'],0,1) == '&') {
380 $suggestions = t3lib_div::trimExplode(':', $AspellResultLines['1'], 1);
381 $suggest = t3lib_div::trimExplode(',', $suggestions['1'], 1);
382 }
383 if (sizeof($suggest) != 0) {
384 $this->suggestionCount++;
385 $this->suggestedWordCount += sizeof($suggest);
386 }
387 $this->suggestedWords .= '"'.$word.'":"'.implode(',',$suggest).'"';
388 $this->misspelled[] = $word;
389 unset($suggest);
390 unset($suggestions);
391 }
392 if (!in_array($word, $incurrent)) {
393 $stringText = preg_replace('/\b'.$word.'\b/'.(($this->parserCharset == 'utf-8')?'u':''), '<span class="htmlarea-spellcheck-error">'.$word.'</span>', $stringText);
394 $incurrent[] = $word;
395 }
396 }
397 unset($AspellResultLines);
398 }
399 $this->wordCount++;
400 }
401 }
402 $this->text .= $stringText;
403 unset($incurrent);
404 return;
405 }
406
407 function collectDataHandler($xml_parser, $string) {
408 $this->xmlCharacterData .= $string;
409 }
410
411 function defaultHandler($xml_parser, $string) {
412 $this->text .= $string;
413 return;
414 }
415
416 }
417
418 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/rtehtmlarea/pi1/class.tx_rtehtmlarea_pi1.php']) {
419 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/rtehtmlarea/pi1/class.tx_rtehtmlarea_pi1.php']);
420 }
421
422 if (TYPO3_MODE=='FE') {
423 tslib_eidtools::connectDB();
424 $spellChecker = t3lib_div::makeInstance('tx_rtehtmlarea_pi1');
425 $spellChecker->main();
426 }
427
428 ?>