FusionDirectory
class_templateHandling.inc
Go to the documentation of this file.
1 <?php
2 /*
3  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
4  Copyright (C) 2011-2016 FusionDirectory
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10 
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with this program; if not, write to the Free Software
18  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
19 */
20 
29 {
31  public static function fetch ($dn)
32  {
33  global $config;
34 
35  $ldap = $config->get_ldap_link();
36  $ldap->cat($dn);
37  $attrs = $ldap->fetch(TRUE);
38  $attrs = static::fieldsFromLDAP($attrs);
39  list($depends, $errors) = static::attributesDependencies($attrs);
40  msg_dialog::displayChecks($errors);
41  $attrs = static::sortAttributes($attrs, $depends);
42  return [$attrs, $depends];
43  }
44 
46  public static function fieldsFromLDAP (array $template_attrs)
47  {
48  $attrs = [];
49  if (isset($template_attrs['fdTemplateField'])) {
50  unset($template_attrs['fdTemplateField']['count']);
51  sort($template_attrs['fdTemplateField']);
52  foreach ($template_attrs['fdTemplateField'] as $field) {
53  if (!preg_match('/^([^:]+):(.*)$/s', $field, $m)) {
54  throw new FusionDirectoryException('Template field does not match format');
55  }
56  if (isset($attrs[$m[1]])) {
57  $attrs[$m[1]][] = $m[2];
58  $attrs[$m[1]]['count']++;
59  } else {
60  $attrs[$m[1]] = [$m[2]];
61  $attrs[$m[1]]['count'] = 1;
62  }
63  }
64  }
65  return $attrs;
66  }
67 
69  public static function fieldsToLDAP (array $template_attrs, array $attrs)
70  {
71  /* First a bit of cleanup */
72  unset($template_attrs['dn']);
73  unset($template_attrs['fdTemplateField']['count']);
74  unset($template_attrs['objectClass']['count']);
75  unset($template_attrs['cn']['count']);
76  if (isset($template_attrs['count'])) {
77  for ($i = 0; $i < $template_attrs['count']; ++$i) {
78  /* Remove numeric keys */
79  unset($template_attrs[$i]);
80  }
81  }
82  unset($template_attrs['count']);
83 
84  /* Remove all concerned values */
85  foreach ($template_attrs['fdTemplateField'] as $key => $value) {
86  preg_match('/^([^:]+):(.*)$/s', $value, $m);
87  if (isset($attrs[$m[1]])) {
88  unset($template_attrs['fdTemplateField'][$key]);
89  }
90  }
91  /* Then insert non-empty values */
92  foreach ($attrs as $key => $value) {
93  if (is_array($value)) {
94  foreach ($value as $v) {
95  if ($value == "") {
96  continue;
97  }
98  $template_attrs['fdTemplateField'][] = $key.':'.$v;
99  }
100  } else {
101  if ($value == "") {
102  continue;
103  }
104  $template_attrs['fdTemplateField'][] = $key.':'.$value;
105  }
106  }
107  sort($template_attrs['fdTemplateField']);
108  return $template_attrs;
109  }
110 
116  public static function checkFields ($attrs)
117  {
118  list(, $errors) = static::attributesDependencies($attrs);
119  return $errors;
120  }
121 
126  public static function parseMask (string $mask, array $attrs)
127  {
128  if ($mask == '|') {
129  return ['%'];
130  }
131  $modifiers = '';
132  if (preg_match('/^([^|]+)\|/', $mask, $m)) {
133  $modifiers = $m[1];
134  $mask = substr($mask, strlen($m[0]));
135  }
136  $result = [];
137  if (isset($attrs[$mask])) {
138  $result = $attrs[$mask];
139  if (is_array($result)) {
140  unset($result['count']);
141  if (empty($result)) {
142  /* No value and empty value is the same in LDAP so we have to treat them the same */
143  $result = [''];
144  }
145  } else {
146  $result = [$result];
147  }
148  } elseif (($mask != '') && !preg_match('/c/', $modifiers)) {
149  throw new FusionDirectoryException(sprintf(_('"%s" was not found in attributes'), $mask));
150  }
151  $len = strlen($modifiers);
152  for ($i = 0; $i < $len; ++$i) {
153  $args = [];
154  $modifier = $modifiers[$i];
155  if (preg_match('/^\[([^\]]+)\].*$/', substr($modifiers, $i + 1), $m)) {
156  /* get modifier args */
157  $args = explode(',', $m[1]);
158  $i += strlen($m[1]) + 2;
159  }
160  $result = static::applyModifier($modifier, $args, $result);
161  }
162  return $result;
163  }
164 
169  public static function neededAttrs (array &$attrs, array $flatdepends)
170  {
171  $needed = [];
172  foreach ($flatdepends as $attr => $depends) {
173  if ((isset($depends[0])) && ($depends[0] == 'askme')) {
174  $needed[] = $attr;
175  unset($flatdepends[$attr]);
176  unset($attrs[$attr]);
177  }
178  }
179  $dependencies = array_unique(array_merge(...array_values($flatdepends)));
180  foreach ($dependencies as $attr) {
181  if (empty($flatdepends[$attr])) {
182  $needed[] = $attr;
183  }
184  }
185  return array_unique($needed);
186  }
187 
196  public static function parseArray (array $attrs, array $specialAttrs, $target = NULL)
197  {
198  foreach ($attrs as $name => &$attr) {
199  if (is_array($attr)) {
200  foreach ($attr as $key => &$string) {
201  if (!is_numeric($key)) {
202  continue;
203  }
204  $string = static::parseString($string, array_merge($attrs, $specialAttrs), NULL, $name, $target);
205  }
206  unset($string);
207  }
208  }
209  unset($attr);
210  return $attrs;
211  }
212 
223  public static function parseString (string $string, array $attrs, $escapeMethod = NULL, string $unique = NULL, string $target = NULL): string
224  {
225  global $config;
226 
227  if (preg_match('/^%%/', $string)) {
228  /* Special case: %% at beginning of string means do not touch it. Used by binary attributes. */
229  return preg_replace('/^%%/', '', $string);
230  }
231 
232  $offset = 0;
233  $vars = [];
234  while (preg_match('/%([^%]+)%/', $string, $m, PREG_OFFSET_CAPTURE, $offset)) {
235  $replace = static::parseMask($m[1][0], $attrs);
236  $vars[] = [$m[0][1], strlen($m[0][0]), $replace];
237  $offset = $m[0][1] + strlen($m[0][0]);
238  }
239 
240  $generator = static::iteratePossibleValues($string, $vars, $escapeMethod);
241 
242  $string = $generator->current();
243 
244  if (($unique !== NULL) && !empty($vars)) {
245  $ldap = $config->get_ldap_link();
246  $ldap->cd($config->current['BASE']);
247  /* Return the first found unique value */
248  foreach ($generator as $value) {
249  if (class_available('archivedObject')) {
250  $filter = archivedObject::buildUniqueSearchFilter($unique, $value);
251  $ldap->search($filter, ['dn']);
252  if ($ldap->count() > 0) {
253  continue;
254  }
255  }
256  $filter = '('.ldap_escape_f($unique).'='.ldap_escape_f($value).')';
257  $ldap->search($filter, ['dn']);
258  if ($ldap->count() == 0) {
259  return $value;
260  }
261  if (($target !== NULL) && ($ldap->count() == 1)) {
262  $attrs = $ldap->fetch();
263  if ($attrs['dn'] == $target) {
264  return $value;
265  }
266  }
267  }
268  }
269 
270  return $string;
271  }
272 
273 
280  protected static function iteratePossibleValues (string $rule, array $variables, $escapeMethod = NULL)
281  {
282  if (!count($variables)) {
283  yield $rule;
284  return;
285  }
286 
287  /* Start from the end to avoid messing the positions, and to avoid ids at the end if not needed (common usecase) */
288  list($pos, $length, $val) = array_pop($variables);
289 
290  /* $val may be an iterator or an array */
291  foreach ($val as $possibility) {
292  if ($escapeMethod !== NULL) {
293  $possibility = $escapeMethod($possibility);
294  }
295  $nrule = mb_substr_replace($rule, $possibility, $pos, $length);
296  foreach (static::iteratePossibleValues($nrule, $variables, $escapeMethod) as $result) {
297  yield $result;
298  }
299  }
300  }
301 
306  public static function listFields ($string)
307  {
308  $fields = [];
309  $offset = 0;
310  while (preg_match('/%([^%]+)%/', $string, $m, PREG_OFFSET_CAPTURE, $offset)) {
311  $mask = $m[1][0];
312  $offset = $m[0][1] + strlen($m[0][0]);
313  if ($mask == '|') {
314  continue;
315  }
316  if (preg_match('/^([^|]+)\|/', $mask, $m)) {
317  $mask = substr($mask, strlen($m[0]));
318  }
319  $fields[] = $mask;
320  }
321  return $fields;
322  }
323 
324  private static function modifierRemoveAccents (array $args, $str)
325  {
326  $mode = 'ascii';
327  if (count($args) >= 1) {
328  $mode = $args[0];
329  }
330 
331  $str = htmlentities($str, ENT_NOQUOTES, 'UTF-8');
332 
333  $str = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $str);
334  // handle ligatures
335  $str = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $str);
336  // delete unhandled characters
337  $str = preg_replace('#&[^;]+;#', '', $str);
338 
339  if ($mode === 'ascii') {
340  return [$str];
341  } elseif ($mode === 'uid') {
342  if (strict_uid_mode()) {
343  $str = preg_replace('/[^a-z0-9_-]/', '', mb_strtolower($str, 'UTF-8'));
344  } else {
345  $str = preg_replace('/[^a-zA-Z0-9 _.-]/', '', $str);
346  }
347  return [$str];
348  } else {
349  throw new FusionDirectoryException(_('Invalid mode for "a" modifier, supported modes are "uid" and "ascii"'));
350  }
351  }
352 
353  private static function modifierTranslit (array $args, $str)
354  {
355  $localesaved = setlocale(LC_CTYPE, 0);
356  $ret = [];
357  foreach ($args as $arg) {
358  setlocale(LC_CTYPE, [$arg,"$arg.UTF8"]);
359  $ret[] = iconv('UTF8', 'ASCII//TRANSLIT', $str);
360  }
361  setlocale(LC_CTYPE, $localesaved);
362  return array_unique($ret);
363  }
364 
365  private static function modifierPregReplace (array $args, $str)
366  {
367  $pattern = '/\s/';
368  $replace = '';
369  if (count($args) >= 1) {
370  $pattern = $args[0];
371  if (count($args) >= 2) {
372  $replace = $args[1];
373  }
374  }
375 
376  return [preg_replace($pattern.'u', $replace, $str)];
377  }
378 
379  private static function modifierSubString (array $args, $str)
380  {
381  if (count($args) < 1) {
382  trigger_error("Missing 's' substr modifier parameter");
383  }
384  if (count($args) < 2) {
385  array_unshift($args, 0);
386  }
387  if (preg_match('/^(\d+)-(\d+)$/', $args[1], $m)) {
388  $res = [];
389  for ($i = $m[1];$i <= $m[2]; ++$i) {
390  $res[] = mb_substr($str, $args[0], $i);
391  }
392  return array_unique($res);
393  } else {
394  return [mb_substr($str, $args[0], $args[1])];
395  }
396  }
397 
398  private static function modifierRandomString (array $args)
399  {
400  $length = 8;
401  $chars = 'b';
402  if (count($args) >= 2) {
403  $length = random_int($args[0], $args[1]);
404  if (count($args) >= 3) {
405  $chars = $args[2];
406  }
407  } elseif (count($args) >= 1) {
408  $length = $args[0];
409  }
410  $res = '';
411  for ($i = 0; $i < $length; ++$i) {
412  switch ($chars) {
413  case 'd':
414  /* digits */
415  $res .= (string)random_int(0, 9);
416  break;
417  case 'l':
418  /* letters */
419  $nb = random_int(65, 116);
420  if ($nb > 90) {
421  /* lowercase */
422  $nb += 6;
423  }
424  $res .= chr($nb);
425  break;
426  case 'b':
427  /* both */
428  default:
429  $nb = random_int(65, 126);
430  if ($nb > 116) {
431  /* digit */
432  $nb = (string)($nb - 117);
433  } else {
434  if ($nb > 90) {
435  /* lowercase */
436  $nb += 6;
437  }
438  $nb = chr($nb);
439  }
440  $res .= $nb;
441  break;
442  }
443  }
444 
445  return [$res];
446  }
447 
448  private static function modifierDate (array $args)
449  {
450  if (count($args) < 1) {
451  $args[] = 'now';
452  }
453  if (count($args) < 2) {
454  $args[] = 'Y-m-d';
455  }
456  $dateObject = new DateTime($args[0], new DateTimeZone('UTC'));
457  if ($args[1] == 'epoch') {
458  /* Special handling for shadowExpire: days since epoch */
459  return [floor($dateObject->format('U') / 86400)];
460  }
461  return [$dateObject->format($args[1])];
462  }
463 
464  /*
465  First parameter is whether the number should always be there or only in case of duplicates (1 or 0, defaults to 0).
466  Second parameter is starting number, defaults to 1.
467  Third parameter is step, defaults to 1.
468  */
469  private static function modifierNumber (array $args)
470  {
471  if (count($args) < 1) {
472  $args[] = FALSE;
473  }
474  if (count($args) < 2) {
475  $args[] = 1;
476  }
477  if (count($args) < 3) {
478  $args[] = 1;
479  }
480  $numberGenerator = function ($mandatory, $start, $step)
481  {
482  if (!$mandatory) {
483  yield '';
484  }
485  $i = $start;
486  while (TRUE) {
487  yield $i;
488  $i += $step;
489  }
490  };
491 
492  return $numberGenerator($args[0], $args[1], $args[2]);
493  }
494 
495  /*
496  Modifier parameters:
497  * id
498  * starting number, defaults to 1.
499  * step, defaults to 1.
500  */
501  private static function modifierIncremental (array $args): array
502  {
503  global $config;
504 
505  if (count($args) < 1) {
506  throw new FusionDirectoryException(_('Missing id parameter for incremental modifier'));
507  }
508  if (count($args) < 2) {
509  $args[] = 1;
510  }
511  if (count($args) < 3) {
512  $args[] = 1;
513  }
514  $configDn = CONFIGRDN.$config->current['BASE'];
515  Lock::addOrFail($configDn);
516  $tabObject = objects::open($configDn, 'configuration');
517  $json = $tabObject->getBaseObject()->fdIncrementalModifierStates;
518  if (empty($json)) {
519  $modifierStates = [];
520  } else {
521  $modifierStates = json_decode($json, TRUE);
522  }
523  if (isset($modifierStates[$args[0]])) {
524  $value = $modifierStates[$args[0]]['value'] + $args[2];
525  } else {
526  $value = $args[1];
527  }
528  $modifierStates[$args[0]] = [
529  'value' => $value,
530  'date' => date('Y-m-d'),
531  ];
532  $tabObject->getBaseObject()->fdIncrementalModifierStates = json_encode($modifierStates);
533  $errors = $tabObject->save();
534  Lock::deleteByObject($configDn);
535  if (!empty($errors)) {
536  throw $errors[0];
537  }
538 
539  return [$value];
540  }
541 
542  private static function modifierTitleCase ($str)
543  {
544  return [mb_convert_case($str, MB_CASE_TITLE, 'UTF-8')];
545  }
546 
555  protected static function applyModifier (string $m, array $args, $str)
556  {
557  mb_internal_encoding('UTF-8');
558  mb_regex_encoding('UTF-8');
559  if (is_array($str) && (!is_numeric($m)) && (strtolower($m) == $m)) {
560  /* $str is an array and $m is lowercase, so it's a string modifier */
561  if (count($str) == 0) {
562  $str = '';
563  } else {
564  $str = reset($str);
565  }
566  }
567  switch ($m) {
568  case 'F':
569  // First
570  $result = [reset($str)];
571  break;
572  case 'L':
573  // Last
574  $result = [end($str)];
575  break;
576  case 'J':
577  // Join
578  if (isset($args[0])) {
579  $result = [join($args[0], $str)];
580  } else {
581  $result = [join($str)];
582  }
583  break;
584  case 'C':
585  // Count
586  $result = [count($str)];
587  break;
588  case 'M':
589  // Match
590  if (count($args) < 1) {
591  trigger_error('Missing "M" match modifier parameter');
592  $args[] = '/.*/';
593  }
594  $result = array_filter(
595  $str,
596  function ($s) use ($args)
597  {
598  return preg_match($args[0], $s);
599  }
600  );
601  break;
602  case '4':
603  // IPv4
604  $result = array_filter($str, 'tests::is_ipv4');
605  break;
606  case '6':
607  // IPv6
608  $result = array_filter($str, 'tests::is_ipv6');
609  break;
610  case 'c':
611  // comment
612  $result = [''];
613  break;
614  case 'b':
615  // base64
616  if (isset($args[0]) && ($args[0] == 'd')) {
617  $result = [base64_decode($str)];
618  }
619  $result = [base64_encode($str)];
620  break;
621  case 'u':
622  // uppercase
623  $result = [mb_strtoupper($str, 'UTF-8')];
624  break;
625  case 'l':
626  // lowercase
627  $result = [mb_strtolower($str, 'UTF-8')];
628  break;
629  case 'a':
630  // remove accent
631  $result = static::modifierRemoveAccents($args, $str);
632  break;
633  case 't':
634  // translit
635  $result = static::modifierTranslit($args, $str);
636  break;
637  case 'p':
638  // spaces
639  $result = static::modifierPregReplace($args, $str);
640  break;
641  case 's':
642  // substring
643  $result = static::modifierSubString($args, $str);
644  break;
645  case 'r':
646  // random string
647  $result = static::modifierRandomString($args);
648  break;
649  case 'd':
650  // date
651  $result = static::modifierDate($args);
652  break;
653  case 'n':
654  // number
655  $result = static::modifierNumber($args);
656  break;
657  case 'i':
658  // title case
659  $result = static::modifierTitleCase($str);
660  break;
661  case 'e':
662  // incremental number
663  $result = static::modifierIncremental($args);
664  break;
665  default:
666  trigger_error("Unkown modifier '$m'");
667  $result = [$str];
668  break;
669  }
670  return $result;
671  }
672 
674  protected static function flatDepends (&$cache, &$errors, $depends, $key, array $forbidden = [])
675  {
676  if (isset($cache[$key])) {
677  return $cache[$key];
678  }
679 
680  $forbidden[] = $key;
681 
682  $array =
683  array_map(
684  function ($a) use (&$cache, &$errors, $depends, $forbidden, $key)
685  {
686  if (in_array($a, $forbidden)) {
687  $errors[] = sprintf(
688  _('Recursive dependency in the template fields: "%1$s" cannot depend on "%2$s" as "%2$s" already depends on "%1$s"'),
689  $key,
690  $a
691  );
692  return [];
693  }
694  $deps = static::flatDepends($cache, $errors, $depends, $a, $forbidden);
695  if (($askmeKey = array_search('askme', $deps)) !== FALSE) {
696  /* Do not flat special askme dependency */
697  unset($deps[$askmeKey]);
698  }
699  return $deps;
700  },
701  $depends[$key]
702  );
703  $array[] = $depends[$key];
704  $cache[$key] = array_unique(array_merge_recursive(...$array));
705  return $cache[$key];
706  }
707 
709  protected static function attributesDependencies (array $attrs)
710  {
711  /* Compute dependencies of each attr */
712  $depends = [];
713  foreach ($attrs as $key => $values) {
714  $depends[$key] = [];
715  if (!is_array($values)) {
716  $values = [$values];
717  }
718  unset($values['count']);
719  foreach ($values as $value) {
720  $offset = 0;
721  while (preg_match('/%([^%\|]+\|)?([^%]+)%/', $value, $m, PREG_OFFSET_CAPTURE, $offset)) {
722  $offset = $m[0][1] + strlen($m[0][0]);
723  $depends[$key][] = $m[2][0];
724  if (!isset($attrs[$m[2][0]])) {
725  /* Dependency which has no value might be missing */
726  $attrs[$m[2][0]] = [];
727  $depends[$m[2][0]] = [];
728  }
729  }
730  }
731  }
732  /* Flattens dependencies */
733  $flatdepends = [];
734  $errors = [];
735  foreach ($depends as $key => $value) {
736  static::flatDepends($flatdepends, $errors, $depends, $key);
737  }
738  return [$flatdepends, $errors];
739  }
740 
742  protected static function sortAttributes (array $attrs, array $flatdepends)
743  {
744  uksort($attrs, function ($k1, $k2) use ($flatdepends)
745  {
746  if (in_array($k1, $flatdepends[$k2])) {
747  return -1;
748  } elseif (in_array($k2, $flatdepends[$k1])) {
749  return 1;
750  } else {
751  /* When no direct dependency, we sort by number of dependencies */
752  $c1 = count($flatdepends[$k1]);
753  $c2 = count($flatdepends[$k2]);
754  if ($c1 == $c2) {
755  return 0;
756  }
757  return (($c1 < $c2) ? -1 : 1);
758  }
759  });
760  return $attrs;
761  }
762 }
this class stores static methods used to parse templates LDAP data
static listFields($string)
Parse template masks in a single string and list the fields it needs.
static parseArray(array $attrs, array $specialAttrs, $target=NULL)
Parse template masks in an array.
const CONFIGRDN
FusionDirectory config object RDN.
static flatDepends(&$cache, &$errors, $depends, $key, array $forbidden=[])
Flattens dependencies (if a depends of b which depends of c then a depends of c)
static fetch($dn)
Fetch a template from LDAP and returns its attributes and dependencies information.
static addOrFail($object, string $user=NULL, int $retries=10)
Add a lock for object(s) or fail.
Definition: class_Lock.inc:294
static open(string $dn, string $type)
Create the tab object for the given dn.
Parent class for all exceptions thrown in FusionDirectory.
static neededAttrs(array &$attrs, array $flatdepends)
Return attrs needed before applying template.
static deleteByObject($object)
Remove a lock for object(s)
Definition: class_Lock.inc:135
static fieldsFromLDAP(array $template_attrs)
Translate template attrs into $attrs as if taken from LDAP.
static fieldsToLDAP(array $template_attrs, array $attrs)
Translate $attrs into template attrs.
strict_uid_mode()
Check if strict naming rules are configured.
Definition: functions.inc:526
static sortAttributes(array $attrs, array $flatdepends)
Sort attrs depending of dependencies.
static attributesDependencies(array $attrs)
Computes dependencies between attributes: which attributes must be filled in order to compute each at...
static parseMask(string $mask, array $attrs)
Parse a mask (without surrounding %) using $attrs attributes and apply modifiers. ...
static applyModifier(string $m, array $args, $str)
Apply a modifier.
static checkFields($attrs)
Check template fields.
static iteratePossibleValues(string $rule, array $variables, $escapeMethod=NULL)
Generator that yields possible template mask values.
static parseString(string $string, array $attrs, $escapeMethod=NULL, string $unique=NULL, string $target=NULL)
Parse template masks in a single string.
class_available($name)
Checks if a class is available.
Definition: functions.inc:92