Commit ed23d323 authored by Côme Chilliet's avatar Côme Chilliet
Browse files

:sparkles: feat(template) Attempt at supporting unique value checks in template masks

The code tries to support generators from modifiers so that random
 string and future number modifiers can return a generator and not store
 large arrays in memory.

issue #5882
Showing with 89 additions and 139 deletions
+89 -139
......@@ -357,7 +357,7 @@ class template
foreach (static::$uiSpecialAttributes as $attr) {
$specialAttrs['caller'.strtoupper($attr)] = $ui->$attr;
}
$this->attrs = templateHandling::parseArray($this->attrs, $specialAttrs);
$this->attrs = templateHandling::parseArray($this->attrs, $specialAttrs, $targetdn);
$this->tabObject->adapt_from_template($this->attrs, array_merge(...array_values($this->attributes)));
$this->applied = TRUE;
......
......@@ -186,18 +186,22 @@ class templateHandling
}
/*! \brief Parse template masks in an array
*
* \param array $attrs The attributes in LDAP format
* \param array $specialAttrs Some additional fake attributes which can be used in masks
* \param string $target Dn of an object to ignore in the unicity check
*
* \return array An array with the final values of attributes
*/
public static function parseArray(array $attrs, array $specialAttrs)
public static function parseArray(array $attrs, array $specialAttrs, $target = NULL)
{
foreach ($attrs as &$attr) {
foreach ($attrs as $name => &$attr) {
if (is_array($attr)) {
foreach ($attr as $key => &$string) {
if (!is_numeric($key)) {
continue;
}
$string = static::parseString($string, array_merge($attrs, $specialAttrs));
$string = static::parseString($string, array_merge($attrs, $specialAttrs), NULL, $name, $target);
}
unset($string);
}
......@@ -207,28 +211,82 @@ class templateHandling
}
/*! \brief Parse template masks in a single string
*
* \param string $string The mask
* \param array $attrs The attributes in LDAP format
* \param callable $escapeMethod Method to call to escape mask result
* \param string $unique Name of the LDAP attribute to check unicity on, if any
* \param string $target Dn of an object to ignore in the unicity check
*
* \return string the string with patterns replaced by their values
*/
public static function parseString($string, array $attrs, $escapeMethod = NULL)
public static function parseString($string, array $attrs, $escapeMethod = NULL, $unique = NULL, $target = NULL)
{
global $config;
if (preg_match('/^%%/', $string)) {
/* Special case: %% at beginning of string means do not touch it. Used by binary attributes. */
return preg_replace('/^%%/', '', $string);
}
$offset = 0;
while (preg_match('/%([^%]+)%/', $string, $m, PREG_OFFSET_CAPTURE, $offset)) {
$vars = array();
while (preg_match('/%([^%]+)%/', $string, $m, PREG_OFFSET_CAPTURE)) {
$replace = static::parseMask($m[1][0], $attrs);
$replace = $replace[0];
if ($escapeMethod !== NULL) {
$replace = $escapeMethod($replace);
$vars[] = array($m[0][1], strlen($m[0][0]), $replace);
}
$generator = static::iteratePossibleValues($string, $vars, $escapeMethod);
$string = $generator->current();
if (($unique !== NULL) && !empty($vars)) {
$ldap = $config->get_ldap_link();
$ldap->cd($config->current['BASE']);
/* Return the first found unique value */
foreach ($generator as $value) {
$filter = '('.ldap_escape_f($unique).'='.ldap_escape_f($value).')';
$ldap->search($filter, array('dn'));
if (
($ldap->count() == 0) ||
(($target !== NULL) && ($ldap->count() == 1) && ($ldap->getDN() == $target))
) {
return $value;
}
}
$string = substr_replace($string, $replace, $m[0][1], strlen($m[0][0]));
$offset = $m[0][1] + strlen($replace);
}
return $string;
}
/*! \brief Generator that yields possible template mask values
*
* \param string $rule The mask
* \param array $variables The possible values for each mask with its position and length
* \param callable $escapeMethod Method to call to escape mask result
*/
protected static function iteratePossibleValues($rule, array $variables, $escapeMethod = NULL)
{
if (!count($variables)) {
yield $rule;
return;
}
/* Start from the end to avoid messing the positions, and to avoid ids at the end if not needed (common usecase) */
list($pos, $length, $val) = array_pop($variables);
/* $val may be an iterator or an array */
foreach ($val as $possibility) {
if ($escapeMethod !== NULL) {
$possibility = $escapeMethod($possibility);
}
$nrule = mb_substr_replace($rule, $possibility, $pos, $length);
foreach (static::iteratePossibleValues($nrule, $variables, $escapeMethod) as $result) {
yield $result;
}
}
}
/*! \brief Parse template masks in a single string and list the fields it needs
*
* \return array An array with the names of the fields used in the string pattern
......@@ -389,7 +447,6 @@ class templateHandling
/* $str is an array and $m is lowercase, so it's a string modifier */
$str = reset($str);
}
$result = array($str);
switch ($m) {
case 'F':
// First
......
......@@ -1292,131 +1292,6 @@ function netmask_to_bits($netmask)
return $res;
}
/*!
* \brief Recursion helper for gen_uids()
*/
function _recurse_gen_uids($rule, $variables)
{
$result = array();
if (!count($variables)) {
return array($rule);
}
reset($variables);
$key = key($variables);
$val = current($variables);
unset($variables[$key]);
foreach ($val as $possibility) {
$nrule = str_replace("{$key}", $possibility, $rule);
$result = array_merge($result, _recurse_gen_uids($nrule, $variables));
}
return $result;
}
/*!
* \brief Generate a list of uid proposals based on a rule
*
* Unroll given rule string by filling in attributes and replacing
* all keywords.
*
* \param string $rule The rule string from fusiondirectory.conf.
*
* \param array $attributes A dictionary of attribute/value mappings
*
* \return array List of valid not used uids
*/
function gen_uids($rule, array $attributes)
{
global $config;
// Attributes should be arrays
foreach ($attributes as $name => $value) {
$attributes[$name] = array($value);
}
/* Search for keys and fill the variables array with all
possible values for that key. */
$stripped = $rule;
$variables = array();
for ($pos = 0; preg_match('/%([^%]+)%/', $stripped, $m, PREG_OFFSET_CAPTURE, $pos); ) {
$variables[$pos] = templateHandling::parseMask($m[1][0], $attributes);
$replace = '{'.$pos.'}';
$stripped = substr_replace($stripped, $replace, $m[0][1], strlen($m[0][0]));
$pos = $m[0][1] + strlen($replace);
}
/* Recurse through all possible combinations */
$proposed = _recurse_gen_uids($stripped, $variables);
/* Get list of used ID's */
$ldap = $config->get_ldap_link();
$ldap->cd($config->current['BASE']);
/* Remove used uids and watch out for id tags */
$ret = array();
foreach ($proposed as $uid) {
/* Check for id tag and modify uid if needed */
if (preg_match('/\{id(:|!)(\d+)}/', $uid, $m)) {
$size = $m[2];
$start = ($m[1] == ":" ? 0 : -1);
for ($i = $start, $p = (10 ** $size) - 1; $i < $p; $i++) {
if ($i == -1) {
$number = "";
} else {
$number = sprintf("%0".$size."d", $i + 1);
}
$res = preg_replace('/{id(:|!)\d+}/', $number, $uid);
$ldap->search('(uid='.ldap_escape_f(preg_replace('/[{}]/', '', $res)).')', array('dn'));
if ($ldap->count() == 0) {
$uid = $res;
break;
}
}
/* Remove link if nothing has been found */
$uid = preg_replace('/{id(:|!)\d+}/', '', $uid);
}
if (preg_match('/\{id#\d+}/', $uid)) {
$size = preg_replace('/^.*{id#(\d+)}.*$/', '\\1', $uid);
while (TRUE) {
$number = sprintf("%0".$size."d", random_int(0, (10 ** $size) - 1));
$res = preg_replace('/{id#(\d+)}/', $number, $uid);
$ldap->search('(uid='.ldap_escape_f(preg_replace('/[{}]/', '', $res)).')', array('dn'));
if ($ldap->count() == 0) {
$uid = $res;
break;
}
}
/* Remove link if nothing has been found */
$uid = preg_replace('/{id#\d+}/', '', $uid);
}
/* Don't assign used ones */
$ldap->search('(uid='.ldap_escape_f(preg_replace('/[{}]/', '', $uid)).')', array('dn'));
if ($ldap->count() == 0) {
/* Add uid, but remove {} first. These are invalid anyway. */
$uid = preg_replace('/[{}]/', '', $uid);
if ($uid != '') {
$ret[] = $uid;
}
}
}
return array_unique($ret);
}
/*!
* \brief Convert various data sizes to bytes
*
......@@ -2315,4 +2190,22 @@ function fopenWithErrorHandling(...$args)
}
return $errors;
}
?>
// Check to see if it exists in case PHP has this function later
if (!function_exists('mb_substr_replace')) {
// Same parameters as substr_replace with the extra encoding parameter
function mb_substr_replace ($string, $replacement, $start, $length = NULL, $encoding = NULL)
{
if ($encoding == NULL) {
$encoding = mb_internal_encoding();
}
if ($length == NULL) {
return mb_substr($string, 0, $start, $encoding).
$replacement;
} else {
return mb_substr($string, 0, $start, $encoding).
$replacement.
mb_substr($string, $start + $length, mb_strlen($string, $encoding), $encoding);
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment