FusionDirectory
class_Lock.inc
1 <?php
2 /*
3  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
4  Copyright (C) 2003-2010 Cajus Pollmeier
5  Copyright (C) 2011-2020 FusionDirectory
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
20 */
21 
22 class Lock
23 {
24  public $dn;
25  public $objectDn;
26  public $userDn;
27  public $timestamp;
28 
29  public function __construct (string $dn, string $objectDn, string $userDn, DateTime $timestamp)
30  {
31  $this->dn = $dn;
32  $this->objectDn = $objectDn;
33  $this->userDn = $userDn;
34  $this->timestamp = $timestamp;
35  }
36 
47  public static function add ($object, string $user = NULL)
48  {
49  global $config, $ui;
50 
51  /* Remember which entries were opened as read only, because we
52  don't need to remove any locks for them later */
53  if (!session::is_set('LOCK_CACHE')) {
54  session::set('LOCK_CACHE', ['']);
55  }
56  if (is_array($object)) {
57  foreach ($object as $obj) {
58  static::add($obj, $user);
59  }
60  return;
61  }
62 
63  if ($user === NULL) {
64  $user = $ui->dn;
65  }
66 
67  $cache = &session::get_ref('LOCK_CACHE');
68  if (isset($_POST['open_readonly'])) {
69  $cache['READ_ONLY'][$object] = TRUE;
70  return;
71  }
72  if (isset($cache['READ_ONLY'][$object])) {
73  unset($cache['READ_ONLY'][$object]);
74  }
75 
76  /* Just a sanity check... */
77  if (empty($object) || empty($user)) {
78  throw new FusionDirectoryError(htmlescape(_('Error while adding a lock. Contact the developers!')));
79  }
80 
81  /* Check for existing entries in lock area */
82  $ldap = $config->get_ldap_link();
83  $ldap->cd(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
84  $ldap->search('(&(objectClass=fdLockEntry)(fdUserDn='.ldap_escape_f($user).')(fdObjectDn='.base64_encode($object).'))',
85  ['fdUserDn']);
86  if ($ldap->get_errno() == 32) {
87  /* No such object, means the locking branch is missing, create it */
88  $ldap->cd($config->current['BASE']);
89  try {
90  $ldap->create_missing_trees(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
91  } catch (FusionDirectoryError $error) {
92  $error->display();
93  }
94  $ldap->cd(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
95  $ldap->search('(&(objectClass=fdLockEntry)(fdUserDn='.ldap_escape_f($user).')(fdObjectDn='.base64_encode($object).'))',
96  ['fdUserDn']);
97  }
98  if (!$ldap->success()) {
99  throw new FusionDirectoryError(
100  sprintf(
101  htmlescape(_('Cannot create locking information in LDAP tree. Please contact your administrator!')).
102  '<br><br>'.htmlescape(_('LDAP server returned: %s')),
103  '<br><br><i>'.htmlescape($ldap->get_error()).'</i>'
104  )
105  );
106  }
107 
108  /* Add lock if none present */
109  if ($ldap->count() == 0) {
110  $attrs = [];
111  $name = md5($object);
112  $dn = 'cn='.$name.','.get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE'];
113  $ldap->cd($dn);
114  $attrs = [
115  'objectClass' => 'fdLockEntry',
116  'cn' => $name,
117  'fdUserDn' => $user,
118  'fdObjectDn' => base64_encode($object),
119  'fdLockTimestamp' => LdapGeneralizedTime::toString(new DateTime('now')),
120  ];
121  $ldap->add($attrs);
122  if (!$ldap->success()) {
123  throw new FusionDirectoryLdapError($dn, LDAP_ADD, $ldap->get_error(), $ldap->get_errno());
124  }
125  }
126  }
127 
135  public static function deleteByObject ($object)
136  {
137  global $config;
138 
139  if (is_array($object)) {
140  foreach ($object as $obj) {
141  static::deleteByObject($obj);
142  }
143  return;
144  }
145 
146  /* Sanity check */
147  if ($object == '') {
148  return;
149  }
150 
151  /* If this object was opened in read only mode then
152  skip removing the lock entry, there wasn't any lock created.
153  */
154  if (session::is_set('LOCK_CACHE')) {
155  $cache = &session::get_ref('LOCK_CACHE');
156  if (isset($cache['READ_ONLY'][$object])) {
157  unset($cache['READ_ONLY'][$object]);
158  return;
159  }
160  }
161 
162  /* Check for existance and remove the entry */
163  $ldap = $config->get_ldap_link();
164  $dn = get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE'];
165  $ldap->cd($dn);
166  $ldap->search('(&(objectClass=fdLockEntry)(fdObjectDn='.base64_encode($object).'))', ['fdObjectDn']);
167  if (!$ldap->success()) {
168  throw new FusionDirectoryLdapError($dn, LDAP_SEARCH, $ldap->get_error(), $ldap->get_errno());
169  } elseif ($attrs = $ldap->fetch()) {
170  $ldap->rmdir($attrs['dn']);
171  if (!$ldap->success()) {
172  throw new FusionDirectoryLdapError($attrs['dn'], LDAP_DEL, $ldap->get_error(), $ldap->get_errno());
173  }
174  }
175  }
176 
185  public static function deleteByUser (string $userdn)
186  {
187  global $config;
188 
189  /* Get LDAP ressources */
190  $ldap = $config->get_ldap_link();
191  $ldap->cd(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
192 
193  /* Remove all objects of this user, drop errors silently in this case. */
194  $ldap->search('(&(objectClass=fdLockEntry)(fdUserDn='.ldap_escape_f($userdn).'))', ['fdUserDn']);
195  while ($attrs = $ldap->fetch()) {
196  $ldap->rmdir($attrs['dn']);
197  }
198  }
199 
211  public static function get ($objects, bool $allow_readonly = FALSE): array
212  {
213  global $config;
214 
215  if (is_array($objects) && (count($objects) == 1)) {
216  $objects = reset($objects);
217  }
218  if (is_array($objects)) {
219  if ($allow_readonly) {
220  throw new FusionDirectoryException('Read only is not possible for several objects');
221  }
222  $filter = '(&(objectClass=fdLockEntry)(|';
223  foreach ($objects as $obj) {
224  $filter .= '(fdObjectDn='.base64_encode($obj).')';
225  }
226  $filter .= '))';
227  } else {
228  if ($allow_readonly && isset($_POST['open_readonly'])) {
229  /* If readonly is allowed and asked and there is only one object, bypass lock detection */
230  return [];
231  }
232  $filter = '(&(objectClass=fdLockEntry)(fdObjectDn='.base64_encode($objects).'))';
233  }
234 
235  /* Get LDAP link, check for presence of the lock entry */
236  $ldap = $config->get_ldap_link();
237  $dn = get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE'];
238  $ldap->cd($dn);
239  $ldap->search($filter, ['fdUserDn','fdObjectDn', 'fdLockTimestamp']);
240  if (!$ldap->success()) {
241  throw new FusionDirectoryLdapError($dn, LDAP_SEARCH, $ldap->get_error(), $ldap->get_errno());
242  }
243 
244  $locks = [];
245  $sessionLifetime = $config->get_cfg_value('sessionLifetime', 1800);
246  if ($sessionLifetime > 0) {
247  $expirationDate = (new DateTime())->sub(new DateInterval('PT'.$sessionLifetime.'S'));
248  }
249  while ($attrs = $ldap->fetch()) {
250  $date = LdapGeneralizedTime::fromString($attrs['fdLockTimestamp'][0]);
251  if (isset($expirationDate) && ($date < $expirationDate)) {
252  /* Delete expired locks */
253  $ldap->rmdir($attrs['dn']);
254  } else {
255  $locks[] = new Lock(
256  $attrs['dn'],
257  base64_decode($attrs['fdObjectDn'][0]),
258  $attrs['fdUserDn'][0],
259  $date
260  );
261  }
262  }
263 
264  if (!is_array($objects) && (count($locks) > 1)) {
265  /* Hmm. We're removing broken LDAP information here and issue a warning. */
266  $warning = new FusionDirectoryWarning(htmlescape(_('Found multiple locks for object to be locked. This should not happen - cleaning up multiple references.')));
267  $warning->display();
268 
269  /* Clean up these references now... */
270  foreach ($locks as $lock) {
271  $ldap->rmdir($lock->dn);
272  }
273 
274  return [];
275  }
276 
277  return $locks;
278  }
279 
280 
294  public static function addOrFail ($object, string $user = NULL, int $retries = 10)
295  {
296  $wait = $retries;
297  while (!empty($locks = Lock::get($object))) {
298  sleep(1);
299 
300  /* Oups - timed out */
301  if ($wait-- == 0) {
302  throw new FusionDirectoryException(_('Timeout while waiting for lock!'));
303  }
304  }
305  Lock::add($object, $user);
306  }
307 
331  public static function genLockedMessage (array $locks, bool $allowReadonly = FALSE, string $action = NULL): string
332  {
333  /* Save variables from LOCK_VARS_TO_USE in session - for further editing */
334  if (session::is_set('LOCK_VARS_TO_USE') && count(session::get('LOCK_VARS_TO_USE'))) {
335  $LOCK_VARS_USED_GET = [];
336  $LOCK_VARS_USED_POST = [];
337  $LOCK_VARS_USED_REQUEST = [];
338  $LOCK_VARS_TO_USE = session::get('LOCK_VARS_TO_USE');
339 
340  foreach ($LOCK_VARS_TO_USE as $name) {
341  if (empty($name)) {
342  continue;
343  }
344 
345  foreach ($_POST as $Pname => $Pvalue) {
346  if (preg_match($name, $Pname)) {
347  $LOCK_VARS_USED_POST[$Pname] = $_POST[$Pname];
348  }
349  }
350 
351  foreach ($_GET as $Pname => $Pvalue) {
352  if (preg_match($name, $Pname)) {
353  $LOCK_VARS_USED_GET[$Pname] = $_GET[$Pname];
354  }
355  }
356 
357  foreach ($_REQUEST as $Pname => $Pvalue) {
358  if (preg_match($name, $Pname)) {
359  $LOCK_VARS_USED_REQUEST[$Pname] = $_REQUEST[$Pname];
360  }
361  }
362  }
363  session::set('LOCK_VARS_TO_USE', []);
364  session::set('LOCK_VARS_USED_GET', $LOCK_VARS_USED_GET);
365  session::set('LOCK_VARS_USED_POST', $LOCK_VARS_USED_POST);
366  session::set('LOCK_VARS_USED_REQUEST', $LOCK_VARS_USED_REQUEST);
367  }
368 
369  /* Prepare and show template */
370  $smarty = get_smarty();
371  $smarty->assign('allow_readonly', $allowReadonly);
372  $smarty->assign('action', ($action ?? _('Edit anyway')));
373  $smarty->assign('locks', $locks);
374 
375  return $smarty->fetch(get_template_path('islocked.tpl'));
376  }
377 }
htmlescape(string $str)
Escape string for HTML output.
Definition: php_setup.inc:32
get_ou($name)
Get the OU of a certain RDN.
Definition: functions.inc:391
get_template_path($filename='', $plugin=FALSE, $path='')
Return themed path for specified base file.
Definition: functions.inc:174
static get($name)
Accessor of a session var.
static toString(DateTime $date, $setToUTC=TRUE)
Convert from DateTime object to LDAP GeneralizedTime formatted string.
static genLockedMessage(array $locks, bool $allowReadonly=FALSE, string $action=NULL)
Generate a lock message.
Definition: class_Lock.inc:331
static get($objects, bool $allow_readonly=FALSE)
Get locks for objects.
Definition: class_Lock.inc:211
static deleteByUser(string $userdn)
Remove all locks owned by a specific userdn.
Definition: class_Lock.inc:185
static set($name, $value)
Set a value in a session.
Error returned by an LDAP operation called from FusionDirectory.
static & get_ref($name)
Accessor of a session var by reference.
static add($object, string $user=NULL)
Add a lock for object(s)
Definition: class_Lock.inc:47
& get_smarty()
Get global smarty object.
Definition: functions.inc:324
static addOrFail($object, string $user=NULL, int $retries=10)
Add a lock for object(s) or fail.
Definition: class_Lock.inc:294
Parent class for all exceptions thrown in FusionDirectory.
static deleteByObject($object)
Remove a lock for object(s)
Definition: class_Lock.inc:135
static fromString($string, $useException=TRUE)
Convert from LDAP GeneralizedTime formatted string to DateTime object.
Parent class for all errors in FusionDirectory.
static is_set($name)
Check if the name of the session is set.