adding php pspell support
[squirrelmail.git] / plugins / squirrelspell / class / cmd_spell.php
1 <?php
2 /**
3 * Command line spellcheck class
4 *
5 * ---
6 * Quick brownn fox brownn
7 *
8 * brownn squirrel.
9 * twentytwo owttnewt
10 * ---
11 * @(#) International Ispell Version 3.1.20 10/10/95, patch 1
12 * *
13 * & brownn 5 7: brown, Browne, browns, brown n, brown-n
14 * *
15 * & brownn 5 18: brown, Browne, browns, brown n, brown-n
16 *
17 *
18 * & brownn 5 1: brown, Browne, browns, brown n, brown-n
19 * *
20 *
21 * & twentytwo 2 1: twenty two, twenty-two
22 * # owttnewt 11
23 *
24 *
25 * ---
26 * $params = array();
27 * $params['spell_command'] = 'ispell -d american -a';
28 * $params['use_proc_open'] = false; // (check_php_version(4,3))
29 * $params['temp_dir'] = '/tmp/'; // $attachment_dir
30 * $params['userdic'] = array(); // user's dictionary
31 * $params['debug'] = true;
32 *
33 * $spell = new cmd_spell($params);
34 * // check $spell->error buffer
35 *
36 * $text = "Quick brownn fox brownn\n\nbrownn squirrel.\ntwentytwo owttnewt";
37 *
38 * $results = $spell->check_text($text);
39 * // check $spell->error buffer
40 * // parse $results
41 *
42 * @copyright &copy; 1999-2006 The SquirrelMail Project Team
43 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
44 * @version $Id$
45 * @package plugins
46 * @subpackage squirrelspell
47 */
48
49 /**
50 * Command line spellcheck class, compatible with ispell and aspell.
51 * @package plugins
52 * @subpackage squirrelspell
53 */
54 class cmd_spell extends squirrelspell {
55 /**
56 * @var string
57 */
58 var $spell_command = '';
59 var $userdic = array();
60 /**
61 * Controls which function is used to execute ispell. proc_open()
62 * should be used in PHP 4.3+. exec() can be used in older PHP versions.
63 * @var boolean
64 */
65 var $use_proc_open = false;
66 /**
67 * @var string
68 */
69 var $temp_dir = '';
70 /**
71 */
72 var $debug = false;
73
74 var $missed_words = array();
75
76 /**
77 * Constructor function
78 * @param array $aParams
79 */
80 function cmd_spell($aParams=array()) {
81 if (! isset($aParams['spell_command'])) {
82 return $this->set_error('Spellcheck command is not set.');
83 } else {
84 $this->spell_command = $aParams['spell_command'];
85 }
86
87 if (isset($aParams['userdic'])) {
88 $this->userdic = $aParams['userdic'];
89 }
90
91 if (isset($aParams['use_proc_open'])) {
92 $this->use_proc_open = (bool) $aParams['use_proc_open'];
93 }
94
95 if (isset($aParams['temp_dir'])) {
96 $this->temp_dir = $aParams['temp_dir'];
97 // add slash to attachment directory, if it does not end with slash.
98 if (substr($this->temp_dir, -1) != '/') {
99 $this->temp_dir = $this->temp_dir . '/';
100 }
101 } elseif (!$this->use_proc_open) {
102 return $this->set_error('Temporally directory is not set.');
103 }
104
105 if (isset($aParams['debug']) && (bool) $aParams['debug']) {
106 $this->debug = true;
107 error_reporting(E_ALL);
108 ini_set('display_errors',1);
109 }
110
111 }
112
113 /**
114 * @param string $sText
115 * @return mixed array with command output or false.
116 */
117 function proc_open_spell($sText) {
118 $descriptorspec = array(
119 0 => array('pipe', 'r'), // stdin is a pipe that the child will read from
120 1 => array('pipe', 'w'), // stdout is a pipe that the child will write to
121 2 => array('pipe', 'w'), // stderr is a pipe that the child will write to
122 );
123
124 if ($this->debug) {
125 $spell_proc = proc_open($this->spell_command, $descriptorspec, $pipes);
126 } else {
127 $spell_proc = @proc_open($this->spell_command, $descriptorspec, $pipes);
128 }
129
130 if ( ! is_resource($spell_proc) ) {
131 return $this->set_error(sprintf(_("Could not run the spellchecker command (%s)."),
132 $this->spell_command));
133 }
134
135 if ( ! @fwrite($pipes[0],$sText) ) {
136 $this->set_error(_("Error while writing to pipe."));
137 // close all three $pipes here.
138 for($i=0; $i<=2; $i++) {
139 // disable all fclose error messages
140 @fclose($pipes[$i]);
141 }
142 return false;
143 }
144
145 fclose($pipes[0]);
146
147 $sqspell_output = array();
148 for($i=1; $i<=2; $i++) {
149 while(!feof($pipes[$i])) {
150 array_push($sqspell_output, rtrim(fgetss($pipes[$i],999),"\r\n"));
151 }
152 fclose($pipes[$i]);
153 }
154
155 if (proc_close($spell_proc)) {
156 $error = '';
157 foreach ($sqspell_output as $line) {
158 $error.= $line . "\n";
159 }
160 return $this->set_error($error);
161 } else {
162 return $sqspell_output;
163 }
164 }
165
166 /**
167 * @param string $sText
168 * @return mixed array with command output or false.
169 */
170 function exec_spell($sText) {
171 // find unused file in attachment directory
172 do {
173 $floc = $this->temp_dir . md5($sText . microtime());
174 } while (file_exists($floc));
175
176 if ($this->debug) {
177 $fp = fopen($floc, 'w');
178 } else {
179 $fp = @fopen($floc, 'w');
180 }
181 if ( ! is_resource($fp) ) {
182 return $this->set_error(sprintf(_("Could not open temporary file '%s'."),
183 $floc) );
184 }
185
186 if ( ! @fwrite($fp, $sText) ) {
187 $this->set_error(sprintf(_("Error while writing to temporary file '%s'."),
188 $floc) );
189 // close file descriptor
190 fclose($fp);
191 return false;
192 }
193 fclose($fp);
194
195 exec("$this->spell_command < $floc 2>&1", $sqspell_output, $exitcode);
196
197 unlink($floc);
198
199 if ($exitcode) {
200 $error = '';
201 foreach ($sqspell_output as $line) {
202 $error.= $line . "\n";
203 }
204 return $this->set_error($error);
205 } else {
206 return $sqspell_output;
207 }
208 }
209
210 /**
211 * Prepares string for ispell/aspell parsing
212 *
213 * Function adds an extra space at the beginning of each line. This way
214 * ispell/aspell don't treat these as command characters.
215 * @param string $sText
216 * @return string
217 */
218 function prepare_text($sText) {
219 // prepend space to every sqspell_new_text line
220 $sText = str_replace("\r\n","\n",$sText);
221 $ret = '';
222 foreach (explode("\n",$sText) as $line) {
223 $ret.= ' ' . $line . "\n";
224 }
225 return $ret;
226 }
227
228 /**
229 * Checks block of text
230 * @param string $sText text
231 * @return array
232 */
233 function check_text($sText) {
234 $this->missed_words = array();
235
236 $sText = $this->prepare_text($sText);
237
238 if ($this->use_proc_open) {
239 $sqspell_output = $this->proc_open_spell($sText);
240 } else {
241 $sqspell_output = $this->exec_spell($sText);
242 }
243
244 /**
245 * Define some variables to be used during the processing.
246 */
247 $current_line=0;
248 /**
249 * Now we process the output of sqspell_command (ispell or aspell in
250 * ispell compatibility mode, whichever). I'm going to be scarce on
251 * comments here, since you can just look at the ispell/aspell output
252 * and figure out what's going on. ;) The best way to describe this is
253 * "Dark Magic".
254 */
255 for ($i=0; $i<sizeof($sqspell_output); $i++){
256 switch (substr($sqspell_output[$i], 0, 1)){
257 /**
258 * Line is empty.
259 * Ispell adds empty lines when an end of line is reached
260 */
261 case '':
262 $current_line++;
263 break;
264 /**
265 * Line begins with "&".
266 * This means there's a misspelled word and a few suggestions.
267 */
268 case '&':
269 list($left, $right) = explode(": ", $sqspell_output[$i]);
270 $tmparray = explode(" ", $left);
271 $sqspell_word=$tmparray[1];
272 /**
273 * Check if the word is in user dictionary.
274 */
275 if (! in_array($sqspell_word,$this->userdic)){
276 $sqspell_symb=intval($tmparray[3])-1;
277 // add suggestions
278 if (!isset($this->missed_words[$sqspell_word])) {
279 foreach(explode(',',$right) as $word) {
280 $this->missed_words[$sqspell_word]['suggestions'][] = trim($word);
281 }
282 }
283 // add location
284 $this->missed_words[$sqspell_word]['locations'][] = "$current_line:$sqspell_symb";
285 }
286 break;
287 /**
288 * Line begins with "#".
289 * This means a misspelled word and no suggestions.
290 */
291 case '#':
292 $tmparray = explode(" ", $sqspell_output[$i]);
293 $sqspell_word=$tmparray[1];
294 /**
295 *
296 * Check if the word is in user dictionary.
297 */
298 if (!in_array($sqspell_word,$this->userdic)){
299 $sqspell_symb=intval($tmparray[2])-1;
300 // no suggestions
301 $this->missed_words[$sqspell_word]['suggestions'] = array();
302 // add location
303 $this->missed_words[$sqspell_word]['locations'][] = "$current_line:$sqspell_symb";
304 }
305 break;
306 }
307 }
308 return $this->missed_words;
309 }
310 }
311
312
313 /**
314 * Define the command used to spellcheck the document.
315 */
316 #$sqspell_command=$SQSPELL_APP[$sqspell_use_app];