* Feature/cleanup: Restructure plugins FindReplace, InsertSmiley, RemoveFormat, Spell...
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / pi1 / class.tx_rtehtmlarea_pi1.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2003-2006 Stanislas Rolland <stanislas.rolland(arobas)fructifor.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 <stanislas.rolland(arobas)fructifor.ca>
28 *
29 * TYPO3 CVS ID: $Id$
30 *
31 */
32 require_once(PATH_tslib.'class.tslib_pibase.php');
33
34 class tx_rtehtmlarea_pi1 extends tslib_pibase {
35
36 /**
37 * back reference to the mother cObj object set at call time
38 *
39 * @var tslib_cObj
40 */
41 var $cObj;
42 var $prefixId = 'tx_rtehtmlarea_pi1'; // Same as class name
43 var $scriptRelPath = 'pi1/class.tx_rtehtmlarea_pi1.php'; // Path to this script relative to the extension dir.
44 var $extKey = 'rtehtmlarea'; // The extension key.
45 var $conf = array();
46 var $siteUrl;
47 var $charset = 'utf-8';
48 var $parserCharset = 'utf-8';
49 var $defaultAspellEncoding = 'utf-8';
50 var $aspellEncoding;
51 var $result;
52 var $text;
53 var $misspelled = array();
54 var $suggestedWords;
55 var $wordCount = 0;
56 var $suggestionCount = 0;
57 var $suggestedWordCount = 0;
58 var $pspell_link;
59 var $pspellMode = 'normal';
60 var $dictionary;
61 var $AspellDirectory;
62 var $pspell_is_available;
63 var $forceCommandMode = 0;
64 var $filePrefix = 'rtehtmlarea_';
65 var $uploadFolder = 'uploads/tx_rtehtmlarea/';
66 var $userUid;
67 var $personalDictsArg = '';
68 var $xmlCharacterData = '';
69
70 /**
71 * Main class of Spell Checker plugin for Typo3 CMS
72 *
73 * @param string $content: content to be displayed
74 * @param array $conf: TS setup for the plugin
75 * @return string content produced by the plugin
76 */
77 function main($conf) {
78 global $TYPO3_CONF_VARS, $TYPO3_DB;
79
80 $this->conf = $conf;
81 $this->tslib_pibase();
82 $this->pi_setPiVarDefaults();
83 $this->pi_loadLL();
84 $this->pi_USER_INT_obj = 1; // Disable caching
85 // Setting start time
86 $time_start = microtime(true);
87 $this->pspell_is_available = in_array('pspell', get_loaded_extensions());
88 $this->AspellDirectory = trim($TYPO3_CONF_VARS['EXTCONF'][$this->extKey]['AspellDirectory'])? trim($TYPO3_CONF_VARS['EXTCONF'][$this->extKey]['AspellDirectory']) : '/usr/bin/aspell';
89 $this->forceCommandMode = (trim($TYPO3_CONF_VARS['EXTCONF'][$this->extKey]['forceCommandMode']))? trim($TYPO3_CONF_VARS['EXTCONF'][$this->extKey]['forceCommandMode']) : 0;
90 $safe_mode_is_enabled = ini_get('safe_mode');
91 if($safe_mode_is_enabled && !$this->pspell_is_available ) echo('Configuration problem: Spell checking cannot be performed');
92 if($safe_mode_is_enabled && $this->forceCommandMode) echo('Configuration problem: Spell checking cannot be performed in command mode');
93 if(!$safe_mode_is_enabled && (!$this->pspell_is_available || $this->forceCommandMode)) {
94 $AspellVersionString = explode('Aspell', shell_exec( $this->AspellDirectory.' -v'));
95 $AspellVersion = substr( $AspellVersionString[1], 0, 4);
96 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');
97 $this->defaultAspellEncoding = trim(shell_exec($this->AspellDirectory.' config encoding'));
98 }
99
100 // Setting the list of dictionaries
101 if(!$safe_mode_is_enabled && (!$this->pspell_is_available || $this->forceCommandMode)) {
102 $dictionaryList = shell_exec( $this->AspellDirectory.' dump dicts');
103 $dictionaryList = implode(',', t3lib_div::trimExplode(chr(10), $dictionaryList, 1));
104 }
105 if( empty($dictionaryList) ) {
106 $dictionaryList = trim($TYPO3_CONF_VARS['EXTCONF'][$this->extKey]['dictionaryList']);
107 }
108 if( empty($dictionaryList) ) {
109 $dictionaryList = 'en';
110 }
111 $dictionaryArray = t3lib_div::trimExplode(',', $dictionaryList, 1);
112
113 $defaultDictionary = trim($TYPO3_CONF_VARS['EXTCONF'][$this->extKey]['defaultDictionary']);
114 if(!$defaultDictionary || !in_array($defaultDictionary, $dictionaryArray)) {
115 $defaultDictionary = 'en';
116 }
117
118 // Get the defined sys_language codes
119 $languageArray = array();
120 $tableA = 'sys_language';
121 $tableB = 'static_languages';
122 $selectFields = $tableA . '.uid,' . $tableB . '.lg_iso_2,' . $tableB . '.lg_country_iso_2';
123 $table = $tableA . ' LEFT JOIN ' . $tableB . ' ON ' . $tableA . '.static_lang_isocode=' . $tableB . '.uid';
124 $whereClause = '1=1 ';
125 $whereClause .= ' AND ' . $tableA . '.hidden != 1';
126 $res = $TYPO3_DB->exec_SELECTquery($selectFields, $table, $whereClause);
127 while($row = $TYPO3_DB->sql_fetch_assoc($res)) {
128 $languageArray[] = strtolower($row['lg_iso_2']).($row['lg_country_iso_2']?'_'.$row['lg_country_iso_2']:'');
129 }
130 if(!in_array($defaultDictionary, $languageArray)) {
131 $languageArray[] = $defaultDictionary;
132 }
133 foreach ($dictionaryArray as $key => $dict) {
134 $lang = explode('-', $dict);
135 if( !in_array(substr($dict, 0, 2), $languageArray) || !empty($lang[1])) {
136 unset($dictionaryArray[$key]);
137 } else {
138 $dictionaryArray[$key] = $lang[0];
139 }
140 }
141 uasort($dictionaryArray, 'strcoll');
142 $dictionaryList = implode(',', $dictionaryArray);
143
144 // Setting the dictionary
145 $this->dictionary = t3lib_div::_POST('dictionary');
146 if( empty($this->dictionary) || !in_array($this->dictionary, $dictionaryArray)) {
147 $this->dictionary = $defaultDictionary;
148 }
149 $dictionaries = substr_replace($dictionaryList, '@'.$this->dictionary, strpos($dictionaryList, $this->dictionary), strlen($this->dictionary));
150
151 // Setting the pspell suggestion mode
152 $this->pspellMode = t3lib_div::_POST('pspell_mode')?t3lib_div::_POST('pspell_mode'): $this->pspellMode;
153 // Now sanitize $this->pspellMode
154 $this->pspellMode = t3lib_div::inList('ultra,fast,normal,bad-spellers',$this->pspellMode)?$this->pspellMode:'normal';
155 switch($this->pspellMode) {
156 case 'ultra':
157 case 'fast':
158 $pspellModeFlag = PSPELL_FAST;
159 break;
160 case 'bad-spellers':
161 $pspellModeFlag = PSPELL_BAD_SPELLERS;
162 break;
163 case 'normal':
164 default:
165 $pspellModeFlag = PSPELL_NORMAL;
166 break;
167 }
168
169 // Setting the charset
170 if (t3lib_div::_POST('pspell_charset')) {
171 $this->charset = trim(t3lib_div::_POST('pspell_charset'));
172 }
173 if (strtolower($this->charset) == 'iso-8859-1') {
174 $this->parserCharset = strtolower($this->charset);
175 }
176 $internal_encoding = mb_internal_encoding(strtoupper($this->parserCharset));
177 //$regex_encoding = mb_regex_encoding(strtoupper($this->parserCharset));
178
179 // In some configurations, Aspell uses 'iso8859-1' instead of 'iso-8859-1'
180 $this->aspellEncoding = $this->parserCharset;
181 if ($this->parserCharset == 'iso-8859-1' && strstr($this->defaultAspellEncoding, '8859-1')) {
182 $this->aspellEncoding = $this->defaultAspellEncoding;
183 }
184
185 // However, we are going to work only in the parser charset
186 if($this->pspell_is_available && !$this->forceCommandMode) {
187 $this->pspell_link = pspell_new($this->dictionary, '', '', $this->parserCharset, $pspellModeFlag);
188 }
189
190 // Setting the path to user personal dicts, if any
191 if (t3lib_div::_POST('enablePersonalDicts') == 'true' && $GLOBALS['TSFE']->beUserLogin) {
192 $this->userUid = 'BE_' . $GLOBALS['BE_USER']->user['uid'];
193 if ($this->userUid) {
194 $this->personalDictPath = t3lib_div::getFileAbsFileName($this->uploadFolder . $this->userUid);
195 if (!is_dir($this->personalDictPath)) {
196 t3lib_div::mkdir($this->personalDictPath);
197 }
198 // escape here for later use
199 $this->personalDictsArg = ' --home-dir=' . escapeshellarg($this->personalDictPath);
200 }
201 }
202
203 $cmd = t3lib_div::_POST('cmd');
204 if ($cmd == 'learn' && !$safe_mode_is_enabled) {
205 // Only availble for BE_USERS, die silently if someone has gotten here by accident
206 if(!$GLOBALS['TSFE']->beUserLogin) die('');
207 // Updating the personal word list
208 $to_p_dict = t3lib_div::_POST('to_p_dict');
209 $to_p_dict = $to_p_dict ? $to_p_dict : array();
210 $to_r_list = t3lib_div::_POST('to_r_list');
211 $to_r_list = $to_r_list ? $to_r_list : array();
212 header('Content-Type: text/plain; charset=' . strtoupper($this->parserCharset));
213 header('Pragma: no-cache');
214 //print_r($to_r_list);
215 if($to_p_dict || $to_r_list) {
216 $tmpFileName = t3lib_div::tempnam($this->filePrefix);
217 if($filehandle = fopen($tmpFileName,'wb')) {
218 foreach ($to_p_dict as $personal_word) {
219 $cmd = '&' . $personal_word . "\n";
220 echo $cmd;
221 fwrite($filehandle, $cmd, strlen($cmd));
222 }
223 foreach ($to_r_list as $replace_pair) {
224 $cmd = '$$ra ' . $replace_pair[0] . ' , ' . $replace_pair[1] . "\n";
225 echo $cmd;
226 fwrite($filehandle, $cmd, strlen($cmd));
227 }
228 $cmd = "#\n";
229 echo $cmd;
230 fwrite($filehandle, $cmd, strlen($cmd));
231 fclose($filehandle);
232 // $this->personalDictsArg has already been escapeshellarg()'ed above, it is an optional paramter and might be empty here
233 $AspellCommand = 'cat ' . escapeshellarg($tmpFileName) . ' | ' . $this->AspellDirectory . ' -a --mode=none' . $this->personalDictsArg . ' --lang=' . escapeshellarg($this->dictionary) . ' --encoding=' . escapeshellarg($this->aspellEncoding) . ' 2>&1';
234 print $AspellCommand . "\n";
235 print shell_exec($AspellCommand);
236 t3lib_div::unlink_tempfile($tmpFileName);
237 echo('Personal word list was updated.');
238 } else {
239 echo('SpellChecker tempfile open error.');
240 }
241 } else {
242 echo('Nothing to add to the personal word list.');
243 }
244 flush();
245 exit();
246 } else {
247 // Check spelling content
248 // Initialize output
249 $this->result = '<?xml version="1.0" encoding="' . $this->parserCharset . '"?>
250 <!DOCTYPE html
251 PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
252 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
253 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="' . substr($this->dictionary, 0, 2) . '" lang="' . substr($this->dictionary, 0, 2) . '">
254 <html>
255 <head>
256 <meta http-equiv="Content-Type" content="text/html; charset=' . $this->parserCharset . '" />
257 <link rel="stylesheet" type="text/css" media="all" href="spell-check-style.css" />
258 <script type="text/javascript">
259 /*<![CDATA[*/
260 <!--
261 ';
262
263 // Getting the input content
264 $content = t3lib_div::_POST('content');
265
266 // Parsing the input HTML
267 $parser = xml_parser_create(strtoupper($this->parserCharset));
268 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
269 xml_set_object($parser, &$this);
270 if (!xml_set_element_handler($parser, 'startHandler', 'endHandler')) echo('Bad xml handler setting');
271 if (!xml_set_character_data_handler($parser, 'collectDataHandler')) echo('Bad xml handler setting');
272 if (!xml_set_default_handler($parser, 'defaultHandler')) echo('Bad xml handler setting');
273 if (!xml_parse($parser,'<?xml version="1.0" encoding="' . $this->parserCharset . '"?><spellchecker> ' . mb_ereg_replace('&nbsp;', ' ', $content) . ' </spellchecker>')) echo('Bad parsing');
274 if (xml_get_error_code($parser)) {
275 die('Line '.xml_get_current_line_number($parser).': '.xml_error_string(xml_get_error_code($parser)));
276 }
277 xml_parser_free($parser);
278 if ($this->pspell_is_available && !$this->forceCommandMode) {
279 pspell_clear_session ($this->pspell_link);
280 }
281 $this->result .= 'var suggested_words = {' . $this->suggestedWords . '};
282 ';
283
284 // Calculating parsing and spell checkting time
285 $time = number_format(microtime(true) - $time_start, 2, ',', ' ');
286
287 // Insert spellcheck info
288 $this->result .= 'var spellcheck_info = { "Total words":"'.$this->wordCount.'","Misspelled words":"'.sizeof($this->misspelled).'","Total suggestions":"'.$this->suggestionCount.'","Total words suggested":"'.$this->suggestedWordCount.'","Spelling checked in":"'.$time.'" };
289 // -->
290 /*]]>*/
291 </script>
292 </head>
293 ';
294 $this->result .= '<body onload="window.parent.finishedSpellChecking();">';
295 $this->result .= preg_replace('/'.preg_quote('<?xml').'.*'.preg_quote('?>').'['.preg_quote(chr(10).chr(13).chr(32)).']*/', '', $this->text);
296 $this->result .= '<div id="HA-spellcheck-dictionaries">'.$dictionaries.'</div>';
297
298 // Closing
299 $this->result .= '
300 </body></html>';
301
302 // Outputting
303 header('Content-Type: text/html; charset=' . strtoupper($this->parserCharset));
304 echo $this->result;
305 }
306
307 } // end of function main
308
309 function startHandler($xml_parser, $tag, $attributes) {
310 if (strlen($this->xmlCharacterData)) {
311 $this->spellCheckHandler($xml_parser, $this->xmlCharacterData);
312 $this->xmlCharacterData = '';
313 }
314
315 switch($tag) {
316 case 'spellchecker':
317 break;
318 case 'br':
319 case 'BR':
320 case 'img':
321 case 'IMG':
322 case 'hr':
323 case 'HR':
324 case 'area':
325 case 'AREA':
326 $this->text .= '<'. mb_strtolower($tag) . ' ';
327 foreach( $attributes as $key => $val) {
328 $this->text .= $key . '="' . $val . '" ';
329 }
330 $this->text .= ' />';
331 break;
332 default:
333 $this->text .= '<'. mb_strtolower($tag) . ' ';
334 foreach( $attributes as $key => $val) {
335 $this->text .= $key . '="' . $val . '" ';
336 }
337 $this->text .= '>';
338 break;
339 }
340 return;
341 }
342
343 function endHandler($xml_parser, $tag) {
344 if (strlen($this->xmlCharacterData)) {
345 $this->spellCheckHandler($xml_parser, $this->xmlCharacterData);
346 $this->xmlCharacterData = '';
347 }
348
349 switch($tag) {
350 case 'spellchecker':
351 break;
352 case 'br':
353 case 'BR':
354 case 'img':
355 case 'IMG':
356 case 'hr':
357 case 'HR':
358 case 'input':
359 case 'INPUT':
360 case 'area':
361 case 'AREA':
362 break;
363 default:
364 $this->text .= '</' . $tag . '>';
365 break;
366 }
367 return;
368 }
369
370 function spellCheckHandler($xml_parser, $string) {
371 $incurrent=array();
372 $stringText = $string;
373 $words = mb_split('\W+', $stringText);
374 while( list(,$word) = each($words) ) {
375 $word = mb_ereg_replace(' ', '', $word);
376 if( $word && !is_numeric($word)) {
377 if($this->pspell_is_available && !$this->forceCommandMode) {
378 if (!pspell_check($this->pspell_link, $word)) {
379 if(!in_array($word, $this->misspelled)) {
380 if(sizeof($this->misspelled) != 0 ) {
381 $this->suggestedWords .= ',';
382 }
383 $suggest = array();
384 $suggest = pspell_suggest($this->pspell_link, $word);
385 if(sizeof($suggest) != 0 ) {
386 $this->suggestionCount++;
387 $this->suggestedWordCount += sizeof($suggest);
388 }
389 $this->suggestedWords .= '"'.$word.'":"'.implode(',',$suggest).'"';
390 $this->misspelled[] = $word;
391 unset($suggest);
392 }
393 if( !in_array($word, $incurrent) ) {
394 $stringText = mb_ereg_replace('\b'.$word.'\b', '<span class="HA-spellcheck-error">'.$word.'</span>', $stringText);
395 $incurrent[] = $word;
396 }
397 }
398 } else {
399 $tmpFileName = t3lib_div::tempnam($this->filePrefix);
400 if(!$filehandle = fopen($tmpFileName,'wb')) echo('SpellChecker tempfile open error');
401 if(!fwrite($filehandle, $word)) echo('SpellChecker tempfile write error');
402 if(!fclose($filehandle)) echo('SpellChecker tempfile close error');
403 $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';
404 $AspellAnswer = shell_exec($AspellCommand);
405 $AspellResultLines = array();
406 $AspellResultLines = t3lib_div::trimExplode(chr(10), $AspellAnswer, 1);
407 if(substr($AspellResultLines[0],0,6) == 'Error:') echo("{$AspellAnswer}");
408 t3lib_div::unlink_tempfile($tmpFileName);
409 if(substr($AspellResultLines['1'],0,1) != '*') {
410 if(!in_array($word, $this->misspelled)) {
411 if(sizeof($this->misspelled) != 0 ) {
412 $this->suggestedWords .= ',';
413 }
414 $suggest = array();
415 $suggestions = array();
416 if (substr($AspellResultLines['1'],0,1) == '&') {
417 $suggestions = t3lib_div::trimExplode(':', $AspellResultLines['1'], 1);
418 $suggest = t3lib_div::trimExplode(',', $suggestions['1'], 1);
419 }
420 if (sizeof($suggest) != 0) {
421 $this->suggestionCount++;
422 $this->suggestedWordCount += sizeof($suggest);
423 }
424 $this->suggestedWords .= '"'.$word.'":"'.implode(',',$suggest).'"';
425 $this->misspelled[] = $word;
426 unset($suggest);
427 unset($suggestions);
428 }
429 if (!in_array($word, $incurrent)) {
430 $stringText = mb_ereg_replace('\b'.$word.'\b', '<span class="HA-spellcheck-error">'.$word.'</span>', $stringText);
431 $incurrent[] = $word;
432 }
433 }
434 unset($AspellResultLines);
435 }
436 $this->wordCount++;
437 }
438 }
439 $this->text .= $stringText;
440 unset($incurrent);
441 return;
442 }
443
444 function collectDataHandler($xml_parser, $string) {
445 $this->xmlCharacterData .= $string;
446 }
447
448 function defaultHandler($xml_parser, $string) {
449 $this->text .= $string;
450 return;
451 }
452
453 } // end of class
454
455 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/rtehtmlarea/pi1/class.tx_rtehtmlarea_pi1.php']) {
456 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/rtehtmlarea/pi1/class.tx_rtehtmlarea_pi1.php']);
457 }
458
459 ?>