5100704d |
1 | <?php |
4b4abf93 |
2 | |
35586184 |
3 | /** |
4 | * abook_local_file.php |
5 | * |
47ccfad4 |
6 | * @copyright © 1999-2006 The SquirrelMail Project Team |
4b4abf93 |
7 | * @license http://opensource.org/licenses/gpl-license.php GNU Public License |
a9d318b0 |
8 | * @version $Id$ |
d6c32258 |
9 | * @package squirrelmail |
a9d318b0 |
10 | * @subpackage addressbook |
35586184 |
11 | */ |
5100704d |
12 | |
d6c32258 |
13 | /** |
147e5af3 |
14 | * Backend for address book as a pipe separated file |
15 | * |
16 | * Stores the address book in a local file |
17 | * |
18 | * An array with the following elements must be passed to |
19 | * the class constructor (elements marked ? are optional): |
20 | *<pre> |
21 | * filename => path to addressbook file |
22 | * ? create => if true: file is created if it does not exist. |
23 | * ? umask => umask set before opening file. |
675357d2 |
24 | * ? name => name of address book. |
91e0dccc |
25 | * ? detect_writeable => detect address book access permissions by |
675357d2 |
26 | * checking file permissions. |
27 | * ? writeable => allow writing into address book. Used only when |
28 | * detect_writeable is set to false. |
e59a9c41 |
29 | * ? listing => enable/disable listing |
147e5af3 |
30 | *</pre> |
31 | * NOTE. This class should not be used directly. Use the |
32 | * "AddressBook" class instead. |
8f6f9ba5 |
33 | * @package squirrelmail |
d6c32258 |
34 | */ |
35586184 |
35 | class abook_local_file extends addressbook_backend { |
4272758c |
36 | /** |
37 | * Backend type |
91e0dccc |
38 | * @var string |
4272758c |
39 | */ |
06b4facd |
40 | var $btype = 'local'; |
4272758c |
41 | /** |
42 | * Backend name |
43 | * @var string |
44 | */ |
06b4facd |
45 | var $bname = 'local_file'; |
46 | |
4272758c |
47 | /** |
48 | * File used to store data |
49 | * @var string |
50 | */ |
51 | var $filename = ''; |
52 | /** |
53 | * File handle |
54 | * @var object |
55 | */ |
06b4facd |
56 | var $filehandle = 0; |
4272758c |
57 | /** |
58 | * Create file, if it not present |
59 | * @var bool |
60 | */ |
61 | var $create = false; |
62 | /** |
63 | * Detect, if address book is writeable by checking file permisions |
64 | * @var bool |
65 | */ |
66 | var $detect_writeable = true; |
67 | /** |
68 | * Control write access to address book |
69 | * |
70 | * Option does not have any effect, if 'detect_writeable' is 'true' |
71 | * @var bool |
72 | */ |
73 | var $writeable = false; |
e59a9c41 |
74 | /** |
75 | * controls listing of address book |
76 | * @var bool |
77 | */ |
78 | var $listing = true; |
4272758c |
79 | /** |
80 | * Umask of the file |
81 | * @var string |
82 | */ |
06b4facd |
83 | var $umask; |
7311c377 |
84 | /** |
85 | * Sets max entry size (number of bytes used for all address book fields |
86 | * (including escapes) + 4 delimiters + 1 linefeed) |
87 | * @var integer |
88 | * @since 1.5.2 |
89 | */ |
90 | var $line_length = 2048; |
06b4facd |
91 | |
92 | /* ========================== Private ======================= */ |
93 | |
147e5af3 |
94 | /** |
95 | * Constructor |
96 | * @param array $param backend options |
97 | * @return bool |
98 | */ |
06b4facd |
99 | function abook_local_file($param) { |
100 | $this->sname = _("Personal address book"); |
101 | $this->umask = Umask(); |
102 | |
103 | if(is_array($param)) { |
104 | if(empty($param['filename'])) { |
105 | return $this->set_error('Invalid parameters'); |
106 | } |
107 | if(!is_string($param['filename'])) { |
108 | return $this->set_error($param['filename'] . ': '. |
109 | _("Not a file name")); |
110 | } |
111 | |
112 | $this->filename = $param['filename']; |
113 | |
147e5af3 |
114 | if(isset($param['create'])) { |
115 | $this->create = $param['create']; |
06b4facd |
116 | } |
117 | if(isset($param['umask'])) { |
118 | $this->umask = $param['umask']; |
119 | } |
4272758c |
120 | if(isset($param['name'])) { |
06b4facd |
121 | $this->sname = $param['name']; |
122 | } |
4272758c |
123 | if(isset($param['detect_writeable'])) { |
124 | $this->detect_writeable = $param['detect_writeable']; |
125 | } |
126 | if(!empty($param['writeable'])) { |
127 | $this->writeable = $param['writeable']; |
128 | } |
e59a9c41 |
129 | if(isset($param['listing'])) { |
130 | $this->listing = $param['listing']; |
131 | } |
7311c377 |
132 | if(isset($param['line_length']) && ! empty($param['line_length'])) { |
133 | $this->line_length = (int) $param['line_length']; |
134 | } |
62f7daa5 |
135 | |
06b4facd |
136 | $this->open(true); |
137 | } else { |
138 | $this->set_error('Invalid argument to constructor'); |
139 | } |
140 | } |
141 | |
147e5af3 |
142 | /** |
143 | * Open the addressbook file and store the file pointer. |
62f7daa5 |
144 | * Use $file as the file to open, or the class' own |
145 | * filename property. If $param is empty and file is |
147e5af3 |
146 | * open, do nothing. |
147 | * @param bool $new is file already opened |
148 | * @return bool |
149 | */ |
06b4facd |
150 | function open($new = false) { |
151 | $this->error = ''; |
152 | $file = $this->filename; |
153 | $create = $this->create; |
d5d43e57 |
154 | $fopenmode = (($this->writeable && is_writable($file)) ? 'a+' : 'r'); |
62f7daa5 |
155 | |
06b4facd |
156 | /* Return true is file is open and $new is unset */ |
157 | if($this->filehandle && !$new) { |
158 | return true; |
159 | } |
62f7daa5 |
160 | |
06b4facd |
161 | /* Check that new file exitsts */ |
162 | if((!(file_exists($file) && is_readable($file))) && !$create) { |
163 | return $this->set_error("$file: " . _("No such file or directory")); |
164 | } |
62f7daa5 |
165 | |
06b4facd |
166 | /* Close old file, if any */ |
167 | if($this->filehandle) { $this->close(); } |
62f7daa5 |
168 | |
06b4facd |
169 | umask($this->umask); |
4272758c |
170 | if (! $this->detect_writeable) { |
171 | $fh = @fopen($file,$fopenmode); |
172 | if ($fh) { |
173 | $this->filehandle = &$fh; |
174 | $this->filename = $file; |
175 | } else { |
176 | return $this->set_error("$file: " . _("Open failed")); |
177 | } |
06b4facd |
178 | } else { |
4272758c |
179 | /* Open file. First try to open for reading and writing, |
180 | * but fall back to read only. */ |
181 | $fh = @fopen($file, 'a+'); |
06b4facd |
182 | if($fh) { |
183 | $this->filehandle = &$fh; |
184 | $this->filename = $file; |
4272758c |
185 | $this->writeable = true; |
06b4facd |
186 | } else { |
4272758c |
187 | $fh = @fopen($file, 'r'); |
188 | if($fh) { |
189 | $this->filehandle = &$fh; |
190 | $this->filename = $file; |
191 | $this->writeable = false; |
192 | } else { |
193 | return $this->set_error("$file: " . _("Open failed")); |
194 | } |
06b4facd |
195 | } |
196 | } |
197 | return true; |
198 | } |
199 | |
147e5af3 |
200 | /** Close the file and forget the filehandle */ |
06b4facd |
201 | function close() { |
202 | @fclose($this->filehandle); |
203 | $this->filehandle = 0; |
204 | $this->filename = ''; |
205 | $this->writable = false; |
206 | } |
207 | |
147e5af3 |
208 | /** Lock the datafile - try 20 times in 5 seconds */ |
06b4facd |
209 | function lock() { |
210 | for($i = 0 ; $i < 20 ; $i++) { |
62f7daa5 |
211 | if(flock($this->filehandle, 2 + 4)) |
06b4facd |
212 | return true; |
213 | else |
214 | usleep(250000); |
215 | } |
216 | return false; |
217 | } |
218 | |
147e5af3 |
219 | /** Unlock the datafile */ |
06b4facd |
220 | function unlock() { |
221 | return flock($this->filehandle, 3); |
222 | } |
223 | |
147e5af3 |
224 | /** |
225 | * Overwrite the file with data from $rows |
226 | * NOTE! Previous locks are broken by this function |
227 | * @param array $rows new data |
228 | * @return bool |
229 | */ |
06b4facd |
230 | function overwrite(&$rows) { |
01265fba |
231 | $this->unlock(); |
dabef6fd |
232 | $newfh = @fopen($this->filename.'.tmp', 'w'); |
233 | |
06b4facd |
234 | if(!$newfh) { |
dabef6fd |
235 | return $this->set_error($this->filename. '.tmp:' . _("Open failed")); |
06b4facd |
236 | } |
62f7daa5 |
237 | |
dabef6fd |
238 | for($i = 0, $cnt=sizeof($rows) ; $i < $cnt ; $i++) { |
06b4facd |
239 | if(is_array($rows[$i])) { |
dabef6fd |
240 | for($j = 0, $cnt_part=count($rows[$i]) ; $j < $cnt_part ; $j++) { |
77ec28e9 |
241 | $rows[$i][$j] = $this->quotevalue($rows[$i][$j]); |
242 | } |
3ecad5e6 |
243 | $tmpwrite = sq_fwrite($newfh, join('|', $rows[$i]) . "\n"); |
244 | if ($tmpwrite === FALSE) { |
dabef6fd |
245 | return $this->set_error($this->filename . '.tmp:' . _("Write failed")); |
246 | } |
06b4facd |
247 | } |
62f7daa5 |
248 | } |
06b4facd |
249 | |
250 | fclose($newfh); |
baa59994 |
251 | if (!@copy($this->filename . '.tmp' , $this->filename)) { |
dabef6fd |
252 | return $this->set_error($this->filename . ':' . _("Unable to update")); |
baa59994 |
253 | } |
dabef6fd |
254 | @unlink($this->filename . '.tmp'); |
06b4facd |
255 | $this->unlock(); |
256 | $this->open(true); |
257 | return true; |
258 | } |
62f7daa5 |
259 | |
06b4facd |
260 | /* ========================== Public ======================== */ |
62f7daa5 |
261 | |
147e5af3 |
262 | /** |
263 | * Search the file |
264 | * @param string $expr search expression |
265 | * @return array search results |
266 | */ |
06b4facd |
267 | function search($expr) { |
268 | |
269 | /* To be replaced by advanded search expression parsing */ |
270 | if(is_array($expr)) { return; } |
62f7daa5 |
271 | |
327e2d96 |
272 | // don't allow wide search when listing is disabled. |
273 | if ($expr=='*' && ! $this->listing) |
274 | return array(); |
275 | |
06b4facd |
276 | /* Make regexp from glob'ed expression |
277 | * May want to quote other special characters like (, ), -, [, ], etc. */ |
278 | $expr = str_replace('?', '.', $expr); |
279 | $expr = str_replace('*', '.*', $expr); |
62f7daa5 |
280 | |
06b4facd |
281 | $res = array(); |
282 | if(!$this->open()) { |
283 | return false; |
284 | } |
285 | @rewind($this->filehandle); |
62f7daa5 |
286 | |
7311c377 |
287 | while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
288 | if (count($row)<5) { |
289 | /** |
290 | * address book is corrupted. |
291 | */ |
292 | global $oTemplate; |
293 | error_box(_("Address book is corrupted. Required fields are missing.")); |
294 | $oTemplate->display('footer.tpl'); |
295 | die(); |
296 | } else { |
297 | $line = join(' ', $row); |
298 | /** |
299 | * TODO: regexp search is supported only in local_file backend. |
300 | * Do we check format of regexp or ignore errors? |
301 | */ |
302 | // errors on eregi call are suppressed in order to prevent display of regexp compilation errors |
303 | if(@eregi($expr, $line)) { |
304 | array_push($res, array('nickname' => $row[0], |
305 | 'name' => $this->fullname($row[1], $row[2]), |
306 | 'firstname' => $row[1], |
307 | 'lastname' => $row[2], |
308 | 'email' => $row[3], |
309 | 'label' => $row[4], |
310 | 'backend' => $this->bnum, |
311 | 'source' => &$this->sname)); |
312 | } |
06b4facd |
313 | } |
314 | } |
62f7daa5 |
315 | |
06b4facd |
316 | return $res; |
317 | } |
62f7daa5 |
318 | |
147e5af3 |
319 | /** |
320 | * Lookup alias |
321 | * @param string $alias alias |
322 | * @return array search results |
323 | */ |
06b4facd |
324 | function lookup($alias) { |
325 | if(empty($alias)) { |
326 | return array(); |
327 | } |
328 | |
329 | $alias = strtolower($alias); |
62f7daa5 |
330 | |
06b4facd |
331 | $this->open(); |
332 | @rewind($this->filehandle); |
62f7daa5 |
333 | |
7311c377 |
334 | while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
335 | if (count($row)<5) { |
336 | /** |
337 | * address book is corrupted. |
338 | */ |
339 | global $oTemplate; |
340 | error_box(_("Address book is corrupted. Required fields are missing.")); |
341 | $oTemplate->display('footer.tpl'); |
342 | die(); |
343 | } else { |
344 | if(strtolower($row[0]) == $alias) { |
345 | return array('nickname' => $row[0], |
346 | 'name' => $this->fullname($row[1], $row[2]), |
347 | 'firstname' => $row[1], |
348 | 'lastname' => $row[2], |
349 | 'email' => $row[3], |
350 | 'label' => $row[4], |
351 | 'backend' => $this->bnum, |
352 | 'source' => &$this->sname); |
353 | } |
06b4facd |
354 | } |
355 | } |
62f7daa5 |
356 | |
06b4facd |
357 | return array(); |
358 | } |
359 | |
147e5af3 |
360 | /** |
361 | * List all addresses |
362 | * @return array list of all addresses |
363 | */ |
06b4facd |
364 | function list_addr() { |
365 | $res = array(); |
e59a9c41 |
366 | |
367 | if(isset($this->listing) && !$this->listing) { |
368 | return array(); |
369 | } |
370 | |
06b4facd |
371 | $this->open(); |
372 | @rewind($this->filehandle); |
62f7daa5 |
373 | |
7311c377 |
374 | while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
375 | if (count($row)<5) { |
376 | /** |
377 | * address book is corrupted. Don't be nice to people that |
378 | * violate address book formating. |
379 | */ |
380 | global $oTemplate; |
381 | error_box(_("Address book is corrupted. Required fields are missing.")); |
382 | $oTemplate->display('footer.tpl'); |
383 | die(); |
384 | } else { |
385 | array_push($res, array('nickname' => $row[0], |
386 | 'name' => $this->fullname($row[1], $row[2]), |
387 | 'firstname' => $row[1], |
388 | 'lastname' => $row[2], |
389 | 'email' => $row[3], |
390 | 'label' => $row[4], |
391 | 'backend' => $this->bnum, |
392 | 'source' => &$this->sname)); |
393 | } |
06b4facd |
394 | } |
395 | return $res; |
396 | } |
397 | |
147e5af3 |
398 | /** |
399 | * Add address |
400 | * @param array $userdata new data |
401 | * @return bool |
402 | */ |
06b4facd |
403 | function add($userdata) { |
404 | if(!$this->writeable) { |
35235328 |
405 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
406 | } |
407 | /* See if user exists already */ |
408 | $ret = $this->lookup($userdata['nickname']); |
409 | if(!empty($ret)) { |
2706a0b1 |
410 | // i18n: don't use html formating in translation |
411 | return $this->set_error(sprintf(_("User \"%s\" already exists"),$ret['nickname'])); |
06b4facd |
412 | } |
62f7daa5 |
413 | |
06b4facd |
414 | /* Here is the data to write */ |
77ec28e9 |
415 | $data = $this->quotevalue($userdata['nickname']) . '|' . |
416 | $this->quotevalue($userdata['firstname']) . '|' . |
8419c13b |
417 | $this->quotevalue((!empty($userdata['lastname'])?$userdata['lastname']:'')) . '|' . |
77ec28e9 |
418 | $this->quotevalue($userdata['email']) . '|' . |
8419c13b |
419 | $this->quotevalue((!empty($userdata['label'])?$userdata['label']:'')); |
77ec28e9 |
420 | |
06b4facd |
421 | /* Strip linefeeds */ |
422 | $data = ereg_replace("[\r\n]", ' ', $data); |
7311c377 |
423 | |
424 | /** |
425 | * Make sure that entry fits into allocated record space. |
426 | * One byte is reserved for linefeed |
427 | */ |
428 | if (strlen($data) >= $this->line_length) { |
429 | return $this->set_error(_("Address book entry is too big")); |
430 | } |
431 | |
06b4facd |
432 | /* Add linefeed at end */ |
433 | $data = $data . "\n"; |
62f7daa5 |
434 | |
06b4facd |
435 | /* Reopen file, just to be sure */ |
436 | $this->open(true); |
437 | if(!$this->writeable) { |
35235328 |
438 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
439 | } |
62f7daa5 |
440 | |
06b4facd |
441 | /* Lock the file */ |
442 | if(!$this->lock()) { |
443 | return $this->set_error(_("Could not lock datafile")); |
444 | } |
62f7daa5 |
445 | |
06b4facd |
446 | /* Write */ |
3ecad5e6 |
447 | $r = sq_fwrite($this->filehandle, $data); |
62f7daa5 |
448 | |
06b4facd |
449 | /* Unlock file */ |
450 | $this->unlock(); |
62f7daa5 |
451 | |
3ecad5e6 |
452 | /* Test write result */ |
453 | if($r === FALSE) { |
147e5af3 |
454 | /* Fail */ |
35235328 |
455 | $this->set_error(_("Write to address book failed")); |
147e5af3 |
456 | return FALSE; |
457 | } |
62f7daa5 |
458 | |
3ecad5e6 |
459 | return TRUE; |
06b4facd |
460 | } |
461 | |
147e5af3 |
462 | /** |
463 | * Delete address |
464 | * @param string $alias alias that has to be deleted |
465 | * @return bool |
466 | */ |
06b4facd |
467 | function remove($alias) { |
468 | if(!$this->writeable) { |
35235328 |
469 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
470 | } |
62f7daa5 |
471 | |
06b4facd |
472 | /* Lock the file to make sure we're the only process working |
473 | * on it. */ |
474 | if(!$this->lock()) { |
475 | return $this->set_error(_("Could not lock datafile")); |
476 | } |
62f7daa5 |
477 | |
06b4facd |
478 | /* Read file into memory, ignoring nicknames to delete */ |
479 | @rewind($this->filehandle); |
480 | $i = 0; |
481 | $rows = array(); |
7311c377 |
482 | while($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
06b4facd |
483 | if(!in_array($row[0], $alias)) { |
484 | $rows[$i++] = $row; |
485 | } |
486 | } |
62f7daa5 |
487 | |
06b4facd |
488 | /* Write data back */ |
489 | if(!$this->overwrite($rows)) { |
490 | $this->unlock(); |
491 | return false; |
492 | } |
62f7daa5 |
493 | |
06b4facd |
494 | $this->unlock(); |
495 | return true; |
496 | } |
497 | |
147e5af3 |
498 | /** |
499 | * Modify address |
500 | * @param string $alias modified alias |
501 | * @param array $userdata new data |
502 | * @return bool true, if operation successful |
503 | */ |
06b4facd |
504 | function modify($alias, $userdata) { |
505 | if(!$this->writeable) { |
35235328 |
506 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
507 | } |
62f7daa5 |
508 | |
06b4facd |
509 | /* See if user exists */ |
510 | $ret = $this->lookup($alias); |
511 | if(empty($ret)) { |
2706a0b1 |
512 | // i18n: don't use html formating in translation |
513 | return $this->set_error(sprintf(_("User \"%s\" does not exist"),$alias)); |
06b4facd |
514 | } |
62f7daa5 |
515 | |
06b4facd |
516 | /* Lock the file to make sure we're the only process working |
517 | * on it. */ |
518 | if(!$this->lock()) { |
519 | return $this->set_error(_("Could not lock datafile")); |
520 | } |
62f7daa5 |
521 | |
7311c377 |
522 | /* calculate userdata size */ |
523 | $data = $this->quotevalue($userdata['nickname']) . '|' |
524 | . $this->quotevalue($userdata['firstname']) . '|' |
525 | . $this->quotevalue((!empty($userdata['lastname'])?$userdata['lastname']:'')) . '|' |
526 | . $this->quotevalue($userdata['email']) . '|' |
527 | . $this->quotevalue((!empty($userdata['label'])?$userdata['label']:'')); |
528 | /* make sure that it fits into allocated space */ |
529 | if (strlen($data) >= $this->line_length) { |
530 | return $this->set_error(_("Address book entry is too big")); |
531 | } |
532 | |
62f7daa5 |
533 | /* Read file into memory, modifying the data for the |
06b4facd |
534 | * user identified by $alias */ |
535 | $this->open(true); |
536 | @rewind($this->filehandle); |
537 | $i = 0; |
538 | $rows = array(); |
7311c377 |
539 | while($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
06b4facd |
540 | if(strtolower($row[0]) != strtolower($alias)) { |
541 | $rows[$i++] = $row; |
542 | } else { |
543 | $rows[$i++] = array(0 => $userdata['nickname'], |
544 | 1 => $userdata['firstname'], |
8419c13b |
545 | 2 => (!empty($userdata['lastname'])?$userdata['lastname']:''), |
62f7daa5 |
546 | 3 => $userdata['email'], |
8419c13b |
547 | 4 => (!empty($userdata['label'])?$userdata['label']:'')); |
06b4facd |
548 | } |
549 | } |
62f7daa5 |
550 | |
06b4facd |
551 | /* Write data back */ |
552 | if(!$this->overwrite($rows)) { |
553 | $this->unlock(); |
554 | return false; |
555 | } |
62f7daa5 |
556 | |
06b4facd |
557 | $this->unlock(); |
558 | return true; |
559 | } |
62f7daa5 |
560 | |
147e5af3 |
561 | /** |
562 | * Function for quoting values before saving |
563 | * @param string $value string that has to be quoted |
564 | * @param string quoted string |
565 | */ |
77ec28e9 |
566 | function quotevalue($value) { |
567 | /* Quote the field if it contains | or ". Double quotes need to |
568 | * be replaced with "" */ |
569 | if(ereg("[|\"]", $value)) { |
570 | $value = '"' . str_replace('"', '""', $value) . '"'; |
571 | } |
572 | return $value; |
573 | } |
574 | |
06b4facd |
575 | } /* End of class abook_local_file */ |
8419c13b |
576 | ?> |