abook_local_file.php 15 KB

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