瀏覽代碼

OAuth refactoring

The registration functionality has been moved from RegLog to the Register page.
Added a check for agreeing to the rules for the first login.
Added ability to edit name when registering via OAuth.
Visman 2 年之前
父節點
當前提交
604e0d2a13
共有 5 個文件被更改,包括 288 次插入175 次删除
  1. 28 110
      app/Models/Pages/RegLog.php
  2. 81 0
      app/Models/Pages/RegLogTrait.php
  3. 173 65
      app/Models/Pages/Register.php
  4. 3 0
      app/lang/en/admin_providers.po
  5. 3 0
      app/lang/ru/admin_providers.po

+ 28 - 110
app/Models/Pages/RegLog.php

@@ -12,12 +12,15 @@ namespace ForkBB\Models\Pages;
 
 use ForkBB\Core\Image;
 use ForkBB\Models\Page;
+use ForkBB\Models\Pages\RegLogTrait;
 use ForkBB\Models\Provider\Driver;
 use ForkBB\Models\User\User;
 use function \ForkBB\__;
 
 class RegLog extends Page
 {
+    use RegLogTrait;
+
     const TIMEOUT = 5;
 
     /**
@@ -110,8 +113,8 @@ class RegLog extends Page
     protected function byGuest(Driver $provider, int $uid): Page
     {
         switch ($provider->stateType) {
-            case 'reg':
             case 'auth':
+            case 'reg':
                 return $this->authOrReg($provider, $uid);
             default:
                 return $this->c->Message->message('Bad request');
@@ -166,127 +169,42 @@ class RegLog extends Page
      */
     protected function authOrReg(Driver $provider, int $uid): Page
     {
-        // регистрация
-        if (empty($uid)) {
-            // на форуме есть пользователь с таким email
-            if (
-                $this->c->providerUser->findByEmail($provider->userEmail) > 0
-                || $this->c->users->loadByEmail($provider->userEmail) instanceof User
-            ) {
-                $auth         = $this->c->Auth;
-                $auth->fIswev = [FORK_MESS_INFO, ['Email message', __($provider->name)]];
-
-                return $auth->forget([], 'GET', $provider->userEmail);
-            }
-
-            if (1 !== $this->c->config->b_regs_allow) {
-                return $this->c->Message->message('No new regs');
-            }
-
-            $user = $this->c->users->create();
-
-            $user->username        = $this->nameGenerator($provider);
-            $user->password        = 'oauth_' . $this->c->Secury->randomPass(7);
-            $user->group_id        = $this->c->config->i_default_user_group;
-            $user->email           = $provider->userEmail;
-            $user->email_confirmed = $provider->userEmailVerifed ? 1 : 0;
-            $user->activate_string = '';
-            $user->u_mark_all_read = \time();
-            $user->email_setting   = $this->c->config->i_default_email_setting;
-            $user->timezone        = $this->c->config->o_default_timezone;
-            $user->language        = $this->user->language;
-            $user->style           = $this->user->style;
-            $user->registered      = \time();
-            $user->registration_ip = $this->user->ip;
-            $user->ip_check_type   = 0;
-            $user->signature       = '';
-            $user->location        = $provider->userLocation;
-            $user->url             = $provider->userURL;
-
-            if ($provider->userAvatar) {
-                $image = $this->c->Files->uploadFromLink($provider->userAvatar);
-
-                if ($image instanceof Image) {
-                    $name   = $this->c->Secury->randomPass(8);
-                    $path   = $this->c->DIR_PUBLIC . "{$this->c->config->o_avatars_dir}/{$name}.(webp|jpg|png|gif)";
-                    $result = $image
-                        ->rename(true)
-                        ->rewrite(false)
-                        ->resize($this->c->config->i_avatars_width, $this->c->config->i_avatars_height)
-                        ->toFile($path, $this->c->config->i_avatars_size);
-
-                    if (true === $result) {
-                        $user->avatar = $image->name() . '.' . $image->ext();
-                    } else {
-                        $this->c->Log->warning('OAuth Failed image processing', [
-                            'user'  => $user->fLog(),
-                            'error' => $image->error(),
-                        ]);
-                    }
-                } else {
-                    $this->c->Log->warning('OAuth Avatar not image', [
-                        'user'  => $user->fLog(),
-                        'error' => $this->c->Files->error(),
-                    ]);
-                }
-            }
-
-            $this->c->users->insert($user);
-
-            if (true !== $this->c->providerUser->registration($user, $provider)) {
-                throw new RuntimeException('Failed to insert data'); // ??????????????????????????????????????????
-            }
-
-            $this->c->Log->info('OAuth Reg: ok', [
-                'user'     => $user->fLog(),
-                'provider' => $provider->name,
-                'userInfo' => $provider->userInfo,
-                'headers'  => true,
-            ]);
-
-        } else {
+        // вход
+        if ($uid > 0) {
             $user = $this->c->users->load($uid);
-        }
 
-        // вход
-        return $this->c->Auth->login([], 'POST', '', $user);
-    }
+            return $this->c->Auth->login([], 'POST', '', $user);
+        }
 
-    /**
-     * Подбирает уникальное имя для регистрации пользователя
-     */
-    protected function nameGenerator(Driver $provider): string
-    {
-        $names = [];
+        // на форуме есть пользователь с таким email
+        if (
+            $this->c->providerUser->findByEmail($provider->userEmail) > 0
+            || $this->c->users->loadByEmail($provider->userEmail) instanceof User
+        ) {
+            $auth         = $this->c->Auth;
+            $auth->fIswev = [FORK_MESS_INFO, ['Email message', __($provider->name)]];
 
-        if ('' != $provider->userName) {
-            $names[] = $provider->userName;
+            return $auth->forget([], 'GET', $provider->userEmail);
         }
 
-        if ('' != $provider->userLogin) {
-            $names[] = $provider->userLogin;
+        // регистрация закрыта
+        if (1 !== $this->c->config->b_regs_allow) {
+            return $this->c->Message->message('No new regs');
         }
 
-        if ('' != ($tmp = (string) \strstr($provider->userEmail, '@', true))) {
-            $names[] = $tmp;
-        }
+        // продолжение регистрации начиная с согласия с правилами
+        if ('reg' !== $provider->stateType) {
+            $page = $this->c->Rules->confirmation();
+            $form = $page->form;
 
-        $names[] = 'user' . \time();
-        $v       = $this->c->Validator->reset()->addRules(['name' => 'required|string:trim|username|noURL:1']);
-        $end     = '';
-        $i       = 0;
+            $form['hidden']['oauth'] = $this->providerToString($provider);
 
-        while ($i < 100) {
-            foreach ($names as $name) {
-                if ($v->validation(['name' => $name . $end])) {
-                    return $v->name;
-                }
-            }
+            $page->form   = $form;
+            $page->fIswev = [FORK_MESS_INFO, 'First time Register?'];
 
-            $end = '_' . $this->c->Secury->randomHash(4);
-            ++$i;
+            return $page;
         }
 
-        throw new RuntimeException('Failed to generate unique username');
+        return $this->c->Register->reg([], 'POST', $provider);
     }
 }

+ 81 - 0
app/Models/Pages/RegLogTrait.php

@@ -11,10 +11,14 @@ declare(strict_types=1);
 namespace ForkBB\Models\Pages;
 
 use ForkBB\Models\Model;
+use ForkBB\Models\Provider\Driver;
 use function \ForkBB\__;
 
 trait RegLogTrait
 {
+    /**
+     * Подготавливает массив данных для формы
+     */
     protected function reglogForm(string $type): array
     {
         if (
@@ -61,4 +65,81 @@ trait RegLogTrait
             'btns'   => $btns,
         ];
     }
+
+    /**
+     * Кодирует данные провайдера(пользователя) в строку
+     */
+    protected function providerToString(Driver $provider): string
+    {
+        $data = [
+            'name'     => $provider->name,
+            'userInfo' => $provider->userInfo,
+        ];
+
+        $data = \base64_encode(\json_encode($data, FORK_JSON_ENCODE));
+        $hash = $this->c->Secury->hash($data);
+
+        return "{$data}:{$hash}";
+    }
+
+    /**
+     * Раскодирует данные провайдера(пользователя) из строку или false
+     */
+    protected function stringToProvider(string $data): Driver|false
+    {
+        $data = \explode(':', $data);
+
+        if (2 !== \count($data)) {
+            return false;
+        }
+
+        if (
+            ! \hash_equals($data[1], $this->c->Secury->hash($data[0]))
+            || ! \is_array($data = \json_decode(\base64_decode($data[0], true), true))
+        ) {
+            return false;
+        }
+
+        $provider           = $this->c->providers->init()->get($data['name']);
+        $provider->userInfo = $data['userInfo'];
+
+        return $provider;
+    }
+
+    /**
+     * Подбирает уникальное имя для регистрации пользователя
+     */
+    protected function nameGenerator(Driver $provider): string
+    {
+        $names = [];
+
+        if ('' != $provider->userName) {
+            $names[] = $provider->userName;
+        }
+
+        if ('' != $provider->userLogin) {
+            $names[] = $provider->userLogin;
+        }
+
+        if ('' != ($tmp = (string) \strstr($provider->userEmail, '@', true))) {
+            $names[] = $tmp;
+        }
+
+        $v    = (clone $this->c->Validator)->reset()->addRules(['name' => 'required|string:trim|username|noURL:1']);
+        $end  = '';
+        $i    = 0;
+
+        while ($i < 3) {
+            foreach ($names as $name) {
+                if ($v->validation(['name' => $name . $end])) {
+                    return $v->name;
+                }
+            }
+
+            $end = '_' . $this->c->Secury->randomHash(4);
+            ++$i;
+        }
+
+        return 'user' . \time();
+    }
 }

+ 173 - 65
app/Models/Pages/Register.php

@@ -10,10 +10,12 @@ declare(strict_types=1);
 
 namespace ForkBB\Models\Pages;
 
+use ForkBB\Core\Image;
 use ForkBB\Core\Validator;
 use ForkBB\Core\Exceptions\MailException;
 use ForkBB\Models\Page;
 use ForkBB\Models\Pages\RegLogTrait;
+use ForkBB\Models\Provider\Driver;
 use ForkBB\Models\User\User;
 use function \ForkBB\__;
 
@@ -21,25 +23,65 @@ class Register extends Page
 {
     use RegLogTrait;
 
+    /**
+     * Флаг входа с помощью OAuth
+     */
+    protected bool $useOAuth = false;
+
     /**
      * Регистрация
      */
-    public function reg(): Page
+    public function reg(array $args, string $method, Driver $provider = null): Page
     {
         $this->c->Lang->load('validator');
         $this->c->Lang->load('register');
 
+        // регистрация через OAuth
+        if (null !== $provider) {
+            $this->provider = $provider;
+
+            $_POST = [
+                'token'    => $this->c->Csrf->create('RegisterForm'),
+                'agree'    => $this->c->Csrf->create('Register'),
+                'oauth'    => $this->providerToString($provider),
+                'register' => 'Register with OAuth',
+            ];
+        // переход от Rules/завершение регистрации через OAuth
+        } else {
+            $v = $this->c->Validator->reset()->addRules(['oauth' => 'string']);
+
+            if (
+                ! $v->validation($_POST)
+                || (
+                    null !== $v->oauth
+                    && ! ($this->provider = $this->stringToProvider($v->oauth)) instanceof Driver
+                )
+            ) {
+                return $this->c->Message->message('Bad request');
+            }
+        }
+
+        $this->useOAuth = $this->provider instanceof Driver;
+
+        $rules = [
+            'token'    => 'token:RegisterForm',
+            'agree'    => 'required|token:Register',
+            'on'       => 'integer',
+            'oauth'    => 'string',
+            'email'    => 'required_with:on|string:trim|email:noban',
+            'username' => 'required_with:on|string:trim|username|noURL:1',
+            'password' => 'required_with:on|string|min:16|max:100000|password',
+            'register' => 'required|string',
+        ];
+
+        if ($this->useOAuth) {
+            unset($rules['email'], $rules['password']);
+        }
+
         $v = $this->c->Validator->reset()
-            ->addValidators([
-            ])->addRules([
-                'token'    => 'token:RegisterForm',
-                'agree'    => 'required|token:Register',
-                'on'       => 'integer',
-                'email'    => 'required_with:on|string:trim|email:noban',
-                'username' => 'required_with:on|string:trim|username|noURL:1',
-                'password' => 'required_with:on|string|min:16|max:100000|password',
-                'register' => 'required|string',
-            ])->addAliases([
+            ->addValidators([])
+            ->addRules($rules)
+            ->addAliases([
                 'email'    => 'Email',
                 'username' => 'Username',
                 'password' => 'Passphrase',
@@ -55,23 +97,24 @@ class Register extends Page
         if ($v->validation($_POST, true)) {
             // завершение регистрации
             if (1 === $v->on) {
-                $userInDB = $this->c->users->loadByEmail($v->email);
+                $email    = $this->useOAuth ? $this->provider->userEmail : $v->email;
+                $userInDB = $this->c->users->loadByEmail($email);
 
                 if ($userInDB instanceof User) {
-                    return $this->regDupe($v, $userInDB);
+                    return $this->regDupe($v, $userInDB, $email);
                 }
 
-                $id = $this->c->providerUser->findByEmail($v->email);
+                $id = $this->c->providerUser->findByEmail($email);
 
                 if ($id > 0) {
                     $userInDB = $this->c->users->load($id);
 
                     if ($userInDB instanceof User) {
-                        return $this->regDupe($v, $userInDB);
+                        return $this->regDupe($v, $userInDB, $email);
                     }
                 }
 
-                return $this->regEnd($v);
+                return $this->regEnd($v, $email);
             }
         } else {
             $this->fIswev = $v->getErrors();
@@ -98,7 +141,7 @@ class Register extends Page
         $this->titles       = 'Register';
         $this->robots       = 'noindex';
         $this->form         = $this->formReg($v);
-        $this->formOAuth    = $this->reglogForm('reg');
+        $this->formOAuth    = $this->useOAuth ? null : $this->reglogForm('reg');
 
         return $this;
     }
@@ -108,49 +151,14 @@ class Register extends Page
      */
     protected function formReg(Validator $v): array
     {
-        return [
+        $form = [
             'action' => $this->c->Router->link('RegisterForm'),
             'hidden' => [
                 'token' => $this->c->Csrf->create('RegisterForm'),
                 'agree' => $v->agree,
                 'on'    => '1',
             ],
-            'sets'   => [
-                'reg' => [
-                    'fields' => [
-                        'email' => [
-                            'autofocus'      => true,
-                            'class'          => ['hint'],
-                            'type'           => 'text',
-                            'maxlength'      => (string) $this->c->MAX_EMAIL_LENGTH,
-                            'value'          => $v->email,
-                            'caption'        => 'Email',
-                            'help'           => 1 === $this->c->config->b_regs_verify ? 'Email help2' : 'Email help',
-                            'required'       => true,
-                            'pattern'        => '.+@.+',
-                            'autocapitalize' => 'off',
-                        ],
-                        'username' => [
-                            'class'     => ['hint'],
-                            'type'      => 'text',
-                            'maxlength' => '25',
-                            'value'     => $v->username,
-                            'caption'   => 'Username',
-                            'help'      => 'Login format',
-                            'required'  => true,
-                            'pattern'   => '^.{2,25}$',
-                        ],
-                        'password' => [
-                            'class'     => ['hint'],
-                            'type'      => 'password',
-                            'caption'   => 'Passphrase',
-                            'help'      => 'Passphrase help',
-                            'required'  => true,
-                            'pattern'   => '^.{16,}$',
-                        ],
-                    ],
-                ],
-            ],
+            'sets'   => [],
             'btns'   => [
                 'register' => [
                     'type'  => 'submit',
@@ -158,14 +166,64 @@ class Register extends Page
                 ],
             ],
         ];
+
+        $fields = [];
+
+        if (! $this->useOAuth) {
+            $fields['email'] = [
+                'autofocus'      => true,
+                'class'          => ['hint'],
+                'type'           => 'text',
+                'maxlength'      => (string) $this->c->MAX_EMAIL_LENGTH,
+                'value'          => $v->email,
+                'caption'        => 'Email',
+                'help'           => 1 === $this->c->config->b_regs_verify ? 'Email help2' : 'Email help',
+                'required'       => true,
+                'pattern'        => '.+@.+',
+                'autocapitalize' => 'off',
+            ];
+        }
+
+        $fields['username'] = [
+            'class'     => ['hint'],
+            'type'      => 'text',
+            'maxlength' => '25',
+            'value'     => $v->username ?? ($this->useOAuth ? $this->nameGenerator($this->provider) : ''),
+            'caption'   => 'Username',
+            'help'      => 'Login format',
+            'required'  => true,
+            'pattern'   => '^.{2,25}$',
+        ];
+
+        if (! $this->useOAuth) {
+            $fields['password'] = [
+                'class'     => ['hint'],
+                'type'      => 'password',
+                'caption'   => 'Passphrase',
+                'help'      => 'Passphrase help',
+                'required'  => true,
+                'pattern'   => '^.{16,}$',
+            ];
+        }
+
+        $form['sets']['reg']['fields'] = $fields;
+
+        if ($this->useOAuth) {
+            $form['hidden']['oauth'] = $v->oauth;
+        }
+
+        return $form;
     }
 
     /**
      * Завершение регистрации
      */
-    protected function regEnd(Validator $v): Page
+    protected function regEnd(Validator $v, string $email): Page
     {
-        if (1 === $this->c->config->b_regs_verify) {
+        if (
+            ! $this->useOAuth
+            && 1 === $this->c->config->b_regs_verify
+        ) {
             $groupId = FORK_GROUP_UNVERIFIED;
             $key     = $this->c->Secury->randomPass(31);
         } else {
@@ -176,10 +234,10 @@ class Register extends Page
         $user = $this->c->users->create();
 
         $user->username        = $v->username;
-        $user->password        = \password_hash($v->password, \PASSWORD_DEFAULT);
+        $user->password        = $this->useOAuth ? 'oauth_' . $this->c->Secury->randomPass(7) : \password_hash($v->password, \PASSWORD_DEFAULT);
         $user->group_id        = $groupId;
-        $user->email           = $v->email;
-        $user->email_confirmed = 0;
+        $user->email           = $email;
+        $user->email_confirmed = $this->useOAuth && $this->provider->userEmailVerifed ? 1 : 0;
         $user->activate_string = $key;
         $user->u_mark_all_read = \time();
         $user->email_setting   = $this->c->config->i_default_email_setting;
@@ -189,9 +247,50 @@ class Register extends Page
         $user->registered      = \time();
         $user->registration_ip = $this->user->ip;
         $user->signature       = '';
+        $user->ip_check_type   = 0;
+        $user->location        = $this->useOAuth ? $this->provider->userLocation : '';
+        $user->url             = $this->useOAuth ? $this->provider->userURL : '';
+
+        if (
+            $this->useOAuth
+            && $this->provider->userAvatar
+        ) {
+            $image = $this->c->Files->uploadFromLink($this->provider->userAvatar);
+
+            if ($image instanceof Image) {
+                $name   = $this->c->Secury->randomPass(8);
+                $path   = $this->c->DIR_PUBLIC . "{$this->c->config->o_avatars_dir}/{$name}.(webp|jpg|png|gif)";
+                $result = $image
+                    ->rename(true)
+                    ->rewrite(false)
+                    ->resize($this->c->config->i_avatars_width, $this->c->config->i_avatars_height)
+                    ->toFile($path, $this->c->config->i_avatars_size);
+
+                if (true === $result) {
+                    $user->avatar = $image->name() . '.' . $image->ext();
+                } else {
+                    $this->c->Log->warning('OAuth Failed image processing', [
+                        'user'  => $user->fLog(),
+                        'error' => $image->error(),
+                    ]);
+                }
+            } else {
+                $this->c->Log->warning('OAuth Avatar not image', [
+                    'user'  => $user->fLog(),
+                    'error' => $this->c->Files->error(),
+                ]);
+            }
+        }
 
         $newUserId = $this->c->users->insert($user);
 
+        if (
+            $this->useOAuth
+            && true !== $this->c->providerUser->registration($user, $this->provider)
+        ) {
+            throw new RuntimeException('Failed to insert data'); // ??????????????????????????????????????????
+        }
+
         $this->c->Log->info('Registriaton: ok', [
             'user'    => $user->fLog(),
             'form'    => $v->getData(false, ['token', 'agree', 'password']),
@@ -240,7 +339,10 @@ class Register extends Page
         $this->c->Lang->load('register');
 
         // отправка письма активации аккаунта
-        if (1 === $this->c->config->b_regs_verify) {
+        if (
+            ! $this->useOAuth
+            && 1 === $this->c->config->b_regs_verify
+        ) {
             $this->c->Csrf->setHashExpiration(259200); // ???? хэш действует 72 часа
 
             $link = $this->c->Router->link(
@@ -264,7 +366,7 @@ class Register extends Page
                     ->setMaxRecipients(1)
                     ->setFolder($this->c->DIR_LANG)
                     ->setLanguage($this->user->language)
-                    ->setTo($v->email)
+                    ->setTo($email)
                     ->setFrom($this->c->config->o_webmaster_email, $tplData['fMailer'])
                     ->setTpl('welcome.tpl', $tplData)
                     ->send();
@@ -286,21 +388,24 @@ class Register extends Page
                 $auth         = $this->c->Auth;
                 $auth->fIswev = [FORK_MESS_WARN, ['Error welcom mail', $this->c->config->o_admin_email]];
 
-                return $auth->forget([], 'GET', $v->email);
+                return $auth->forget([], 'GET', $email);
             }
         // форма логина
         } else {
+            return $this->c->Auth->login([], 'POST', '', $user);
+/*
             $auth         = $this->c->Auth;
             $auth->fIswev = [FORK_MESS_SUCC, 'Reg complete'];
 
             return $auth->login([], 'GET', $v->username);
+*/
         }
     }
 
     /**
      * Делает вид, что пользователь зарегистрирован (для предотвращения утечки email)
      */
-    protected function regDupe(Validator $v, User $userInDB): Page
+    protected function regDupe(Validator $v, User $userInDB, string $email): Page
     {
         $this->c->Log->warning('Registriaton: dupe', [
             'user'     => $this->user->fLog(), // ????
@@ -346,7 +451,10 @@ class Register extends Page
         $this->c->Lang->load('register');
 
         // фейк отправки письма активации аккаунта
-        if (1 === $this->c->config->b_regs_verify) {
+        if (
+            ! $this->useOAuth
+            && 1 === $this->c->config->b_regs_verify
+        ) {
             $isSent = true;
 
             // письмо активации аккаунта отправлено
@@ -357,7 +465,7 @@ class Register extends Page
                 $auth         = $this->c->Auth;
                 $auth->fIswev = [FORK_MESS_WARN, ['Error welcom mail', $this->c->config->o_admin_email]];
 
-                return $auth->forget([], 'GET', $v->email);
+                return $auth->forget([], 'GET', $email);
             }
         // форма логина
         } else {

+ 3 - 0
app/lang/en/admin_providers.po

@@ -119,3 +119,6 @@ msgstr "Email verified"
 
 msgid "Account removed"
 msgstr "Account removed."
+
+msgid "First time Register?"
+msgstr "You are here for the first time. Register?"

+ 3 - 0
app/lang/ru/admin_providers.po

@@ -119,3 +119,6 @@ msgstr "Адрес проверен"
 
 msgid "Account removed"
 msgstr "Аккаунт удален."
+
+msgid "First time Register?"
+msgstr "Вы здесь впервые. Зарегистрируетесь?"