abook_local_file.php 14 KB

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