adding phpdoc blocks in addressbook classes (one more backend to go)
[squirrelmail.git] / functions / abook_local_file.php
1 <?php
2 /**
3 * abook_local_file.php
4 *
5 * Copyright (c) 1999-2004 The SquirrelMail Project Team
6 * Licensed under the GNU GPL. For full terms see the file COPYING.
7 *
8 * @version $Id$
9 * @package squirrelmail
10 * @subpackage addressbook
11 */
12
13 /**
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.
24 * ? name => name of address book
25 *</pre>
26 * NOTE. This class should not be used directly. Use the
27 * "AddressBook" class instead.
28 * @package squirrelmail
29 */
30 class abook_local_file extends addressbook_backend {
31 /**
32 * Backend type
33 * @var string
34 */
35 var $btype = 'local';
36 /**
37 * Backend name
38 * @var string
39 */
40 var $bname = 'local_file';
41
42 /**
43 * File used to store data
44 * @var string
45 */
46 var $filename = '';
47 /**
48 * File handle
49 * @var object
50 */
51 var $filehandle = 0;
52 /**
53 * Create file, if it not present
54 * @var bool
55 */
56 var $create = false;
57 /**
58 * Detect, if address book is writeable by checking file permisions
59 * @var bool
60 */
61 var $detect_writeable = true;
62 /**
63 * Control write access to address book
64 *
65 * Option does not have any effect, if 'detect_writeable' is 'true'
66 * @var bool
67 */
68 var $writeable = false;
69 /**
70 * Umask of the file
71 * @var string
72 */
73 var $umask;
74
75 /* ========================== Private ======================= */
76
77 /**
78 * Constructor
79 * @param array $param backend options
80 * @return bool
81 */
82 function abook_local_file($param) {
83 $this->sname = _("Personal address book");
84 $this->umask = Umask();
85
86 if(is_array($param)) {
87 if(empty($param['filename'])) {
88 return $this->set_error('Invalid parameters');
89 }
90 if(!is_string($param['filename'])) {
91 return $this->set_error($param['filename'] . ': '.
92 _("Not a file name"));
93 }
94
95 $this->filename = $param['filename'];
96
97 if(isset($param['create'])) {
98 $this->create = $param['create'];
99 }
100 if(isset($param['umask'])) {
101 $this->umask = $param['umask'];
102 }
103 if(isset($param['name'])) {
104 $this->sname = $param['name'];
105 }
106 if(isset($param['detect_writeable'])) {
107 $this->detect_writeable = $param['detect_writeable'];
108 }
109 if(!empty($param['writeable'])) {
110 $this->writeable = $param['writeable'];
111 }
112
113 $this->open(true);
114 } else {
115 $this->set_error('Invalid argument to constructor');
116 }
117 }
118
119 /**
120 * Open the addressbook file and store the file pointer.
121 * Use $file as the file to open, or the class' own
122 * filename property. If $param is empty and file is
123 * open, do nothing.
124 * @param bool $new is file already opened
125 * @return bool
126 */
127 function open($new = false) {
128 $this->error = '';
129 $file = $this->filename;
130 $create = $this->create;
131 $fopenmode = ($this->writeable ? 'a+' : 'r');
132
133 /* Return true is file is open and $new is unset */
134 if($this->filehandle && !$new) {
135 return true;
136 }
137
138 /* Check that new file exitsts */
139 if((!(file_exists($file) && is_readable($file))) && !$create) {
140 return $this->set_error("$file: " . _("No such file or directory"));
141 }
142
143 /* Close old file, if any */
144 if($this->filehandle) { $this->close(); }
145
146 umask($this->umask);
147 if (! $this->detect_writeable) {
148 $fh = @fopen($file,$fopenmode);
149 if ($fh) {
150 $this->filehandle = &$fh;
151 $this->filename = $file;
152 } else {
153 return $this->set_error("$file: " . _("Open failed"));
154 }
155 } else {
156 /* Open file. First try to open for reading and writing,
157 * but fall back to read only. */
158 $fh = @fopen($file, 'a+');
159 if($fh) {
160 $this->filehandle = &$fh;
161 $this->filename = $file;
162 $this->writeable = true;
163 } else {
164 $fh = @fopen($file, 'r');
165 if($fh) {
166 $this->filehandle = &$fh;
167 $this->filename = $file;
168 $this->writeable = false;
169 } else {
170 return $this->set_error("$file: " . _("Open failed"));
171 }
172 }
173 }
174 return true;
175 }
176
177 /** Close the file and forget the filehandle */
178 function close() {
179 @fclose($this->filehandle);
180 $this->filehandle = 0;
181 $this->filename = '';
182 $this->writable = false;
183 }
184
185 /** Lock the datafile - try 20 times in 5 seconds */
186 function lock() {
187 for($i = 0 ; $i < 20 ; $i++) {
188 if(flock($this->filehandle, 2 + 4))
189 return true;
190 else
191 usleep(250000);
192 }
193 return false;
194 }
195
196 /** Unlock the datafile */
197 function unlock() {
198 return flock($this->filehandle, 3);
199 }
200
201 /**
202 * Overwrite the file with data from $rows
203 * NOTE! Previous locks are broken by this function
204 * @param array $rows new data
205 * @return bool
206 */
207 function overwrite(&$rows) {
208 $this->unlock();
209 $newfh = @fopen($this->filename.'.tmp', 'w');
210
211 if(!$newfh) {
212 return $this->set_error($this->filename. '.tmp:' . _("Open failed"));
213 }
214
215 for($i = 0, $cnt=sizeof($rows) ; $i < $cnt ; $i++) {
216 if(is_array($rows[$i])) {
217 for($j = 0, $cnt_part=count($rows[$i]) ; $j < $cnt_part ; $j++) {
218 $rows[$i][$j] = $this->quotevalue($rows[$i][$j]);
219 }
220 $tmpwrite = sq_fwrite($newfh, join('|', $rows[$i]) . "\n");
221 if ($tmpwrite === FALSE) {
222 return $this->set_error($this->filename . '.tmp:' . _("Write failed"));
223 }
224 }
225 }
226
227 fclose($newfh);
228 if (!@copy($this->filename . '.tmp' , $this->filename)) {
229 return $this->set_error($this->filename . ':' . _("Unable to update"));
230 }
231 @unlink($this->filename . '.tmp');
232 $this->unlock();
233 $this->open(true);
234 return true;
235 }
236
237 /* ========================== Public ======================== */
238
239 /**
240 * Search the file
241 * @param string $expr search expression
242 * @return array search results
243 */
244 function search($expr) {
245
246 /* To be replaced by advanded search expression parsing */
247 if(is_array($expr)) { return; }
248
249 /* Make regexp from glob'ed expression
250 * May want to quote other special characters like (, ), -, [, ], etc. */
251 $expr = str_replace('?', '.', $expr);
252 $expr = str_replace('*', '.*', $expr);
253
254 $res = array();
255 if(!$this->open()) {
256 return false;
257 }
258 @rewind($this->filehandle);
259
260 while ($row = @fgetcsv($this->filehandle, 2048, '|')) {
261 $line = join(' ', $row);
262 if(eregi($expr, $line)) {
263 array_push($res, array('nickname' => $row[0],
264 'name' => $row[1] . ' ' . $row[2],
265 'firstname' => $row[1],
266 'lastname' => $row[2],
267 'email' => $row[3],
268 'label' => $row[4],
269 'backend' => $this->bnum,
270 'source' => &$this->sname));
271 }
272 }
273
274 return $res;
275 }
276
277 /**
278 * Lookup alias
279 * @param string $alias alias
280 * @return array search results
281 */
282 function lookup($alias) {
283 if(empty($alias)) {
284 return array();
285 }
286
287 $alias = strtolower($alias);
288
289 $this->open();
290 @rewind($this->filehandle);
291
292 while ($row = @fgetcsv($this->filehandle, 2048, '|')) {
293 if(strtolower($row[0]) == $alias) {
294 return array('nickname' => $row[0],
295 'name' => $row[1] . ' ' . $row[2],
296 'firstname' => $row[1],
297 'lastname' => $row[2],
298 'email' => $row[3],
299 'label' => $row[4],
300 'backend' => $this->bnum,
301 'source' => &$this->sname);
302 }
303 }
304
305 return array();
306 }
307
308 /**
309 * List all addresses
310 * @return array list of all addresses
311 */
312 function list_addr() {
313 $res = array();
314 $this->open();
315 @rewind($this->filehandle);
316
317 while ($row = @fgetcsv($this->filehandle, 2048, '|')) {
318 array_push($res, array('nickname' => $row[0],
319 'name' => $row[1] . ' ' . $row[2],
320 'firstname' => $row[1],
321 'lastname' => $row[2],
322 'email' => $row[3],
323 'label' => $row[4],
324 'backend' => $this->bnum,
325 'source' => &$this->sname));
326 }
327 return $res;
328 }
329
330 /**
331 * Add address
332 * @param array $userdata new data
333 * @return bool
334 */
335 function add($userdata) {
336 if(!$this->writeable) {
337 return $this->set_error(_("Addressbook is read-only"));
338 }
339 /* See if user exists already */
340 $ret = $this->lookup($userdata['nickname']);
341 if(!empty($ret)) {
342 return $this->set_error(sprintf(_("User '%s' already exist"),
343 $ret['nickname']));
344 }
345
346 /* Here is the data to write */
347 $data = $this->quotevalue($userdata['nickname']) . '|' .
348 $this->quotevalue($userdata['firstname']) . '|' .
349 $this->quotevalue($userdata['lastname']) . '|' .
350 $this->quotevalue($userdata['email']) . '|' .
351 $this->quotevalue($userdata['label']);
352
353 /* Strip linefeeds */
354 $data = ereg_replace("[\r\n]", ' ', $data);
355 /* Add linefeed at end */
356 $data = $data . "\n";
357
358 /* Reopen file, just to be sure */
359 $this->open(true);
360 if(!$this->writeable) {
361 return $this->set_error(_("Addressbook is read-only"));
362 }
363
364 /* Lock the file */
365 if(!$this->lock()) {
366 return $this->set_error(_("Could not lock datafile"));
367 }
368
369 /* Write */
370 $r = sq_fwrite($this->filehandle, $data);
371
372 /* Unlock file */
373 $this->unlock();
374
375 /* Test write result */
376 if($r === FALSE) {
377 /* Fail */
378 $this->set_error(_("Write to addressbook failed"));
379 return FALSE;
380 }
381
382 return TRUE;
383 }
384
385 /**
386 * Delete address
387 * @param string $alias alias that has to be deleted
388 * @return bool
389 */
390 function remove($alias) {
391 if(!$this->writeable) {
392 return $this->set_error(_("Addressbook is read-only"));
393 }
394
395 /* Lock the file to make sure we're the only process working
396 * on it. */
397 if(!$this->lock()) {
398 return $this->set_error(_("Could not lock datafile"));
399 }
400
401 /* Read file into memory, ignoring nicknames to delete */
402 @rewind($this->filehandle);
403 $i = 0;
404 $rows = array();
405 while($row = @fgetcsv($this->filehandle, 2048, '|')) {
406 if(!in_array($row[0], $alias)) {
407 $rows[$i++] = $row;
408 }
409 }
410
411 /* Write data back */
412 if(!$this->overwrite($rows)) {
413 $this->unlock();
414 return false;
415 }
416
417 $this->unlock();
418 return true;
419 }
420
421 /**
422 * Modify address
423 * @param string $alias modified alias
424 * @param array $userdata new data
425 * @return bool true, if operation successful
426 */
427 function modify($alias, $userdata) {
428 if(!$this->writeable) {
429 return $this->set_error(_("Addressbook is read-only"));
430 }
431
432 /* See if user exists */
433 $ret = $this->lookup($alias);
434 if(empty($ret)) {
435 return $this->set_error(sprintf(_("User '%s' does not exist"),
436 $alias));
437 }
438
439 /* Lock the file to make sure we're the only process working
440 * on it. */
441 if(!$this->lock()) {
442 return $this->set_error(_("Could not lock datafile"));
443 }
444
445 /* Read file into memory, modifying the data for the
446 * user identified by $alias */
447 $this->open(true);
448 @rewind($this->filehandle);
449 $i = 0;
450 $rows = array();
451 while($row = @fgetcsv($this->filehandle, 2048, '|')) {
452 if(strtolower($row[0]) != strtolower($alias)) {
453 $rows[$i++] = $row;
454 } else {
455 $rows[$i++] = array(0 => $userdata['nickname'],
456 1 => $userdata['firstname'],
457 2 => $userdata['lastname'],
458 3 => $userdata['email'],
459 4 => $userdata['label']);
460 }
461 }
462
463 /* Write data back */
464 if(!$this->overwrite($rows)) {
465 $this->unlock();
466 return false;
467 }
468
469 $this->unlock();
470 return true;
471 }
472
473 /**
474 * Function for quoting values before saving
475 * @param string $value string that has to be quoted
476 * @param string quoted string
477 */
478 function quotevalue($value) {
479 /* Quote the field if it contains | or ". Double quotes need to
480 * be replaced with "" */
481 if(ereg("[|\"]", $value)) {
482 $value = '"' . str_replace('"', '""', $value) . '"';
483 }
484 return $value;
485 }
486
487 } /* End of class abook_local_file */
488 ?>