diff --git a/include/class_template.inc b/include/class_template.inc
index fb0deaea40e07b44986f3db6a486058dcec4fabc..f827e7707b45ef48f81892ff5f08a717fa9b1b34 100644
--- a/include/class_template.inc
+++ b/include/class_template.inc
@@ -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;
diff --git a/include/class_templateHandling.inc b/include/class_templateHandling.inc
index 1bffdc7f5d81395a827ca477ff5523c3aedc0da4..d1072a28bf0ec518c58be51bf0db8e87a7b898c0 100644
--- a/include/class_templateHandling.inc
+++ b/include/class_templateHandling.inc
@@ -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
diff --git a/include/functions.inc b/include/functions.inc
index 176e34ca0ce25ca3d17af86fd7524627b0c627ff..68fed3acb24dd25c55f76f8262b9ae7ec52121c9 100644
--- a/include/functions.inc
+++ b/include/functions.inc
@@ -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);
+    }
+  }
+}