1<?php
2/**
3 * Copyright 2003-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (GPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/gpl.
7 *
8 * @category  Horde
9 * @copyright 2003-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/gpl GPL
11 * @package   Passwd
12 */
13
14/**
15 * Changes a user's password in a Pine password file.
16 *
17 * @author    Max Kalika <max@horde.org>
18 * @category  Horde
19 * @copyright 2003-2017 Horde LLC
20 * @license   http://www.horde.org/licenses/gpl GPL
21 * @package   Passwd
22 */
23class Passwd_Driver_Pine extends Passwd_Driver
24{
25    /** Lower boundary character. */
26    const FIRSTCH = 0x20;
27
28    /** Upper boundary character. */
29    const LASTCH = 0x7e;
30
31    /** Median character. */
32    const TABSZ = 0x5f;
33
34    /**
35     * Boolean which contains state of the ftp connection.
36     *
37     * @var boolean
38     */
39    protected $_connected = false;
40
41    /**
42     * Contents array of the pine password file.
43     *
44     * @var array
45     */
46    protected $_contents = array();
47
48    /**
49     * Horde_Vfs instance.
50     *
51     * @var VFS
52     */
53    protected $_ftp;
54
55    /**
56     */
57    public function __construct(array $params = array())
58    {
59        self::__construct(array_merge(array(
60            /* We self-encrypt here, so plaintext is needed. */
61            'encryption' => 'plain',
62            'show_encryption' => false,
63
64            /* Sensible FTP server parameters. */
65            'host' => 'localhost',
66            'port' => 21,
67            'path' => '',
68            'file' => '.pinepw',
69
70            /* Connect to FTP server using just-passed-in credentials?
71             * Only useful if using the composite driver and changing
72             * system (FTP) password prior to this one. */
73            'use_new_passwd' => false,
74
75            /* What host to look for on each line? */
76            'imaphost' => 'localhost'
77        ), $params));
78    }
79
80    /**
81     * Connects to the FTP server.
82     *
83     * @throws Passwd_Exception
84     */
85    protected function _connect($user, $password)
86    {
87        if ($this->_connected) {
88            return;
89        }
90
91        $params = array(
92            'username' => $user,
93            'password' => $password,
94            'hostspec' => $this->_params['host'],
95            'port' => $this->_params['port'],
96        );
97
98        try {
99            $this->_ftp = Horde_Vfs::factory('ftp', $params);
100            $this->_ftp->checkCredentials();
101        } catch (Horde_Vfs_Exception $e) {
102            throw new Passwd_Exception($e);
103        }
104
105        $this->_connected = true;
106    }
107
108    /**
109     * Disconnect from the FTP server.
110     *
111     * @throws Passwd_Exception
112     */
113    protected function _disconnect()
114    {
115        if ($this->_connected) {
116            try {
117                $this->_ftp->disconnect();
118            } catch (Horde_Vfs_Exception $e) {
119                throw new Passwd_Exception($e);
120            }
121            $this->_connected = false;
122        }
123    }
124
125    /**
126     * Decodes a Pine-encoded password string.
127     *
128     * The algorithm is borrowed from read_passfile() and xlate_out()
129     * functions in pine/imap.c file distributed in the Pine source archive.
130     *
131     * @param string $string  The contents of a pine-encoded password file.
132     *
133     * @return array  List of lines of decoded elements.
134     */
135    protected function _decode($string)
136    {
137        $list = array();
138
139        $lines = explode("\n", $string);
140        for ($n = 0; $n < sizeof($lines); $n++) {
141            $key = $n;
142            $tmp = $lines[$n];
143            for ($i = 0; $i < strlen($tmp); $i++) {
144                if ((ord($tmp[$i]) >= self::FIRSTCH) &&
145                    (ord($tmp[$i]) <= self::LASTCH)) {
146                    $xch  = ord($tmp[$i]) - ($dti = $key);
147                    $xch += ($xch < self::FIRSTCH - self::TABSZ)
148                        ? 2 * self::TABSZ
149                        : ($xch < self::FIRSTCH) ? self::TABSZ : 0;
150                    $dti  = ($xch - self::FIRSTCH) + $dti;
151                    $dti -= ($dti >= 2 * self::TABSZ)
152                        ? 2 * self::TABSZ
153                        : ($dti >= self::TABSZ) ? self::TABSZ : 0;
154                    $key  = $dti;
155                    $tmp[$i] = chr($xch);
156                }
157            }
158
159            if ($i && $tmp[$i - 1] == "\n") {
160                $tmp = substr($tmp, 0, -1);
161            }
162
163            $parts = explode("\t", $tmp);
164            if (count($parts) >= 4) {
165                $list[] = $parts;
166            }
167        }
168
169        return $list;
170    }
171
172    /**
173     * Encodes an array of elements into a Pine-readable password string.
174     *
175     * The algorithm is borrowed from write_passfile() and xlate_in()
176     * functions in pine/imap.c file distributed in the Pine source archive.
177     *
178     * @param array  $lines  List of lines of decoded elements.
179     *
180     * @return array  Contents of a pine-readable password file.
181     */
182    protected function _encode($lines)
183    {
184        $string = '';
185        for ($n = 0; $n < sizeof($lines); $n++) {
186            if (isset($lines[$n][4])) {
187                $lines[$n][4] = "\t" . $lines[$n][4];
188            } else {
189                $lines[$n][4] = '';
190            }
191
192            $key = $n;
193            $tmp = vsprintf("%.100s\t%.100s\t%.100s\t%d%s\n", $lines[$n]);
194            for ($i = 0; $i < strlen($tmp); $i++) {
195                $eti = $key;
196                if ((ord($tmp[$i]) >= self::FIRSTCH) &&
197                    (ord($tmp[$i]) <= self::LASTCH)) {
198                    $eti += ord($tmp[$i]) - self::FIRSTCH;
199                    $eti -= ($eti >= 2 * self::TABSZ)
200                        ? 2 * self::TABSZ
201                        : ($eti >= self::TABSZ) ? self::TABSZ : 0;
202                    $key  = $eti;
203                    $tmp[$i] = chr($eti + self::FIRSTCH);
204                 }
205            }
206
207            $string .= $tmp;
208        }
209
210        return $string;
211    }
212
213    /**
214     * Finds out if a username and password is valid.
215     *
216     * @param string $user         The userID to check.
217     * @param string $oldPassword  An old password to check.
218     *
219     * @throws Passwd_Exception
220     */
221    protected function _lookup($user, $oldPassword)
222    {
223        try {
224            $contents = $this->_ftp->read($this->_params['path'],
225                                          $this->_params['file']);
226        } catch (Horde_Vfs_Exception $e) {
227            throw new Passwd_Exception($e);
228        }
229
230        $this->_contents = $this->_decode($contents);
231        foreach ($this->_contents as $line) {
232            if ($line[1] == $user &&
233                (($line[2] == $this->_params['imaphost']) ||
234                 (!empty($line[4]) && $line[4] == $this->_params['imaphost']))) {
235                $this->_comparePasswords($line[0], $oldPassword);
236                return;
237            }
238        }
239
240        throw new Passwd_Exception(_("User not found."));
241    }
242
243    /**
244     * Modifies a pine password record for a user.
245     *
246     * @param string $user         The user whose record we will udpate.
247     * @param string $newPassword  The new password value to set.
248     *
249     * @throws Passwd_Exception
250     */
251    protected function _modify($user, $newPassword)
252    {
253        for ($i = 0; $i < sizeof($this->_contents); $i++) {
254            if ($this->_contents[$i][1] == $user &&
255                (($this->_contents[$i][2] == $this->_params['imaphost']) ||
256                 (!empty($this->_contents[$i][4]) &&
257                  $this->_contents[$i][4] == $this->_params['imaphost']))) {
258                $this->_contents[$i][0] = $newPassword;
259            }
260        }
261
262        $string = $this->_encode($this->_contents);
263        try {
264            $this->_ftp->writeData($this->_params['path'],
265                                   $this->_params['file'],
266                                   $string);
267        } catch (Horde_Vfs_Exception $e) {
268            throw new Passwd_Exception($e);
269        }
270    }
271
272    /**
273     */
274    protected function _changePassword($user, $oldpass, $newpass)
275    {
276        /* Connect to the ftp server. */
277        $this->_connect($user, $this->_params['use_new_passwd'] ? $newpass : $oldpass);
278        $this->_lookup($user, $oldpass);
279        $this->_modify($user, $newpass);
280        $this->_disconnect();
281    }
282
283}
284