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