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
11 if (!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.
15 if (!defined('FEDAUTH_CHANGE_TYPE_CREATE'))  define('FEDAUTH_CHANGE_TYPE_CREATE',  'C');
16 if (!defined('FEDAUTH_CHANGE_TYPE_DELETE'))  define('FEDAUTH_CHANGE_TYPE_DELETE',  'D');
17 if (!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  */
25 class 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