Browse Source

Update User\IsUniqueName

SQLite databases (without an ICU module) and PostgreSQL (up to version 12 and without collation with Provider = ICU, Deterministic = false) have problems in comparison of case-insensitive UTF-8 strings. This variant of the algorithm is trying to solve the problem.
Visman 3 năm trước cách đây
mục cha
commit
c0f9f64ff4
1 tập tin đã thay đổi với 131 bổ sung10 xóa
  1. 131 10
      app/Models/User/IsUniqueName.php

+ 131 - 10
app/Models/User/IsUniqueName.php

@@ -15,27 +15,148 @@ use ForkBB\Models\User\User;
 
 
 class IsUniqueName extends Action
 class IsUniqueName extends Action
 {
 {
+    /**
+     * Добавляет экранированный символ в конец каждого элемента $input дописывая их в $output
+     */
+    protected function addSymbol(array $input, string $symbol, array $output = []): array
+    {
+        if ('#%' === $symbol) {
+            $symbol = '%';
+        } else {
+            $symbol = \str_replace(['#', '_', '%'], ['##', '#_', '#%'], $symbol);
+        }
+
+        if (empty($input)) {
+            $input = [''];
+        }
+
+        foreach ($input as $str) {
+            $output[] = $str . $symbol;
+        }
+
+        return $output;
+    }
+
+    /**
+     * Строит массив вариантов для сранения LIKE
+     */
+    protected function variants(string $str): array
+    {
+        \preg_match_all('%.%us', $str, $matches);
+
+        $result = [];
+
+        foreach ($matches[0] as $i => $symbol) {
+            $tmp = [];
+
+            if (isset($symbol[1])) {
+                if ($i > 1) {
+                    return $this->addSymbol($result, '#%'); // добавить % (без #) в конец каждого элемента
+                }
+
+                $symbolL = \mb_strtolower($symbol, 'UTF-8');
+                $symbolU = \mb_strtoupper($symbol, 'UTF-8');
+
+                if ($symbolL !== $symbol) {
+                    $tmp = $this->addSymbol($result, $symbolL, $tmp);
+                }
+
+                if ($symbolU !== $symbol) {
+                    $tmp = $this->addSymbol($result, $symbolU, $tmp);
+                }
+            }
+
+            $result = $this->addSymbol($result, $symbol, $tmp);
+        }
+
+        return $result;
+    }
+
     /**
     /**
      * Проверка на уникальность имени пользователя
      * Проверка на уникальность имени пользователя
      */
      */
     public function isUniqueName(User $user): bool
     public function isUniqueName(User $user): bool
     {
     {
+        $name7bit = 0 === \preg_match('%[\x80-\xFF]%', $user->username);
+        $norm7bit = 0 === \preg_match('%[\x80-\xFF]%', $user->username_normal);
+        $like     = 'LIKE';
+
+        switch ($this->c->DB->getType()) {
+            case 'mysql':
+                break;
+            case 'pgsql':
+                $like = 'ILIKE';
+            case 'sqlite':
+            default:
+                // UTF-8 не нужен
+                if ($name7bit && $norm7bit) {
+                    break;
+                }
+                // бд поддерживает UTF-8 сравнение без учета регистра
+                if ($this->c->DB->query("SELECT ?s {$like} ?s", ['Ы', 'ы'])->fetchColumn()) {
+                    break;
+                }
+
+                $vars  = [(int) $user->id];
+                $query = 'SELECT u.username, u.username_normal
+                    FROM ::users AS u
+                    WHERE u.id!=?i
+                        AND (';
+
+                $sptr = '';
+                $arr  = $this->variants($user->username);
+
+                foreach ($arr as $value) {
+                    $vars[] = $value;
+                    $query .= "{$sptr}u.username {$like} ?s ESCAPE '#'";
+                    $sptr   = ' OR ';
+                }
+
+                $arr  = $this->variants($user->username_normal);
+
+                foreach ($arr as $value) {
+                    $vars[] = $value;
+                    $query .= "{$sptr}u.username_normal {$like} ?s ESCAPE '#'";
+                    $sptr   = ' OR ';
+                }
+
+                $query .= ')';
+
+                $nameL = \mb_strtolower($user->username, 'UTF-8');
+                $nameU = \mb_strtoupper($user->username, 'UTF-8');
+                $normL = \mb_strtolower($user->username_normal, 'UTF-8');
+                $normU = \mb_strtoupper($user->username_normal, 'UTF-8');
+
+                $stmt = $this->c->DB->query($query, $vars);
+
+                while (false !== ($row = $stmt->fetch())) {
+                    if (
+                        \mb_strtolower($row['username'], 'UTF-8') === $nameL
+                        || \mb_strtoupper($row['username'], 'UTF-8') === $nameU
+                        || \mb_strtolower($row['username_normal'], 'UTF-8') === $normL
+                        || \mb_strtoupper($row['username_normal'], 'UTF-8') === $normU
+                    ) {
+                        $stmt->closeCursor();
+
+                        return false;
+                    }
+                }
+
+                return true;
+        }
+
         $vars = [
         $vars = [
             ':id'    => (int) $user->id,
             ':id'    => (int) $user->id,
-            ':name'  => $user->username,
-            ':norm'  => $user->username_normal,
-            ':normL' => $this->manager->normUsername(\mb_strtolower($user->username, 'UTF-8')), // ????
-            ':normU' => $this->manager->normUsername(\mb_strtoupper($user->username, 'UTF-8')), // ????
+            ':name'  => \str_replace(['#', '_', '%'], ['##', '#_', '#%'], $user->username),
+            ':norm'  => \str_replace(['#', '_', '%'], ['##', '#_', '#%'], $user->username_normal),
         ];
         ];
-        $query = 'SELECT 1
+        $query = "SELECT 1
             FROM ::users AS u
             FROM ::users AS u
             WHERE u.id!=?i:id
             WHERE u.id!=?i:id
                 AND (
                 AND (
-                    LOWER(u.username)=LOWER(?s:name)
-                    OR u.username_normal=?s:norm
-                    OR LOWER(u.username_normal)=?s:normL
-                    OR UPPER(u.username_normal)=?s:normU
-                )';
+                    u.username {$like} ?s:name ESCAPE '#'
+                    OR u.username_normal {$like} ?s:norm ESCAPE '#'
+                )";
 
 
         $result = $this->c->DB->query($query, $vars)->fetchAll();
         $result = $this->c->DB->query($query, $vars)->fetchAll();