5100704d |
1 | <?php |
4b4abf93 |
2 | |
35586184 |
3 | /** |
4 | * abook_local_file.php |
5 | * |
30460a05 |
6 | * @copyright 1999-2009 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) { |
232fb714 |
100 | $this->sname = _("Personal Address Book"); |
06b4facd |
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; |
0a496c76 |
154 | $fopenmode = (($this->writeable && sq_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'); |
53d36779 |
255 | @chmod($this->filename, 0600); |
06b4facd |
256 | $this->unlock(); |
257 | $this->open(true); |
258 | return true; |
259 | } |
62f7daa5 |
260 | |
06b4facd |
261 | /* ========================== Public ======================== */ |
62f7daa5 |
262 | |
147e5af3 |
263 | /** |
264 | * Search the file |
265 | * @param string $expr search expression |
266 | * @return array search results |
267 | */ |
06b4facd |
268 | function search($expr) { |
269 | |
270 | /* To be replaced by advanded search expression parsing */ |
271 | if(is_array($expr)) { return; } |
62f7daa5 |
272 | |
327e2d96 |
273 | // don't allow wide search when listing is disabled. |
274 | if ($expr=='*' && ! $this->listing) |
275 | return array(); |
276 | |
06b4facd |
277 | /* Make regexp from glob'ed expression |
278 | * May want to quote other special characters like (, ), -, [, ], etc. */ |
279 | $expr = str_replace('?', '.', $expr); |
280 | $expr = str_replace('*', '.*', $expr); |
62f7daa5 |
281 | |
06b4facd |
282 | $res = array(); |
283 | if(!$this->open()) { |
284 | return false; |
285 | } |
286 | @rewind($this->filehandle); |
62f7daa5 |
287 | |
7311c377 |
288 | while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
289 | if (count($row)<5) { |
290 | /** |
291 | * address book is corrupted. |
292 | */ |
293 | global $oTemplate; |
294 | error_box(_("Address book is corrupted. Required fields are missing.")); |
295 | $oTemplate->display('footer.tpl'); |
296 | die(); |
297 | } else { |
298 | $line = join(' ', $row); |
299 | /** |
300 | * TODO: regexp search is supported only in local_file backend. |
301 | * Do we check format of regexp or ignore errors? |
302 | */ |
b7910e12 |
303 | // errors on preg_match call are suppressed in order to prevent display of regexp compilation errors |
304 | if(@preg_match('/' . $expr . '/i', $line)) { |
7311c377 |
305 | array_push($res, array('nickname' => $row[0], |
306 | 'name' => $this->fullname($row[1], $row[2]), |
307 | 'firstname' => $row[1], |
308 | 'lastname' => $row[2], |
309 | 'email' => $row[3], |
310 | 'label' => $row[4], |
311 | 'backend' => $this->bnum, |
312 | 'source' => &$this->sname)); |
313 | } |
06b4facd |
314 | } |
315 | } |
62f7daa5 |
316 | |
06b4facd |
317 | return $res; |
318 | } |
62f7daa5 |
319 | |
147e5af3 |
320 | /** |
503c7650 |
321 | * Lookup an address by the indicated field. |
322 | * |
323 | * @param string $value The value to look up |
324 | * @param integer $field The field to look in, should be one |
325 | * of the SM_ABOOK_FIELD_* constants |
326 | * defined in include/constants.php |
327 | * (OPTIONAL; defaults to nickname field) |
bf55ebab |
328 | * NOTE: uniqueness is only guaranteed |
329 | * when the nickname field is used here; |
330 | * otherwise, the first matching address |
331 | * is returned. |
503c7650 |
332 | * |
333 | * @return array Array with lookup results when the value |
334 | * was found, an empty array if the value was |
335 | * not found. |
336 | * |
147e5af3 |
337 | */ |
503c7650 |
338 | function lookup($value, $field=SM_ABOOK_FIELD_NICKNAME) { |
339 | if(empty($value)) { |
06b4facd |
340 | return array(); |
341 | } |
342 | |
503c7650 |
343 | $value = strtolower($value); |
62f7daa5 |
344 | |
06b4facd |
345 | $this->open(); |
346 | @rewind($this->filehandle); |
62f7daa5 |
347 | |
7311c377 |
348 | while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
349 | if (count($row)<5) { |
350 | /** |
351 | * address book is corrupted. |
352 | */ |
353 | global $oTemplate; |
354 | error_box(_("Address book is corrupted. Required fields are missing.")); |
355 | $oTemplate->display('footer.tpl'); |
356 | die(); |
357 | } else { |
503c7650 |
358 | if(strtolower($row[$field]) == $value) { |
7311c377 |
359 | return array('nickname' => $row[0], |
360 | 'name' => $this->fullname($row[1], $row[2]), |
361 | 'firstname' => $row[1], |
362 | 'lastname' => $row[2], |
363 | 'email' => $row[3], |
364 | 'label' => $row[4], |
365 | 'backend' => $this->bnum, |
366 | 'source' => &$this->sname); |
367 | } |
06b4facd |
368 | } |
369 | } |
62f7daa5 |
370 | |
06b4facd |
371 | return array(); |
372 | } |
373 | |
147e5af3 |
374 | /** |
375 | * List all addresses |
376 | * @return array list of all addresses |
377 | */ |
06b4facd |
378 | function list_addr() { |
379 | $res = array(); |
e59a9c41 |
380 | |
381 | if(isset($this->listing) && !$this->listing) { |
382 | return array(); |
383 | } |
384 | |
06b4facd |
385 | $this->open(); |
386 | @rewind($this->filehandle); |
62f7daa5 |
387 | |
7311c377 |
388 | while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
389 | if (count($row)<5) { |
390 | /** |
391 | * address book is corrupted. Don't be nice to people that |
392 | * violate address book formating. |
393 | */ |
394 | global $oTemplate; |
395 | error_box(_("Address book is corrupted. Required fields are missing.")); |
396 | $oTemplate->display('footer.tpl'); |
397 | die(); |
398 | } else { |
399 | array_push($res, array('nickname' => $row[0], |
400 | 'name' => $this->fullname($row[1], $row[2]), |
401 | 'firstname' => $row[1], |
402 | 'lastname' => $row[2], |
403 | 'email' => $row[3], |
404 | 'label' => $row[4], |
405 | 'backend' => $this->bnum, |
406 | 'source' => &$this->sname)); |
407 | } |
06b4facd |
408 | } |
409 | return $res; |
410 | } |
411 | |
147e5af3 |
412 | /** |
413 | * Add address |
414 | * @param array $userdata new data |
415 | * @return bool |
416 | */ |
06b4facd |
417 | function add($userdata) { |
418 | if(!$this->writeable) { |
35235328 |
419 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
420 | } |
421 | /* See if user exists already */ |
422 | $ret = $this->lookup($userdata['nickname']); |
423 | if(!empty($ret)) { |
2706a0b1 |
424 | // i18n: don't use html formating in translation |
425 | return $this->set_error(sprintf(_("User \"%s\" already exists"),$ret['nickname'])); |
06b4facd |
426 | } |
62f7daa5 |
427 | |
06b4facd |
428 | /* Here is the data to write */ |
77ec28e9 |
429 | $data = $this->quotevalue($userdata['nickname']) . '|' . |
430 | $this->quotevalue($userdata['firstname']) . '|' . |
8419c13b |
431 | $this->quotevalue((!empty($userdata['lastname'])?$userdata['lastname']:'')) . '|' . |
77ec28e9 |
432 | $this->quotevalue($userdata['email']) . '|' . |
8419c13b |
433 | $this->quotevalue((!empty($userdata['label'])?$userdata['label']:'')); |
77ec28e9 |
434 | |
06b4facd |
435 | /* Strip linefeeds */ |
b7910e12 |
436 | $nl_str = array("\r","\n"); |
437 | $data = str_replace($nl_str, ' ', $data); |
7311c377 |
438 | |
439 | /** |
440 | * Make sure that entry fits into allocated record space. |
441 | * One byte is reserved for linefeed |
442 | */ |
443 | if (strlen($data) >= $this->line_length) { |
444 | return $this->set_error(_("Address book entry is too big")); |
445 | } |
446 | |
06b4facd |
447 | /* Add linefeed at end */ |
448 | $data = $data . "\n"; |
62f7daa5 |
449 | |
06b4facd |
450 | /* Reopen file, just to be sure */ |
451 | $this->open(true); |
452 | if(!$this->writeable) { |
35235328 |
453 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
454 | } |
62f7daa5 |
455 | |
06b4facd |
456 | /* Lock the file */ |
457 | if(!$this->lock()) { |
458 | return $this->set_error(_("Could not lock datafile")); |
459 | } |
62f7daa5 |
460 | |
06b4facd |
461 | /* Write */ |
3ecad5e6 |
462 | $r = sq_fwrite($this->filehandle, $data); |
62f7daa5 |
463 | |
06b4facd |
464 | /* Unlock file */ |
465 | $this->unlock(); |
62f7daa5 |
466 | |
3ecad5e6 |
467 | /* Test write result */ |
468 | if($r === FALSE) { |
147e5af3 |
469 | /* Fail */ |
35235328 |
470 | $this->set_error(_("Write to address book failed")); |
147e5af3 |
471 | return FALSE; |
472 | } |
62f7daa5 |
473 | |
3ecad5e6 |
474 | return TRUE; |
06b4facd |
475 | } |
476 | |
147e5af3 |
477 | /** |
478 | * Delete address |
479 | * @param string $alias alias that has to be deleted |
480 | * @return bool |
481 | */ |
06b4facd |
482 | function remove($alias) { |
483 | if(!$this->writeable) { |
35235328 |
484 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
485 | } |
62f7daa5 |
486 | |
06b4facd |
487 | /* Lock the file to make sure we're the only process working |
488 | * on it. */ |
489 | if(!$this->lock()) { |
490 | return $this->set_error(_("Could not lock datafile")); |
491 | } |
62f7daa5 |
492 | |
06b4facd |
493 | /* Read file into memory, ignoring nicknames to delete */ |
494 | @rewind($this->filehandle); |
495 | $i = 0; |
496 | $rows = array(); |
7311c377 |
497 | while($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
06b4facd |
498 | if(!in_array($row[0], $alias)) { |
499 | $rows[$i++] = $row; |
500 | } |
501 | } |
62f7daa5 |
502 | |
06b4facd |
503 | /* Write data back */ |
504 | if(!$this->overwrite($rows)) { |
505 | $this->unlock(); |
506 | return false; |
507 | } |
62f7daa5 |
508 | |
06b4facd |
509 | $this->unlock(); |
510 | return true; |
511 | } |
512 | |
147e5af3 |
513 | /** |
514 | * Modify address |
515 | * @param string $alias modified alias |
516 | * @param array $userdata new data |
517 | * @return bool true, if operation successful |
518 | */ |
06b4facd |
519 | function modify($alias, $userdata) { |
520 | if(!$this->writeable) { |
35235328 |
521 | return $this->set_error(_("Address book is read-only")); |
06b4facd |
522 | } |
62f7daa5 |
523 | |
06b4facd |
524 | /* See if user exists */ |
525 | $ret = $this->lookup($alias); |
526 | if(empty($ret)) { |
2706a0b1 |
527 | // i18n: don't use html formating in translation |
528 | return $this->set_error(sprintf(_("User \"%s\" does not exist"),$alias)); |
06b4facd |
529 | } |
849164a1 |
530 | |
531 | /* If the alias changed, see if the new alias exists */ |
532 | if (strtolower($alias) != strtolower($userdata['nickname'])) { |
533 | $ret = $this->lookup($userdata['nickname']); |
534 | if (!empty($ret)) { |
535 | return $this->set_error(sprintf(_("User \"%s\" already exists"), $userdata['nickname'])); |
536 | } |
537 | } |
538 | |
06b4facd |
539 | /* Lock the file to make sure we're the only process working |
540 | * on it. */ |
541 | if(!$this->lock()) { |
542 | return $this->set_error(_("Could not lock datafile")); |
543 | } |
62f7daa5 |
544 | |
7311c377 |
545 | /* calculate userdata size */ |
546 | $data = $this->quotevalue($userdata['nickname']) . '|' |
547 | . $this->quotevalue($userdata['firstname']) . '|' |
548 | . $this->quotevalue((!empty($userdata['lastname'])?$userdata['lastname']:'')) . '|' |
549 | . $this->quotevalue($userdata['email']) . '|' |
550 | . $this->quotevalue((!empty($userdata['label'])?$userdata['label']:'')); |
551 | /* make sure that it fits into allocated space */ |
552 | if (strlen($data) >= $this->line_length) { |
553 | return $this->set_error(_("Address book entry is too big")); |
554 | } |
555 | |
62f7daa5 |
556 | /* Read file into memory, modifying the data for the |
06b4facd |
557 | * user identified by $alias */ |
558 | $this->open(true); |
559 | @rewind($this->filehandle); |
560 | $i = 0; |
561 | $rows = array(); |
7311c377 |
562 | while($row = @fgetcsv($this->filehandle, $this->line_length, '|')) { |
06b4facd |
563 | if(strtolower($row[0]) != strtolower($alias)) { |
564 | $rows[$i++] = $row; |
565 | } else { |
566 | $rows[$i++] = array(0 => $userdata['nickname'], |
567 | 1 => $userdata['firstname'], |
8419c13b |
568 | 2 => (!empty($userdata['lastname'])?$userdata['lastname']:''), |
62f7daa5 |
569 | 3 => $userdata['email'], |
8419c13b |
570 | 4 => (!empty($userdata['label'])?$userdata['label']:'')); |
06b4facd |
571 | } |
572 | } |
62f7daa5 |
573 | |
06b4facd |
574 | /* Write data back */ |
575 | if(!$this->overwrite($rows)) { |
576 | $this->unlock(); |
577 | return false; |
578 | } |
62f7daa5 |
579 | |
06b4facd |
580 | $this->unlock(); |
581 | return true; |
582 | } |
62f7daa5 |
583 | |
147e5af3 |
584 | /** |
585 | * Function for quoting values before saving |
586 | * @param string $value string that has to be quoted |
587 | * @param string quoted string |
588 | */ |
77ec28e9 |
589 | function quotevalue($value) { |
590 | /* Quote the field if it contains | or ". Double quotes need to |
591 | * be replaced with "" */ |
bbb2bab5 |
592 | if(stristr($value, '"') || stristr($value, '|')) { |
77ec28e9 |
593 | $value = '"' . str_replace('"', '""', $value) . '"'; |
594 | } |
595 | return $value; |
596 | } |
6b0fe53b |
597 | } |