FusionDirectory
class_SnapshotHandler.inc
1 <?php
2 /*
3  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
4 
5  Copyright (C) 2003-2010 Cajus Pollmeier
6  Copyright (C) 2011-2019 FusionDirectory
7 
8  This program is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or
11  (at your option) any later version.
12 
13  This program is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  GNU General Public License for more details.
17 
18  You should have received a copy of the GNU General Public License
19  along with this program; if not, write to the Free Software
20  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
21 */
22 
23 /*
24  * \file class_SnapshotHandler
25  * Source code for class SnapshotHandler
26  */
27 
33 {
34  protected $enabled;
35  protected $snapshotRDN;
36  protected $snapshotsCache;
37 
38  static function plInfo ()
39  {
40  return [
41  'plShortName' => _('Snapshot'),
42  'plDescription' => _('Snapshot handler'),
43  /* Categories for snapshots are computed in config class */
44  'plCategory' => [],
45 
46  'plProvidedAcls' => [
47  'restore_over' => _('Restore over an existing object'),
48  'restore_deleted' => _('Restore a deleted object'),
49  ]
50  ];
51  }
52 
56  function __construct ()
57  {
58  global $config;
59  $this->enabled = $config->snapshotEnabled();
60  if ($this->enabled) {
61  /* Prepare base */
62  $this->snapshotRDN = $config->get_cfg_value('snapshotBase');
63  $ldap = $config->get_ldap_link();
64  $ldap->cd($config->current['BASE']);
65  try {
66  $ldap->create_missing_trees($this->snapshotRDN);
67  } catch (FusionDirectoryError $error) {
68  $error->display();
69  }
70  }
71  }
72 
78  function enabled ()
79  {
80  return $this->enabled;
81  }
82 
83  /* \brief Get the snapshot dn of an object dn
84  */
85  protected function snapshot_dn ($dn)
86  {
87  global $config;
88  return preg_replace("/".preg_quote($config->current['BASE'], '/')."$/", "", $dn)
89  .$this->snapshotRDN;
90  }
91 
95  function hasDeletedSnapshots ($bases)
96  {
97  foreach ($bases as $base) {
98  if (count($this->getAllDeletedSnapshots($base)) > 0) {
99  return TRUE;
100  }
101  }
102  return FALSE;
103  }
104 
108  function initSnapshotCache ($base)
109  {
110  global $config;
111  if (!$this->enabled()) {
112  return;
113  }
114 
115  $ldap = $config->get_ldap_link();
116 
117  // Initialize base
118  $base = $this->snapshot_dn($base);
119 
120  /* Fetch all objects with */
121  $ldap->cd($base);
122  $ldap->search('(&(objectClass=gosaSnapshotObject)(gosaSnapshotDN=*))', ['gosaSnapshotDN']);
123 
124  /* Store for which object we have snapshots */
125  $this->snapshotsCache = [];
126  while ($entry = $ldap->fetch()) {
127  $this->snapshotsCache[$entry['gosaSnapshotDN'][0]] = TRUE;
128  }
129  }
130 
136  function hasSnapshots ($dn)
137  {
138  return isset($this->snapshotsCache[$dn]);
139  }
140 
148  function getSnapshots ($dn, $raw = FALSE)
149  {
150  global $config;
151  if (!$this->enabled()) {
152  return [];
153  }
154 
155  $ldap = $config->get_ldap_link();
156 
157  $objectBase = preg_replace("/^[^,]*./", "", $dn);
158 
159  // Initialize base
160  $base = $this->snapshot_dn($objectBase);
161 
162  /* Fetch all objects with gosaSnapshotDN=$dn */
163  $ldap->cd($base);
164  $ldap->search(
165  '(&(objectClass=gosaSnapshotObject)(gosaSnapshotDN='.ldap_escape_f($dn).'))',
166  ['gosaSnapshotTimestamp','gosaSnapshotDN','description'],
167  'one'
168  );
169 
170  /* Put results into a list and add description if missing */
171  $objects = [];
172  while ($entry = $ldap->fetch(TRUE)) {
173  if (!isset($entry['description'][0])) {
174  $entry['description'][0] = "";
175  }
176  $objects[] = $entry;
177  }
178 
179  /* Return the raw array, or format the result */
180  if ($raw) {
181  return $objects;
182  } else {
183  $tmp = [];
184  foreach ($objects as $entry) {
185  $tmp[base64_encode($entry['dn'])] = $entry['description'][0];
186  }
187  }
188  return $tmp;
189  }
190 
191 
203  function createSnapshot ($dn, string $description, string $objectType, string $snapshotSource = 'FD')
204  {
205  global $config;
206  if (!$this->enabled()) {
207  logging::debug(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $dn, 'Snapshot are disabled but tried to create snapshot');
208  return;
209  }
210 
211  if (is_array($dn)) {
212  $dns = $dn;
213  $dn = $dns[0];
214  } else {
215  $dns = [$dn];
216  }
217 
218  $ldap = $config->get_ldap_link();
219 
220  /* check if the dn exists */
221  if (!$ldap->dn_exists($dn)) {
222  logging::debug(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $dn, 'Tried to snapshot non-existing dn');
223  return;
224  }
225 
226  /* Extract seconds & mysecs, they are used as entry index */
227  list($usec, $sec) = explode(" ", microtime());
228 
229  /* Collect some infos */
230  $base_of_object = preg_replace('/^[^,]+,/i', '', $dn);
231  $new_base = $this->snapshot_dn($base_of_object);
232  /* Create object */
233  $data = '';
234  foreach ($dns as $tmp_dn) {
235  try {
236  $data .= $ldap->generateLdif($tmp_dn, '(!(objectClass=gosaDepartment))', 'sub');
237  } catch (LDIFExportException $e) {
238  $error = new FusionDirectoryError(
239  htmlescape(sprintf(
240  _('Failed to create snapshot: %s'),
241  $e->getMessage()
242  ))
243  );
244  $error->display();
245  return;
246  }
247  }
248 
249  $target = [];
250 
251  $target['objectClass'] = ['top', 'gosaSnapshotObject'];
252  $target['gosaSnapshotData'] = gzcompress($data, 6);
253  $target['gosaSnapshotDN'] = $dn;
254  $target['description'] = $description;
255  $target['fdSnapshotObjectType'] = $objectType;
256  $target['fdSnapshotDataSource'] = $snapshotSource;
257  $target['fdSnapshotHash'] = md5($data);
258 
259  /* Insert the new snapshot
260  But we have to check first, if the given gosaSnapshotTimestamp
261  is already used, in this case we should increment this value till there is
262  an unused value. */
263  do {
264  $target['gosaSnapshotTimestamp'] = str_replace('.', '', $sec.'-'.$usec);
265  $new_dn = 'gosaSnapshotTimestamp='.$target['gosaSnapshotTimestamp'].','.$new_base;
266  $ldap->cat($new_dn);
267  $usec++;
268  } while ($ldap->count());
269 
270  /* Insert this new snapshot */
271  $ldap->cd($this->snapshotRDN);
272  try {
273  $ldap->create_missing_trees($this->snapshotRDN);
274  $ldap->create_missing_trees($new_base);
275  } catch (FusionDirectoryError $error) {
276  $error->display();
277  }
278  $ldap->cd($new_dn);
279  $ldap->add($target);
280 
281  if (!$ldap->success()) {
282  $error = new FusionDirectoryLdapError($new_dn, LDAP_ADD, $ldap->get_error(), $ldap->get_errno());
283  $error->display();
284  }
285  logging::log('snapshot', 'create', $new_dn, array_keys($target), $ldap->get_error());
286  }
287 
288  // function verifing the configuration retention for snapshots.
289  // Remove snapshots from the user if retention rules approves.
290  public function verifySnapshotRetention (string $dn) : void
291  {
292  global $config;
293 
294  // In case the snap configuration has not set any numbers
295  if (isset($config->current['SNAPSHOTMINRETENTION']) && !empty($config->current['SNAPSHOTMINRETENTION'])) {
296  $snapMinRetention = $config->current['SNAPSHOTMINRETENTION'];
297  } else {
298  $snapMinRetention = 0;
299  }
300 
301  if (isset($config->current['SNAPSHOTRETENTIONDAYS']) && !empty($config->current['SNAPSHOTRETENTIONDAYS'])) {
302  $snapRetentionDays = $config->current['SNAPSHOTRETENTIONDAYS'];
303  } else {
304  $snapRetentionDays = -1;
305  }
306 
307  // calculate the epoch date on which snaps can be delete.
308  if ($snapRetentionDays !== -1) {
309  $todayMinusRetention = time() - ($snapRetentionDays * 24 * 60 * 60);
310  $snapDateToDelete = strtotime(date('Y-m-d H:i:s', $todayMinusRetention));
311 
312  $dnSnapshotsList = $this->getSnapshots($dn, TRUE);
313  $snapToDelete = [];
314  $snapCount = 0;
315 
316  // Generate an arrays with snapshot to delete due to overdate.
317  if (isset($dnSnapshotsList) && !empty($dnSnapshotsList)) {
318  foreach ($dnSnapshotsList as $snap) {
319  $snapCount += 1;
320  // let's keep seconds instead of nanosecs
321  $snapEpoch = preg_split('/-/', $snap['gosaSnapshotTimestamp'][0]);
322  if ($snapEpoch[0] < $snapDateToDelete) {
323  $snapToDelete[] = $snap['dn'];
324  }
325  }
326  }
327 
328  // The not empty is not mandatory but is more ressource friendly
329  if (!empty($snapToDelete) && ($snapCount > $snapMinRetention)) {
330  $snapToKeep = $snapCount - $snapMinRetention;
331  // Sort snapToDelete by old first DN timestamp is the only thing different.
332  sort($snapToDelete);
333  for ($i = 0; $i < $snapToKeep; $i++) {
334  // not empty required because array keeps on being iterated even if NULL object.
335  if (!empty($snapToDelete[$i])) {
336  $this->removeSnapshot($snapToDelete[$i]);
337  }
338  }
339  }
340  }
341  }
342 
348  function removeSnapshot ($dn)
349  {
350  global $config;
351  $ldap = $config->get_ldap_link();
352  $ldap->cd($config->current['BASE']);
353  $ldap->rmdir_recursive($dn);
354  if (!$ldap->success()) {
355  $error = new FusionDirectoryLdapError($dn, LDAP_DEL, $ldap->get_error(), $ldap->get_errno());
356  $error->display();
357  }
358  logging::log('snapshot', 'delete', $dn, [], $ldap->get_error());
359  }
360 
366  function getAvailableSnapsShots ($dn)
367  {
368  global $config;
369  if (!$this->enabled()) {
370  return [];
371  }
372 
373  $ldap = $config->get_ldap_link();
374 
375  /* Prepare bases and some other infos */
376  $base_of_object = preg_replace('/^[^,]+,/i', '', $dn);
377  $new_base = $this->snapshot_dn($base_of_object);
378  $tmp = [];
379 
380  /* Fetch all objects with gosaSnapshotDN=$dn */
381  $ldap->cd($new_base);
382  $ldap->search(
383  '(&(objectClass=gosaSnapshotObject)(gosaSnapshotDN='.ldap_escape_f($dn).'))',
384  ['gosaSnapshotTimestamp','gosaSnapshotDN','description','fdSnapshotObjectType','fdSnapshotHash'],
385  'one'
386  );
387 
388  /* Put results into a list and add description if missing */
389  while ($entry = $ldap->fetch(TRUE)) {
390  if (!isset($entry['description'][0])) {
391  $entry['description'][0] = "";
392  }
393  $tmp[] = $entry;
394  }
395 
396  return $tmp;
397  }
398 
404  function getAllDeletedSnapshots ($base_of_object)
405  {
406  global $config;
407  if (!$this->enabled()) {
408  return [];
409  }
410 
411  $ldap = $config->get_ldap_link();
412 
413  /* Prepare bases */
414  $new_base = $this->snapshot_dn($base_of_object);
415  /* Fetch all objects and check if they do not exist anymore */
416  $tmp = [];
417  $ldap->cd($new_base);
418  $ldap->search(
419  '(objectClass=gosaSnapshotObject)',
420  ['gosaSnapshotTimestamp','gosaSnapshotDN','description','fdSnapshotObjectType', 'fdSnapshotHash'],
421  'one'
422  );
423  while ($entry = $ldap->fetch(TRUE)) {
424  $chk = str_replace($new_base, "", $entry['dn']);
425  if (preg_match("/,ou=/", $chk)) {
426  continue;
427  }
428 
429  if (!isset($entry['description'][0])) {
430  $entry['description'][0] = "";
431  }
432  $tmp[] = $entry;
433  }
434 
435  /* Check if entry still exists */
436  foreach ($tmp as $key => $entry) {
437  if ($ldap->dn_exists($entry['gosaSnapshotDN'][0])) {
438  unset($tmp[$key]);
439  }
440  }
441 
442  return $tmp;
443  }
444 
445 
451  function restoreSnapshot ($dn)
452  {
453  global $config;
454  if (!$this->enabled()) {
455  logging::debug(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $dn, 'Snapshot are disabled but tried to restore snapshot');
456  return FALSE;
457  }
458 
459  $ldap = $config->get_ldap_link();
460 
461  /* Get the snapshot */
462  $ldap->cat($dn, ['gosaSnapshotData','gosaSnapshotDN'], '(gosaSnapshotData=*)');
463  if ($attrs = $ldap->fetch()) {
464  /* Prepare import string */
465  $data = gzuncompress($attrs['gosaSnapshotData'][0]);
466  if ($data === FALSE) {
467  $error = new FusionDirectoryError(htmlescape(_('There was a problem uncompressing snapshot data')));
468  $error->display();
469  return FALSE;
470  }
471  } else {
472  $error = new FusionDirectoryError(htmlescape(_('Snapshot data could not be fetched')));
473  $error->display();
474  return FALSE;
475  }
476 
477  /* Import the given data */
478  try {
479  $ldap->import_complete_ldif($data, FALSE, FALSE);
480  logging::log('snapshot', 'restore', $dn, [], $ldap->get_error());
481  if (!$ldap->success()) {
482  $error = new FusionDirectoryLdapError($dn, NULL, $ldap->get_error(), $ldap->get_errno());
483  $error->display();
484  return FALSE;
485  }
486  return $attrs['gosaSnapshotDN'][0];
487  } catch (LDIFImportException $e) {
488  $error = new FusionDirectoryError($e->getMessage(), 0, $e);
489  $error->display();
490  logging::log('snapshot', 'restore', $dn, [], $e->getMessage());
491  return FALSE;
492  }
493  }
494 }
htmlescape(string $str)
Escape string for HTML output.
Definition: php_setup.inc:32
getAvailableSnapsShots($dn)
Get the available snapshots.
static log(string $action, string $objecttype, string $object, array $changes=[], string $result='')
logging method
Error returned by an LDAP operation called from FusionDirectory.
enabled()
Check if the snapshot is enable.
getSnapshots($dn, $raw=FALSE)
Get snapshots.
hasDeletedSnapshots($bases)
Check if there are deleted snapshots.
initSnapshotCache($base)
Cache Snapshot information for all objects in $base.
static debug(int $level, int $line, string $function, string $file, $data, string $info='')
Debug output method.
Exception class which can be thrown by LDAP class if LDIF export fails.
getAllDeletedSnapshots($base_of_object)
Get all deleted snapshots.
This class contains all the function needed to handle the snapshot functionality. ...
removeSnapshot($dn)
Remove a snapshot.
Parent class for all errors in FusionDirectory.
createSnapshot($dn, string $description, string $objectType, string $snapshotSource='FD')
Create a snapshot of the current object.
restoreSnapshot($dn)
Restore selected snapshot.
Exception class which can be thrown by LDAP if the LDIF format is broken.
__construct()
Create handler.
hasSnapshots($dn)
Check if the DN has snapshots.