abook_local_file.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. <?php
  2. /**
  3. * abook_local_file.php
  4. *
  5. * @copyright 1999-2025 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. /**
  82. * Sets max entry size (number of bytes used for all address book fields
  83. * (including escapes) + 4 delimiters + 1 linefeed)
  84. * @var integer
  85. * @since 1.5.2
  86. */
  87. var $line_length = 2048;
  88. /* ========================== Private ======================= */
  89. /**
  90. * Constructor (PHP5 style, required in some future version of PHP)
  91. * @param array $param backend options
  92. * @return bool
  93. */
  94. function __construct($param) {
  95. $this->sname = _("Personal Address Book");
  96. $this->umask = Umask();
  97. if(is_array($param)) {
  98. if(empty($param['filename'])) {
  99. return $this->set_error('Invalid parameters');
  100. }
  101. if(!is_string($param['filename'])) {
  102. return $this->set_error($param['filename'] . ': '.
  103. _("Not a file name"));
  104. }
  105. $this->filename = $param['filename'];
  106. if(isset($param['create'])) {
  107. $this->create = $param['create'];
  108. }
  109. if(isset($param['umask'])) {
  110. $this->umask = $param['umask'];
  111. }
  112. if(isset($param['name'])) {
  113. $this->sname = $param['name'];
  114. }
  115. if(isset($param['detect_writeable'])) {
  116. $this->detect_writeable = $param['detect_writeable'];
  117. }
  118. if(!empty($param['writeable'])) {
  119. $this->writeable = $param['writeable'];
  120. }
  121. if(isset($param['listing'])) {
  122. $this->listing = $param['listing'];
  123. }
  124. if(isset($param['line_length']) && ! empty($param['line_length'])) {
  125. $this->line_length = (int) $param['line_length'];
  126. }
  127. $this->open(true);
  128. } else {
  129. $this->set_error('Invalid argument to constructor');
  130. }
  131. }
  132. /**
  133. * Constructor (PHP4 style, kept for compatibility reasons)
  134. * @param array $param backend options
  135. * @return bool
  136. */
  137. function abook_local_file($param) {
  138. return self::__construct($param);
  139. }
  140. /**
  141. * Open the addressbook file and store the file pointer.
  142. * Use $file as the file to open, or the class' own
  143. * filename property. If $param is empty and file is
  144. * open, do nothing.
  145. * @param bool $new is file already opened
  146. * @return bool
  147. */
  148. function open($new = false) {
  149. $this->error = '';
  150. $file = $this->filename;
  151. $create = $this->create;
  152. $fopenmode = (($this->writeable && sq_is_writable($file)) ? 'a+' : 'r');
  153. /* Return true is file is open and $new is unset */
  154. if($this->filehandle && !$new) {
  155. return true;
  156. }
  157. /* Check that new file exitsts */
  158. if((!(file_exists($file) && is_readable($file))) && !$create) {
  159. return $this->set_error("$file: " . _("No such file or directory"));
  160. }
  161. /* Close old file, if any */
  162. if($this->filehandle) { $this->close(); }
  163. umask($this->umask);
  164. if (! $this->detect_writeable) {
  165. $fh = @fopen($file,$fopenmode);
  166. if ($fh) {
  167. $this->filehandle = &$fh;
  168. $this->filename = $file;
  169. } else {
  170. return $this->set_error("$file: " . _("Open failed"));
  171. }
  172. } else {
  173. /* Open file. First try to open for reading and writing,
  174. * but fall back to read only. */
  175. $fh = @fopen($file, 'a+');
  176. if($fh) {
  177. $this->filehandle = &$fh;
  178. $this->filename = $file;
  179. $this->writeable = true;
  180. } else {
  181. $fh = @fopen($file, 'r');
  182. if($fh) {
  183. $this->filehandle = &$fh;
  184. $this->filename = $file;
  185. $this->writeable = false;
  186. } else {
  187. return $this->set_error("$file: " . _("Open failed"));
  188. }
  189. }
  190. }
  191. return true;
  192. }
  193. /** Close the file and forget the filehandle */
  194. function close() {
  195. @fclose($this->filehandle);
  196. $this->filehandle = 0;
  197. $this->filename = '';
  198. $this->writable = false;
  199. }
  200. /** Lock the datafile - try 20 times in 5 seconds */
  201. function lock() {
  202. for($i = 0 ; $i < 20 ; $i++) {
  203. if(flock($this->filehandle, 2 + 4))
  204. return true;
  205. else
  206. usleep(250000);
  207. }
  208. return false;
  209. }
  210. /** Unlock the datafile */
  211. function unlock() {
  212. return flock($this->filehandle, 3);
  213. }
  214. /**
  215. * Overwrite the file with data from $rows
  216. * NOTE! Previous locks are broken by this function
  217. * @param array $rows new data
  218. * @return bool
  219. */
  220. function overwrite(&$rows) {
  221. $this->unlock();
  222. $newfh = @fopen($this->filename.'.tmp', 'w');
  223. if(!$newfh) {
  224. return $this->set_error($this->filename. '.tmp:' . _("Open failed"));
  225. }
  226. for($i = 0, $cnt=sizeof($rows) ; $i < $cnt ; $i++) {
  227. if(is_array($rows[$i])) {
  228. for($j = 0, $cnt_part=count($rows[$i]) ; $j < $cnt_part ; $j++) {
  229. $rows[$i][$j] = $this->quotevalue($rows[$i][$j]);
  230. }
  231. $tmpwrite = sq_fwrite($newfh, join('|', $rows[$i]) . "\n");
  232. if ($tmpwrite === FALSE) {
  233. return $this->set_error($this->filename . '.tmp:' . _("Write failed"));
  234. }
  235. }
  236. }
  237. fclose($newfh);
  238. if (!@copy($this->filename . '.tmp' , $this->filename)) {
  239. return $this->set_error($this->filename . ':' . _("Unable to update"));
  240. }
  241. @unlink($this->filename . '.tmp');
  242. @chmod($this->filename, 0600);
  243. $this->unlock();
  244. $this->open(true);
  245. return true;
  246. }
  247. /* ========================== Public ======================== */
  248. /**
  249. * Search the file
  250. * @param string $expr search expression
  251. * @return array search results
  252. */
  253. function search($expr) {
  254. /* To be replaced by advanded search expression parsing */
  255. if(is_array($expr)) { return; }
  256. // don't allow wide search when listing is disabled.
  257. if ($expr=='*' && ! $this->listing)
  258. return array();
  259. // Make regexp from glob'ed expression
  260. $expr = preg_quote($expr);
  261. $expr = str_replace(array('\\?', '\\*'), array('.', '.*'), $expr);
  262. $res = array();
  263. if(!$this->open()) {
  264. return false;
  265. }
  266. @rewind($this->filehandle);
  267. while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) {
  268. if (count($row)<5) {
  269. /**
  270. * address book is corrupted.
  271. */
  272. global $oTemplate;
  273. error_box(_("Address book is corrupted. Required fields are missing."));
  274. $oTemplate->display('footer.tpl');
  275. die();
  276. } else {
  277. /**
  278. * TODO: regexp search is supported only in local_file backend.
  279. * Do we check format of regexp or ignore errors?
  280. */
  281. // errors on preg_match call are suppressed in order to prevent display of regexp compilation errors
  282. if (@preg_match('/' . $expr . '/i', $row[0]) // nickname
  283. || @preg_match('/' . $expr . '/i', $row[1]) // firstname
  284. || @preg_match('/' . $expr . '/i', $row[2]) // lastname
  285. || @preg_match('/' . $expr . '/i', $row[3])) { // email
  286. array_push($res, array('nickname' => $row[0],
  287. 'name' => $this->fullname($row[1], $row[2]),
  288. 'firstname' => $row[1],
  289. 'lastname' => $row[2],
  290. 'email' => $row[3],
  291. 'label' => $row[4],
  292. 'backend' => $this->bnum,
  293. 'source' => &$this->sname));
  294. }
  295. }
  296. }
  297. return $res;
  298. }
  299. /**
  300. * Lookup an address by the indicated field.
  301. *
  302. * @param string $value The value to look up
  303. * @param integer $field The field to look in, should be one
  304. * of the SM_ABOOK_FIELD_* constants
  305. * defined in include/constants.php
  306. * (OPTIONAL; defaults to nickname field)
  307. * NOTE: uniqueness is only guaranteed
  308. * when the nickname field is used here;
  309. * otherwise, the first matching address
  310. * is returned.
  311. *
  312. * @return array Array with lookup results when the value
  313. * was found, an empty array if the value was
  314. * not found.
  315. *
  316. */
  317. function lookup($value, $field=SM_ABOOK_FIELD_NICKNAME) {
  318. if(empty($value)) {
  319. return array();
  320. }
  321. $value = strtolower($value);
  322. $this->open();
  323. @rewind($this->filehandle);
  324. while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) {
  325. if (count($row)<5) {
  326. /**
  327. * address book is corrupted.
  328. */
  329. global $oTemplate;
  330. error_box(_("Address book is corrupted. Required fields are missing."));
  331. $oTemplate->display('footer.tpl');
  332. die();
  333. } else {
  334. if(strtolower($row[$field]) == $value) {
  335. return array('nickname' => $row[0],
  336. 'name' => $this->fullname($row[1], $row[2]),
  337. 'firstname' => $row[1],
  338. 'lastname' => $row[2],
  339. 'email' => $row[3],
  340. 'label' => $row[4],
  341. 'backend' => $this->bnum,
  342. 'source' => &$this->sname);
  343. }
  344. }
  345. }
  346. return array();
  347. }
  348. /**
  349. * List all addresses
  350. * @return array list of all addresses
  351. */
  352. function list_addr() {
  353. $res = array();
  354. if(isset($this->listing) && !$this->listing) {
  355. return array();
  356. }
  357. $this->open();
  358. @rewind($this->filehandle);
  359. while ($row = @fgetcsv($this->filehandle, $this->line_length, '|')) {
  360. if (count($row)<5) {
  361. /**
  362. * address book is corrupted. Don't be nice to people that
  363. * violate address book formating.
  364. */
  365. global $oTemplate;
  366. error_box(_("Address book is corrupted. Required fields are missing."));
  367. $oTemplate->display('footer.tpl');
  368. die();
  369. } else {
  370. array_push($res, array('nickname' => $row[0],
  371. 'name' => $this->fullname($row[1], $row[2]),
  372. 'firstname' => $row[1],
  373. 'lastname' => $row[2],
  374. 'email' => $row[3],
  375. 'label' => $row[4],
  376. 'backend' => $this->bnum,
  377. 'source' => &$this->sname));
  378. }
  379. }
  380. return $res;
  381. }
  382. /**
  383. * Add address
  384. * @param array $userdata new data
  385. * @return bool
  386. */
  387. function add($userdata) {
  388. if(!$this->writeable) {
  389. return $this->set_error(_("Address book is read-only"));
  390. }
  391. /* See if user exists already */
  392. $ret = $this->lookup($userdata['nickname']);
  393. if(!empty($ret)) {
  394. // i18n: don't use html formating in translation
  395. return $this->set_error(sprintf(_("User \"%s\" already exists"),$ret['nickname']));
  396. }
  397. /* Here is the data to write */
  398. $data = $this->quotevalue($userdata['nickname']) . '|' .
  399. $this->quotevalue($userdata['firstname']) . '|' .
  400. $this->quotevalue((!empty($userdata['lastname'])?$userdata['lastname']:'')) . '|' .
  401. $this->quotevalue($userdata['email']) . '|' .
  402. $this->quotevalue((!empty($userdata['label'])?$userdata['label']:''));
  403. /* Strip linefeeds */
  404. $nl_str = array("\r","\n");
  405. $data = str_replace($nl_str, ' ', $data);
  406. /**
  407. * Make sure that entry fits into allocated record space.
  408. * One byte is reserved for linefeed
  409. */
  410. if (strlen($data) >= $this->line_length) {
  411. return $this->set_error(_("Address book entry is too big"));
  412. }
  413. /* Add linefeed at end */
  414. $data = $data . "\n";
  415. /* Reopen file, just to be sure */
  416. $this->open(true);
  417. if(!$this->writeable) {
  418. return $this->set_error(_("Address book is read-only"));
  419. }
  420. /* Lock the file */
  421. if(!$this->lock()) {
  422. return $this->set_error(_("Could not lock datafile"));
  423. }
  424. /* Write */
  425. $r = sq_fwrite($this->filehandle, $data);
  426. /* Unlock file */
  427. $this->unlock();
  428. /* Test write result */
  429. if($r === FALSE) {
  430. /* Fail */
  431. $this->set_error(_("Write to address book failed"));
  432. return FALSE;
  433. }
  434. return TRUE;
  435. }
  436. /**
  437. * Delete address
  438. * @param string $alias alias that has to be deleted
  439. * @return bool
  440. */
  441. function remove($alias) {
  442. if(!$this->writeable) {
  443. return $this->set_error(_("Address book is read-only"));
  444. }
  445. /* Lock the file to make sure we're the only process working
  446. * on it. */
  447. if(!$this->lock()) {
  448. return $this->set_error(_("Could not lock datafile"));
  449. }
  450. /* Read file into memory, ignoring nicknames to delete */
  451. @rewind($this->filehandle);
  452. $i = 0;
  453. $rows = array();
  454. while($row = @fgetcsv($this->filehandle, $this->line_length, '|')) {
  455. if(!in_array($row[0], $alias)) {
  456. $rows[$i++] = $row;
  457. }
  458. }
  459. /* Write data back */
  460. if(!$this->overwrite($rows)) {
  461. $this->unlock();
  462. return false;
  463. }
  464. $this->unlock();
  465. return true;
  466. }
  467. /**
  468. * Modify address
  469. * @param string $alias modified alias
  470. * @param array $userdata new data
  471. * @return bool true, if operation successful
  472. */
  473. function modify($alias, $userdata) {
  474. if(!$this->writeable) {
  475. return $this->set_error(_("Address book is read-only"));
  476. }
  477. /* See if user exists */
  478. $ret = $this->lookup($alias);
  479. if(empty($ret)) {
  480. // i18n: don't use html formating in translation
  481. return $this->set_error(sprintf(_("User \"%s\" does not exist"),$alias));
  482. }
  483. /* If the alias changed, see if the new alias exists */
  484. if (strtolower($alias) != strtolower($userdata['nickname'])) {
  485. $ret = $this->lookup($userdata['nickname']);
  486. if (!empty($ret)) {
  487. return $this->set_error(sprintf(_("User \"%s\" already exists"), $userdata['nickname']));
  488. }
  489. }
  490. /* Lock the file to make sure we're the only process working
  491. * on it. */
  492. if(!$this->lock()) {
  493. return $this->set_error(_("Could not lock datafile"));
  494. }
  495. /* calculate userdata size */
  496. $data = $this->quotevalue($userdata['nickname']) . '|'
  497. . $this->quotevalue($userdata['firstname']) . '|'
  498. . $this->quotevalue((!empty($userdata['lastname'])?$userdata['lastname']:'')) . '|'
  499. . $this->quotevalue($userdata['email']) . '|'
  500. . $this->quotevalue((!empty($userdata['label'])?$userdata['label']:''));
  501. /* make sure that it fits into allocated space */
  502. if (strlen($data) >= $this->line_length) {
  503. return $this->set_error(_("Address book entry is too big"));
  504. }
  505. /* Read file into memory, modifying the data for the
  506. * user identified by $alias */
  507. $this->open(true);
  508. @rewind($this->filehandle);
  509. $i = 0;
  510. $rows = array();
  511. while($row = @fgetcsv($this->filehandle, $this->line_length, '|')) {
  512. if(strtolower($row[0]) != strtolower($alias)) {
  513. $rows[$i++] = $row;
  514. } else {
  515. $rows[$i++] = array(0 => $userdata['nickname'],
  516. 1 => $userdata['firstname'],
  517. 2 => (!empty($userdata['lastname'])?$userdata['lastname']:''),
  518. 3 => $userdata['email'],
  519. 4 => (!empty($userdata['label'])?$userdata['label']:''));
  520. }
  521. }
  522. /* Write data back */
  523. if(!$this->overwrite($rows)) {
  524. $this->unlock();
  525. return false;
  526. }
  527. $this->unlock();
  528. return true;
  529. }
  530. /**
  531. * Function for quoting values before saving
  532. * @param string $value string that has to be quoted
  533. * @param string quoted string
  534. */
  535. function quotevalue($value) {
  536. /* Quote the field if it contains | or ". Double quotes need to
  537. * be replaced with "" */
  538. if(stristr($value, '"') || stristr($value, '|')) {
  539. $value = '"' . str_replace('"', '""', $value) . '"';
  540. }
  541. return $value;
  542. }
  543. }