1<?php
2/**
3 * Federated Login for DokuWiki - file-based data storage class
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @link       http://www.dokuwiki.org/plugin:fedauth
7 * @author     Aoi Karasu <aoikarasu@gmail.com>
8 */
9
10// The filestore requires Dokuwiki
11if (!defined('DOKU_INC')) die();
12
13// Constants for known core changelog line types.
14// Use these in place of string literals for more readable code.
15if (!defined('FEDAUTH_CHANGE_TYPE_CREATE'))  define('FEDAUTH_CHANGE_TYPE_CREATE',  'C');
16if (!defined('FEDAUTH_CHANGE_TYPE_DELETE'))  define('FEDAUTH_CHANGE_TYPE_DELETE',  'D');
17if (!defined('FEDAUTH_CHANGE_TYPE_REFRESH')) define('FEDAUTH_CHANGE_TYPE_REFRESH', 'R');
18
19/**
20 * Federated login file-based data storage class. Handles all
21 * i/o operations performed on authentication user data.
22 *
23 * @author     Aoi Karasu <aoikarasu@gmail.com>
24 */
25class fa_filestore {
26
27    /**
28     * Base data storage path, eg. [dw_root]/data/users/
29     */
30    var $root = '';
31
32    /**
33     * Base providers configuration path, eg. [dw_root]/conf/fedauth/
34     */
35    var $conf = '';
36
37    /**
38     * Array containing user identity entries.
39     */
40    var $userData = null;
41
42    /**
43     * Array containing the identities from single provider
44     * service associated with local usernames (accounts).
45     */
46    var $claimedIdentities = null;
47
48    /**
49     * Creates the class instance.
50     */
51    function __construct() {
52        $this->root = DOKU_INC . 'data/users/';
53        $this->conf = DOKU_CONF . 'fedauth/';
54        io_makeFileDir($this->root);
55        io_makeFileDir($this->conf);
56//        print "<pre>" . $this->root . "\n" . $this->conf . "</pre>";
57    }
58
59    /**
60     * Loads the identities from single provider service
61     * associated with local usernames (accounts) from a file.
62     *
63     * @param string $providerId provider identifier
64     * @return mixed identity+username pairs array or false on failure
65     */
66    function &getClaimedIdentities($providerId) {
67        if (is_array($this->claimedIdentities)) {
68            return $this->claimedIdentities;
69        }
70        $lines = @file_get_contents($this->provFN($providerId));
71        $lines = explode("\n", $lines);
72        if (empty($lines)) return false;
73
74        $data = array();
75        foreach ($lines as $value) {
76            $tmp = $this->parseClaimedIdentitiesLine($value);
77            if ($tmp !== false) {
78                $data[] = $tmp;
79            }
80        }
81        $this->claimedIdentities = $data;
82        return $this->claimedIdentities;
83    }
84
85    /**
86     * Returns a username associated with a claimed identity.
87     *
88     * @param string $providerId provider identifier
89     * @param string $claimedId claimed identifier
90     * @return mixed username or false on failure
91     */
92    function getUsernameByIdentity($providerId, $claimedId) {
93        if (($entries =& $this->getClaimedIdentities($providerId)) === false) return false;
94
95        $strip = array("\t", "\n");
96        $claimedId = str_replace($strip, '', $claimedId);
97        foreach ($entries as $entry) {
98            if ($entry['ident'] == $claimedId) {
99                return $entry['user'];
100            }
101        }
102        return false;
103    }
104
105    /**
106     * Parses a claimed identity line into it's components.
107     *
108     * @param string $line claimed identity line to parse
109     * @return mixed claimed identity entry array or false on failure
110     */
111    function parseClaimedIdentitiesLine($line) {
112        $tmp = explode("\t", $line);
113        if ($tmp!==false && count($tmp)>1) {
114            $info = array(
115                'ident' => $tmp[0], // user identity
116                'user'  => $tmp[1]); // local username
117            return $info;
118        }
119        return false;
120    }
121
122    /**
123     * Loads user authentication data from file.
124     *
125     * @return mixed user authentication data array or false on failure
126     */
127    function &getUserData() {
128        if (is_array($this->userData)) {
129            return $this->userData;
130        }
131        $lines = @file_get_contents($this->userFN());
132        $lines = explode("\n", $lines);
133        if (empty($lines)) return false;
134
135        $data = array();
136        foreach ($lines as $value) {
137            $tmp = $this->parseUserDataLine($value);
138            if ($tmp !== false) {
139                $data[] = $tmp;
140            }
141        }
142        $this->userData = $data;
143        return $this->userData;
144    }
145
146    /**
147     * Searches for user authentication data entry by identity.
148     *
149     * @param string $claimedId identity associated with the user data entry
150     * @return mixed user authentication data array or false on failure
151     */
152    function getUserDataEntry($claimedId) {
153        if (($entries =& $this->getUserData()) === false) return false;
154
155        $strip = array("\t", "\n");
156        $claimedId = str_replace($strip, '', $claimedId);
157        foreach ($entries as $entry) {
158            if ($entry['ident'] == $claimedId) {
159                return $entry;
160            }
161        }
162        return false;
163    }
164
165    /**
166     * Parses an user data line into it's components.
167     *
168     * @param string $line user data line to parse
169     * @return mixed user data entry array or false on failure
170     */
171    function parseUserDataLine($line) {
172        $tmp = explode("\t", $line);
173        if ($tmp!==false && count($tmp)>1) {
174            $info = array(
175                'id'    => $tmp[0], // provider id
176                'ident' => $tmp[1], // user identity
177                'last'  => (int)$tmp[2]); // last used
178            return $info;
179        }
180        return false;
181    }
182
183    /**
184     * Adds an entry to the user authentication data file.
185     *
186     * @param string $providerId identifier of the auth provider service
187     * @param string $claimedId user authentication identity
188     * @param int $date (optional) timestamp of the change
189     */
190    function addUserDataEntry($providerId, $claimedId, $date=null) {
191        if (!$date) $date = time(); //use current time if none supplied
192        $user = $_SERVER['REMOTE_USER'];
193
194        $strip = array("\t", "\n");
195        $data = array(
196            'id'    => $providerId, // provider id
197            'ident' => str_replace($strip, '', $claimedId), // user identity
198            'last'  => $date); // last used
199        $provline = array(
200            'ident' => $data['ident'],
201            'user'  => $user);
202
203        $dataline = implode("\t", $data) . "\n";
204        $provline = implode("\t", $provline) . "\n";
205        io_saveFile($this->userFN($user), $dataline, true); //user data
206        io_saveFile($this->provFN($providerId), $provline, true); //global provider identities
207        $this->addLogEntry($date, $providerId, $claimedId, FEDAUTH_CHANGE_TYPE_CREATE);
208
209        $this->userData = null; // force reload userData on next get
210    }
211
212    /**
213     * Deletes user authentication data entry from the data file by identity.
214     *
215     * @param string $claimedId identity associated with the user data entry
216     * @return mixed deleted entry or false on failure
217     */
218    function deleteUserDataEntry($claimedId) {
219        if ($entry = $this->getUserDataEntry($claimedId)) {
220            $user = $_SERVER['REMOTE_USER'];
221            $provline = array(
222                'ident' => $entry['ident'],
223                'user'  => $user);
224            $dataline = implode("\t", $entry) . "\n";
225            $provline = implode("\t", $provline) . "\n";
226            io_deleteFromFile($this->userFN($user), $dataline);
227            io_deleteFromFile($this->provFN($entry['id']), $provline);
228            $this->addLogEntry(time(), $entry['id'], $claimedId, FEDAUTH_CHANGE_TYPE_DELETE);
229            $this->userData = null; // force reload userData on next get
230            return $entry;
231        }
232        return false;
233    }
234
235    /**
236     * Updates last used time for user authentication data entry by identity.
237     *
238     * @param string $claimedId identity associated with the user data entry
239     */
240    function refreshUserDataEntry($claimedId) {
241        if ($entry = $this->getUserDataEntry($claimedId)) {
242            $user = $_SERVER['REMOTE_USER'];
243            $dataline = implode("\t", $entry) . "\n";
244            io_deleteFromFile($this->userFN($user), $dataline);
245            $entry['last'] = time();
246            $dataline = implode("\t", $entry) . "\n";
247            io_saveFile($this->userFN($user), $dataline, true);
248            $this->addLogEntry(time(), $entry['id'], $claimedId, FEDAUTH_CHANGE_TYPE_REFRESH);
249            $this->userData = null; // force reload userData on next get
250        }
251    }
252
253    /**
254     * Parses a changelog line into it's components.
255     *
256     * @param string $line changelog line to parse
257     * @return mixed changelog entry array or false on failure
258     */
259    function parseChangelogLine($line) {
260        $tmp = explode("\t", $line);
261        if ($tmp!==false && count($tmp)>1) {
262            $info = array(
263                'date'  => (int)$tmp[0],
264                'ip'    => $tmp[1],
265                'type'  => $tmp[2],
266                'id'    => $tmp[3],
267                'ident' => $tmp[4],
268                'user'  => $tmp[5]);
269            return $info;
270        }
271        return false;
272    }
273
274    /**
275     * Adds an entry to the user authentication data changelog.
276     *
277     * @param int $date timestamp of the change
278     * @param string $providerId identifier of the auth provider service
279     * @param string $claimedId user authentication identity
280     * @param string $type type of the change see FEDAUTH_CHANGE_TYPE_*
281     * @param bool $isExternal (optional) is change made by user or as result of internal cleanup
282     */
283    function addLogEntry($date, $providerId, $claimedId, $type, $isExternal=true) {
284        if (!$date) $date = time(); //use current time if none supplied
285        $remote = ($isExternal) ? clientIP(true) : '127.0.0.1';
286        $user   = ($isExternal) ? $_SERVER['REMOTE_USER'] : '';
287
288        $strip = array("\t", "\n");
289        $logline = array(
290            'date'  => $date,
291            'ip'    => $remote,
292            'type'  => str_replace($strip, '', $type),
293            'id'    => $providerId,
294            'ident' => str_replace($strip, '', $claimedId),
295            'user'  => $user);
296
297        // add the changelog line
298        $logline = implode("\t", $logline) . "\n";
299        $refresh = ($type == FEDAUTH_CHANGE_TYPE_REFRESH);
300        io_saveFile($this->userFN($user, $refresh ? 'activity' : 'changes'), $logline, true); //user data changelog
301        // don't use global provider changelog to log sign-ins; too much data
302        if (!$refresh) {
303            io_saveFile($this->provFN($providerId, 'changes'), $logline, true);
304        }
305    }
306
307    /**
308     * Returns the full path to the provider file with user identities.
309     * Note: use the extension parameter to get related (eg. meta) file path.
310     *
311     * @param string $provid provider identifier
312     * @param string $ext (optional) file extension
313     * @return string the full path
314     */
315    function provFN($provid, $ext='conf') {
316        return $this->conf . $provid . '.' . $ext;
317    }
318
319    /**
320     * Returns the full path to the user file with all associated idenities.
321     * Note: use the extension parameter to get related (eg. meta) file path.
322     *
323     * @param string $username (optional) the username; leave empty to autodetect
324     * @param string $ext (optional) file extension
325     * @return string the full path
326     */
327    function userFN($username='', $ext='data') {
328        if (empty($username)) $username = $_SERVER['REMOTE_USER'];
329        return $this->root . substr($username, 0, 1) . '/' . $username . '/fedauth.' . $ext;
330    }
331
332} /* fa_filestore */
333
334/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
335