1<?php
2/**
3 * Copyright 2005-2016 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2005-2016 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   SpellChecker
12 */
13
14/**
15 * A spellcheck driver for the aspell/ispell binary.
16 *
17 * @author    Chuck Hagenbuch <chuck@horde.org>
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 2005-2016 Horde LLC
21 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
22 * @package   SpellChecker
23 */
24class Horde_SpellChecker_Aspell extends Horde_SpellChecker
25{
26    /**
27     * @param array $args  Additional arguments:
28     *   - path: (string) Path to the aspell binary.
29     */
30    public function __construct(array $args = array())
31    {
32        parent::__construct(array_merge(array(
33            'path' => 'aspell'
34        ), $args));
35    }
36
37    /**
38     */
39    public function spellCheck($text)
40    {
41        $ret = array(
42            'bad' => array(),
43            'suggestions' => array()
44        );
45
46        if ($this->_params['html']) {
47            $input = strtr($text, "\n", ' ');
48        } else {
49            $words = $this->_getWords($text);
50            if (!count($words)) {
51                return $ret;
52            }
53            $input = implode(' ', $words);
54        }
55
56        // Descriptor array.
57        $descspec = array(
58            0 => array('pipe', 'r'),
59            1 => array('pipe', 'w'),
60            2 => array('pipe', 'w')
61        );
62
63        $process = proc_open($this->_cmd(), $descspec, $pipes);
64        if (!is_resource($process)) {
65            throw new Horde_SpellChecker_Exception('Spellcheck failed. Command line: ' . $this->_cmd());
66        }
67
68        // The '^' character tells aspell to spell check the entire line.
69        fwrite($pipes[0], '^' . $input);
70        fclose($pipes[0]);
71
72        // Read stdout.
73        $out = '';
74        while (!feof($pipes[1])) {
75            $out .= fread($pipes[1], 8192);
76        }
77        fclose($pipes[1]);
78
79        // Read stderr.
80        $err = '';
81        while (!feof($pipes[2])) {
82            $err .= fread($pipes[2], 8192);
83        }
84        fclose($pipes[2]);
85
86        // We can't rely on the return value of proc_close:
87        // http://bugs.php.net/bug.php?id=29123
88        proc_close($process);
89
90        if (strlen($out) === 0) {
91            throw new Horde_SpellChecker_Exception('Spellcheck failed. Command line: ' . $this->_cmd());
92        }
93
94        // Parse output.
95        foreach (array_map('trim', explode("\n", $out)) as $line) {
96            if (!strlen($line)) {
97                continue;
98            }
99
100            @list(,$word,) = explode(' ', $line, 3);
101
102            if ($this->_inLocalDictionary($word) ||
103                in_array($word, $ret['bad'])) {
104                continue;
105            }
106
107            switch ($line[0]) {
108            case '#':
109                // Misspelling with no suggestions.
110                $ret['bad'][] = $word;
111                $ret['suggestions'][] = array();
112                break;
113
114            case '&':
115                // Suggestions.
116                $ret['bad'][] = $word;
117                $ret['suggestions'][] = array_slice(explode(', ', substr($line, strpos($line, ':') + 2)), 0, $this->_params['maxSuggestions']);
118                break;
119            }
120        }
121
122        return $ret;
123    }
124
125    /**
126     * Create the command line string.
127     *
128     * @return string  The command to run.
129     */
130    protected function _cmd()
131    {
132        $args = array('-a', '--encoding=UTF-8');
133
134        switch ($this->_params['suggestMode']) {
135        case self::SUGGEST_FAST:
136            $args[] = '--sug-mode=fast';
137            break;
138
139        case self::SUGGEST_SLOW:
140            $args[] = '--sug-mode=bad-spellers';
141            break;
142
143        default:
144            $args[] = '--sug-mode=normal';
145        }
146
147        $args[] = '--lang=' . escapeshellarg($this->_params['locale']);
148        $args[] = '--ignore=' . escapeshellarg(max($this->_params['minLength'] - 1, 0));
149
150        if ($this->_params['html']) {
151            $args[] = '-H';
152            $args[] = '--rem-html-check=alt';
153        }
154
155        return escapeshellcmd($this->_params['path']) . ' ' .
156               implode(' ', $args);
157    }
158
159}
160