core_auth   F
last analyzed

Complexity

Total Complexity 120

Size/Duplication

Total Lines 833
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 311
dl 0
loc 833
rs 2
c 0
b 0
f 0
ccs 0
cts 369
cp 0
wmc 120

19 Methods

Rating   Name   Duplication   Size   Complexity  
B player_register_model() 0 45 11
A auth_reset() 0 9 1
F login() 0 109 26
A password_check() 0 15 5
A password_salt_generate() 0 4 1
A get_accessible_user_list() 0 17 3
A cookie_set() 0 2 3
A logout() 0 17 6
B get_active_user() 0 29 8
A make_random_password() 0 2 1
C register_player_db_create() 0 55 12
A password_encode() 0 2 1
A password_change() 0 43 6
A impersonate() 0 29 5
A __construct() 0 13 1
A player_register_view() 0 31 5
B flog() 0 18 10
A register_player_name_validate() 0 21 5
B make_return_array() 0 68 10

How to fix   Complexity   

Complex Class

Complex classes like core_auth often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use core_auth, and based on these observations, apply Extract Interface, too.

1
<?php
2
/** @noinspection PhpUnnecessaryCurlyVarSyntaxInspection */
3
/** @noinspection PhpDeprecationInspection */
4
5
// define("DEBUG_AUTH", true);
6
use DBAL\db_mysql;
7
use Modules\sn_module;
8
9
/**
10
 * Статический над-класс, который обеспечивает интерфейс авторизации для остального кода
11
 *
12
 * User: Gorlum
13
 * Date: 21.04.2015
14
 * Time: 3:51
15
 *
16
 * version #46a151#
17
 */
18
class core_auth extends sn_module {
19
  public $versionCommitted = '#46a151#';
20
21
  public $manifest = [
22
    'package'   => 'core',
23
    'name'      => 'auth',
24
    'version'   => '0a0',
25
    'copyright' => 'Project "SuperNova.WS" #46a151# copyright © 2009-2018 Gorlum',
26
27
    self::M_LOAD_ORDER => MODULE_LOAD_ORDER_CORE_AUTH,
28
29
    'mvc' => [
30
      'pages' => [
31
        'player_register' => 'classes/core_auth'
32
      ],
33
34
      'model' => [
35
        'player_register' => [
36
          /** @see core_auth::player_register_model() */
37
          'callable' => 'player_register_model',
38
        ],
39
      ],
40
41
      'view' => [
42
        'player_register' => [
43
          /** @see core_auth::player_register_view() */
44
          'callable' => 'player_register_view',
45
        ],
46
      ],
47
    ],
48
  ];
49
50
  /**
51
   * БД из которой читать данные
52
   *
53
   * @var db_mysql $db
54
   */
55
  static $db;
56
  /**
57
   * Информация об устройстве
58
   *
59
   * @var RequestInfo
60
   */
61
  static $device;
62
  /**
63
   * Аккаунт ????
64
   *
65
   * @var Account
66
   */
67
  public $account = null;
68
  /**
69
   * Запись текущего игрока из `users`
70
   *
71
   * @var null
72
   */
73
  static $user = null;
74
75
  /**
76
   * Основной провайдер
77
   *
78
   * @var auth_local
79
   */
80
  public static $main_provider = null;
81
82
  /**
83
   * Статус инициализации
84
   *
85
   * @var bool
86
   */
87
  // protected static $is_init = false;
88
  /**
89
   * Список провайдеров
90
   *
91
   * @var auth_local[]
92
   */
93
  protected $providers = array();
94
95
  /**
96
   * Глобальный статус входа в игру
97
   *
98
   * @var int
99
   */
100
  static $login_status = LOGIN_UNDEFINED;
101
  static $login_message = '';
102
103
  /**
104
   * Имя, предлагаемое пользователю в качестве имени игрока
105
   *
106
   * @var string
107
   */
108
  protected $player_suggested_name = '';
109
110
  /**
111
   * Список полностью авторизированных аккаунтов с LOGIN_SUCCESS
112
   *
113
   * @var auth_local[]
114
   */
115
  protected $providers_authorised = array();
116
  /** @var auth_local[] $provider_error_list Статусы всех провайдеров */
117
  protected $provider_error_list = array();
118
//  /** @var string[] */
119
//  protected $provider_error_messages = array();
120
  /** @var array $accessible_user_row_list Список юзеров (user_row - записей из `user`), доступных всем авторизированным аккаунтам */
121
  protected $accessible_user_row_list = array();
122
123
  protected $user_id_to_provider = array();
124
125
  /** @var bool $is_impersonating Флаг имперсонации */
126
  protected $is_impersonating = false;
127
  protected $impersonator_username = '';
128
129
  /** @var bool $is_player_register Флаг регистрации пользователя */
130
  protected $is_player_register = false;
131
  /** @var int $register_status */
132
  protected $register_status = LOGIN_UNDEFINED;
133
134
  /**
135
   * Максимальный локальный уровень авторизации игрока
136
   *
137
   * @var int
138
   */
139
  public $auth_level_max_local = AUTH_LEVEL_ANONYMOUS;
140
141
  /**
142
   * @var int
143
   */
144
  public $partner_id = 0;
145
146
  /**
147
   * @var string
148
   */
149
  protected $server_name = '';
150
151
  /**
152
   * @param string $filename
153
   *
154
   * @throws Exception
155
   */
156
  // TODO - OK 4.7
157
  public function __construct($filename = __FILE__) {
158
    parent::__construct($filename);
159
160
    // В этой точке все модули уже прогружены и инициализированы по 1 экземпляру
161
    self::$db = SN::$db;
162
163
    self::$device             = new RequestInfo();
164
    $this->is_player_register = (bool)sys_get_param('player_register');
165
    $this->partner_id         = sys_get_param_int('id_ref', sys_get_param_int('partner_id'));
166
    $this->server_name        = sys_get_param_str_unsafe('server_name', SN_ROOT_VIRTUAL);
167
168
    self::$main_provider = new auth_local();
169
    SN::$gc->modules->registerModule(core_auth::$main_provider->manifest['name'], core_auth::$main_provider);
170
  }
171
172
  // TODO - OK v4.7
173
  public function player_register_model() {
174
    // TODO ВСЕГДА ПРЕДЛАГАТЬ РЕГАТЬ ИГРОКА ИЛИ ПОДКЛЮЧИТЬ ИМЕЮЩЕГОСЯ!
175
176
    // TODO в auth_local делать проверку БД на существование имени игрока в локальной БД - что бы избежать лишнего шага (см.выше)
177
    // TODO Хотя тут может получиться вечный цикл - ПОДУМАТЬ
178
    // TODO Тут же можно пробовать провести попытку слияния аккаунтов - хотя это и очень небезопасно
179
180
    if (sys_get_param('login_player_register_logout')) {
181
      $this->logout();
182
    }
183
184
    $original_suggest = '';
185
    // Смотрим - есть ли у нас данные от пользователя
186
    if (($player_name_submitted = sys_get_param('submit_player_name'))) {
187
      // Попытка регистрации нового игрока из данных, введенных пользователем
188
      $this->player_suggested_name = sys_get_param_str_unsafe('player_suggested_name');
189
    } else {
190
      foreach ($this->providers_authorised as $provider) {
191
        if ($this->player_suggested_name = $provider->player_name_suggest()) { // OK 4.5
192
          $original_suggest = $provider->player_name_suggest();
193
          break;
194
        }
195
      }
196
    }
197
198
    // Если у нас провайдеры не дают имени и пользователь не дал свой вариант - это у нас первый логин в игру
199
    if (!$this->player_suggested_name) {
200
      $max_user_id = db_player_get_max_id(); // 4.5
201
      // TODO - предлагать имя игрока по локали
202
203
      // Проверить наличие такого имени в истории имён
204
      do {
205
        db_mysql::db_transaction_rollback();
206
        $this->player_suggested_name = 'Emperor ' . mt_rand($max_user_id + 1, $max_user_id + 1000);
207
        db_mysql::db_transaction_start();
208
      } while (db_player_name_exists($this->player_suggested_name));
209
210
    }
211
212
    if ($player_name_submitted) {
213
      $this->register_player_db_create($this->player_suggested_name); // OK 4.5
214
      if ($this->register_status == LOGIN_SUCCESS) {
215
        sys_redirect(SN_ROOT_VIRTUAL . 'overview.php');
216
      } /** @noinspection PhpStatementHasEmptyBodyInspection */
217
      elseif ($this->register_status == REGISTER_ERROR_PLAYER_NAME_EXISTS && $original_suggest == $this->player_suggested_name) {
218
        // self::$player_suggested_name .= ' ' . $this->account->account_id;
219
      }
220
//      if(self::$login_status != LOGIN_SUCCESS) {
221
//        // TODO Ошибка при регистрации нового игрока под текущим именем
222
//      }
223
    }
224
225
  }
226
227
  // TODO - OK v4.7
228
  public function player_register_view($template = null) {
229
    global $template_result, $lang;
230
231
    define('LOGIN_LOGOUT', true);
232
233
    $template_result[F_PLAYER_REGISTER_MESSAGE] =
234
      isset($template_result[F_PLAYER_REGISTER_MESSAGE]) && $template_result[F_PLAYER_REGISTER_MESSAGE]
235
        ? $template_result[F_PLAYER_REGISTER_MESSAGE]
236
        : ($this->register_status != LOGIN_UNDEFINED
237
        ? $lang['sys_login_messages'][$this->register_status]
238
        : false
239
      );
240
241
    if ($this->register_status == LOGIN_ERROR_USERNAME_RESTRICTED_CHARACTERS) {
242
      $prohibited_characters                      = array_map(function ($value) {
243
        return "'" . htmlentities($value, ENT_QUOTES, 'UTF-8') . "'";
244
      }, str_split(LOGIN_REGISTER_CHARACTERS_PROHIBITED));
0 ignored issues
show
Bug introduced by
It seems like str_split(LOGIN_REGISTER_CHARACTERS_PROHIBITED) can also be of type true; however, parameter $array of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

244
      }, /** @scrutinizer ignore-type */ str_split(LOGIN_REGISTER_CHARACTERS_PROHIBITED));
Loading history...
245
      $template_result[F_PLAYER_REGISTER_MESSAGE] .= implode(', ', $prohibited_characters);
246
    }
247
248
    $template_result = array_merge($template_result, array(
249
      'NAVBAR'                  => false,
250
      'PLAYER_SUGGESTED_NAME'   => sys_safe_output($this->player_suggested_name),
251
      'PARTNER_ID'              => sys_safe_output($this->partner_id),
252
      'SERVER_NAME'             => sys_safe_output($this->server_name),
253
      'PLAYER_REGISTER_STATUS'  => $this->register_status,
254
      'PLAYER_REGISTER_MESSAGE' => $template_result[F_PLAYER_REGISTER_MESSAGE],
255
      'LOGIN_UNDEFINED'         => LOGIN_UNDEFINED,
256
    ));
257
258
    return SnTemplate::gettemplate('login_player_register', $template);
259
  }
260
261
  /**
262
   * Функция пытается залогиниться по всем известным провайдерам
263
   *
264
   * @return array|void
265
   */
266
  // TODO - OK v4.5
267
  public function login() {
268
    global $lang;
269
270
    // !self::$is_init ? self::init() : false;
271
272
    if (!SN::$gc->modules->countModulesInGroup('auth')) {
273
      die('{Не обнаружено ни одного провайдера авторизации в core_auth::login()!}');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
274
    }
275
276
    if (!empty($_POST)) {
277
      self::flog(dump($_POST, '$_POST'));
278
    }
279
    if (!empty($_GET)) {
280
      self::flog(dump($_GET, '$_GET'));
281
    }
282
    if (!empty($_COOKIE)) {
283
      self::flog(dump($_COOKIE, '$_COOKIE'));
284
    }
285
286
    $this->auth_reset(); // OK v4.5
287
288
    $this->providers = array();
289
    foreach (SN::$gc->modules->getModulesInGroup('auth', true) as $module) {
290
      /** @var auth_abstract $module */
291
      $this->providers[$module->provider_id] = $module;
292
    }
293
294
    // pdump($this->providers);
295
    foreach ($this->providers as $provider_id => $provider) {
296
      $login_status = $provider->login(); // OK v4.5
297
      self::flog(($provider->manifest['name'] . '->' . 'login_try - ') . (empty($provider->account->account_id) ? $lang['sys_login_messages'][$provider->account_login_status] : dump($provider)));
298
      /** @noinspection PhpConditionCheckedByNextConditionInspection */
299
      if ($login_status == LOGIN_SUCCESS && is_object($provider->account) && $provider->account instanceof Account && $provider->account->account_id) {
300
        $this->providers_authorised[$provider_id] = &$this->providers[$provider_id];
301
302
        $this->user_id_to_provider = array_replace_recursive(
303
          $this->user_id_to_provider,
304
          // static::db_translate_get_users_from_account_list($provider_id, $provider->account->account_id) // OK 4.5
305
          PlayerToAccountTranslate::db_translate_get_users_from_account_list($provider_id, $provider->account->account_id) // OK 4.5
306
        );
307
      } elseif ($login_status != LOGIN_UNDEFINED) {
308
        $this->provider_error_list[$provider_id] = $login_status;
309
      }
310
    }
311
312
    if (empty($this->providers_authorised)) {
313
      // Ни один аккаунт не авторизирован
314
      // Проверяем - есть ли у нас ошибки в аккаунтах?
315
      if (!empty($this->provider_error_list)) {
316
        // Если есть - выводим их
317
        self::$login_status = reset($this->provider_error_list);
318
        $providerError      = $this->providers[key($this->provider_error_list)]->account_login_message;
319
320
        if (!empty($providerError)) {
321
          self::$login_message = $providerError;
322
        }
323
      }
324
      // Иначе - это первый запуск страницы. ИЛИ СПЕЦИАЛЬНОЕ ДЕЙСТВИЕ!
325
      // ...которые по факты должны обрабатываться в рамках provider->login()
326
    } else {
327
      // Есть хотя бы один авторизированный аккаунт
328
      $temp          = reset($this->providers_authorised);
329
      $this->account = $temp->account;
330
331
      $this->get_accessible_user_list();
332
      // В self::$accessible_user_row_list - список доступных игроков для данных аккаунтов с соответствующими записями из таблицы `users`
333
334
      // Остались ли у нас в списке доступные игроки?
335
      if (empty($this->accessible_user_row_list)) {
336
        // Нет ни одного игрока ни на одном авторизированном аккаунте
337
        // Надо регистрировать нового игрока
338
339
        // Сейчас происходит процесс регистрации игрока?
340
        if (!$this->is_player_register) {
341
          // Нет - отправляем на процесс регистрации
342
          $partner_id = sys_get_param_int('id_ref', sys_get_param_int('partner_id'));
343
          sys_redirect(SN_ROOT_VIRTUAL . 'index.php?page=player_register&player_register=1' . ($partner_id ? '&id_ref=' . $partner_id : ''));
344
        }
345
      } else {
346
        // Да, есть доступные игроки, которые так же прописаны в базе
347
        $this->get_active_user(); // 4.5
348
349
        if ($this->is_impersonating = !empty($_COOKIE[SN_COOKIE_U_I]) ? $_COOKIE[SN_COOKIE_U_I] : 0) {
0 ignored issues
show
Documentation Bug introduced by
It seems like ! empty($_COOKIE[SN_COOK...OKIE[SN_COOKIE_U_I] : 0 can also be of type integer. However, the property $is_impersonating is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
350
          $a_user                      = db_user_by_id($this->is_impersonating);
0 ignored issues
show
Deprecated Code introduced by
The function db_user_by_id() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

350
          $a_user                      = /** @scrutinizer ignore-deprecated */ db_user_by_id($this->is_impersonating);
Loading history...
351
          $this->impersonator_username = $a_user['username'];
352
        }
353
354
355
        //Прописываем текущего игрока на все авторизированные аккаунты
356
        // TODO - ИЛИ ВСЕХ ИГРОКОВ??
357
        if (empty($this->is_impersonating)) {
358
          foreach ($this->providers_authorised as $provider_id => $provider) {
359
            if (empty($this->user_id_to_provider[self::$user['id']][$provider_id])) {
360
              // self::db_translate_register_user($provider_id, $provider->account->account_id, self::$user['id']);
361
              PlayerToAccountTranslate::db_translate_register_user($provider_id, $provider->account->account_id, self::$user['id']);
362
              $this->user_id_to_provider[self::$user['id']][$provider_id][$provider->account->account_id] = true;
363
            }
364
          }
365
        }
366
      }
367
    }
368
369
    if (empty(self::$user['id'])) {
370
      self::cookie_set(''); // OK 4.5
371
    } elseif (self::$user['id'] != $_COOKIE[SN_COOKIE_U]) {
372
      self::cookie_set(self::$user['id']); // OK 4.5
373
    }
374
375
    return $this->make_return_array();
376
  }
377
378
  /**
379
   * Логаут игрока и всех аккаунтов
380
   *
381
   * @param bool|string $redirect          Нужно ли сделать перенаправление после логаута
382
   *                                       <p><b>false</b> - не перенаправлять</p>
383
   *                                       <p><i><b>true</b></i> - перенаправить на главную страницу</p>
384
   *                                       <p><b>string</b> - перенаправить на указанный URL</p>
385
   */
386
  // OK v4.7
387
  public function logout($redirect = true) {
388
    if (!empty($_COOKIE[SN_COOKIE_U_I])) {
389
      self::cookie_set($_COOKIE[SN_COOKIE_U_I]);
390
      self::cookie_set(0, true);
391
      self::$main_provider->logout();
392
    } else {
393
      foreach ($this->providers as $provider) {
394
        $provider->logout();
395
      }
396
397
      self::cookie_set(0);
398
    }
399
400
    if ($redirect === true) {
401
      sys_redirect(SN_ROOT_RELATIVE . (empty($_COOKIE[SN_COOKIE_U]) ? 'login.php' : 'admin/overview.php'));
402
    } elseif ($redirect !== false) {
403
      sys_redirect($redirect);
404
    }
405
  }
406
407
  /**
408
   * Имперсонация
409
   *
410
   * @param $user_selected
411
   */
412
  public function impersonate($user_selected) {
413
    if ($_COOKIE[SN_COOKIE_U_I]) {
414
      die('You already impersonating someone. Go back to living other\'s life! Or clear your cookies and try again'); // TODO: Log it
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
415
    }
416
417
    if ($this->auth_level_max_local < AUTH_LEVEL_ADMINISTRATOR) {
418
      die('You can\'t impersonate - too low level'); // TODO: Log it
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
419
    }
420
421
    if ($this->auth_level_max_local <= $user_selected['authlevel']) {
422
      die('You can\'t impersonate this account - level is greater or equal to yours'); // TODO: Log it
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
423
    }
424
425
    $account_translate      = PlayerToAccountTranslate::db_translate_get_account_by_user_id($user_selected['id'], self::$main_provider->provider_id);
426
    $account_translate      = reset($account_translate[$user_selected['id']][self::$main_provider->provider_id]);
427
    $account_to_impersonate = new Account(self::$main_provider->db);
428
    $account_to_impersonate->db_get_by_id($account_translate['provider_account_id']);
429
    if (!$account_to_impersonate->is_exists) {
430
      die('Какая-то ошибка - не могу найти аккаунт для имперсонации'); // TODO: Log it
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
431
    }
432
    self::$main_provider->impersonate($account_to_impersonate);
433
434
    self::cookie_set($_COOKIE[SN_COOKIE_U], true, 0);
435
436
    // TODO - Имперсонация - только на одну сессию
437
    self::cookie_set($user_selected['id']);
438
439
    // sec_set_cookie_by_user($user_selected, 0);
440
    sys_redirect(SN_ROOT_RELATIVE);
441
  }
442
443
  /**
444
   * Проверяет пароль на совпадение с текущими паролями
445
   *
446
   * @param $password_unsafe
447
   *
448
   * @return bool
449
   */
450
  // OK v4.6
451
  // TODO - ПЕРЕДЕЛАТЬ!
452
  public function password_check($password_unsafe) {
453
    $result = false;
454
455
    if (empty($this->providers_authorised)) {
456
      // TODO - такого быть не может!
457
      self::flog("password_check: Не найдено ни одного авторизированного провайдера в self::\$providers_authorised", true);
458
    } else {
459
      foreach ($this->providers_authorised as $provider) {
460
        if ($provider->is_feature_supported(AUTH_FEATURE_HAS_PASSWORD)) {
461
          $result = $result || $provider->password_check($password_unsafe);
462
        }
463
      }
464
    }
465
466
    return $result;
467
  }
468
469
  /**
470
   * Меняет старый пароль на новый
471
   *
472
   * @param $old_password_unsafe
473
   * @param $new_password_unsafe
474
   *
475
   * @return bool
476
   * @throws Exception
477
   */
478
  // OK v4.6
479
  public function password_change($old_password_unsafe, $new_password_unsafe) {
480
    global $lang;
481
482
    if (empty($this->providers_authorised)) {
483
      // TODO - такого быть не может!
484
      self::flog("Не найдено ни одного авторизированного провайдера в self::\$providers_authorised", true);
485
486
      return false;
487
    }
488
489
    // TODO - Проверять пароль на корректность
490
491
    // TODO - Не менять (?) пароль у аккаунтов, к которым пристёгнут хоть один игрок с AUTH_LEVEL > 0
492
493
    $salt_unsafe = self::password_salt_generate();
494
495
    $providers_changed_password = array();
496
    foreach ($this->providers_authorised as $provider_id => $provider) {
497
      if (
498
        !$provider->is_feature_supported(AUTH_FEATURE_PASSWORD_CHANGE)
499
        || !$provider->password_change($old_password_unsafe, $new_password_unsafe, $salt_unsafe)
500
      ) {
501
        continue;
502
      }
503
504
      // Узнаем список игроков, которые прикреплены к этому аккаунту
505
      // $account_translation = self::db_translate_get_users_from_account_list($provider_id, $provider->account->account_id);
506
      $account_translation = PlayerToAccountTranslate::db_translate_get_users_from_account_list($provider_id, $provider->account->account_id);
507
508
      // Рассылаем уведомления о смене пароля в ЛС
509
      foreach ($account_translation as $user_id => $provider_info) {
510
        // TODO - Указывать тип аккаунта, на котором сменён пароль
511
        msg_send_simple_message($user_id, 0, SN_TIME_NOW, MSG_TYPE_ADMIN,
512
          $lang['sys_administration'], $lang['sys_login_register_message_title'],
513
          sprintf($lang['sys_login_register_message_body'], $provider->account->account_name, $new_password_unsafe), false //true
514
        );
515
      }
516
      $providers_changed_password[$provider_id] = $provider;
517
    }
518
519
    // TODO - отсылать уведомление на емейл
520
521
    return !empty($providers_changed_password);
522
  }
523
524
525
526
527
  /**
528
   * Сбрасывает значения полей
529
   */
530
  // OK v4.5
531
  protected function auth_reset() {
532
    self::$login_status             = LOGIN_UNDEFINED;
533
    $this->player_suggested_name    = '';
534
    $this->account                  = null;
535
    self::$user                     = null;
536
    $this->providers_authorised     = array(); // Все аккаунты, которые успешно залогинились
537
    $this->provider_error_list      = array(); // Статусы всех аккаунтов
538
    $this->accessible_user_row_list = array();
539
    $this->user_id_to_provider      = array();
540
  }
541
542
  /**
543
   * Функция пытается создать игрока в БД, делая все нужные проверки
544
   *
545
   * @param $player_name_unsafe
546
   */
547
  // OK v4
548
  protected function register_player_db_create($player_name_unsafe) {
549
    try {
550
      // Проверить корректность имени
551
      $this->register_player_name_validate($player_name_unsafe);
552
553
      db_mysql::db_transaction_start();
554
      // Проверить наличие такого имени в истории имён
555
556
      if (db_player_name_exists($player_name_unsafe)) {
557
        throw new Exception(REGISTER_ERROR_PLAYER_NAME_EXISTS, ERR_ERROR);
558
      }
559
560
      // Узнаем язык и емейл игрока
561
      $player_language = '';
562
      $player_email    = '';
563
      // TODO - порнография - работа должна происходить над списком аккаунтов, а не только на одном аккаунте...
564
      foreach ($this->providers_authorised as $provider) {
565
        if (!$player_language && $provider->account->account_language) {
566
          $player_language = $provider->account->account_language;
567
        }
568
        if (!$player_email && $provider->account->account_email) {
569
          $player_email = $provider->account->account_email;
570
        }
571
      }
572
      $player_language = sys_get_param_str('lang') ? sys_get_param_str('lang') : $player_language;
573
      $player_language = $player_language ?: DEFAULT_LANG;
574
575
      // TODO - дописать exceptions в процедуре создания игрока
576
      self::$user = player_create($player_name_unsafe, $player_email, array(
577
        'partner_id'   => sys_get_param_int('id_ref', sys_get_param_int('partner_id')),
578
        'language_iso' => static::$db->db_escape($player_language),
579
        // 'password_encoded_unsafe' => $this->data[F_ACCOUNT]['account_password'],
580
        // 'salt' => $this->data[F_ACCOUNT]['account_salt'],
581
      ));
582
      // Зарегистрировать на него аккаунты из self::$accounts_authorised
583
      $a_user = self::$user;
584
      foreach ($this->providers_authorised as $provider) {
585
        // TODO - порнография. Должен быть отдельный класс трансляторов - в т.ч. и кэширующий транслятор
586
        // TODO - ну и работа должна происходить над списком аккаунтов, а не только на одном аккаунте...
587
        // self::db_translate_register_user($provider->provider_id, $provider->account->account_id, $a_user['id']);
588
        PlayerToAccountTranslate::db_translate_register_user($provider->provider_id, $provider->account->account_id, $a_user['id']);
589
590
      }
591
      // Установить куку игрока
592
      self::cookie_set(self::$user['id']);
593
594
      db_mysql::db_transaction_commit();
595
      $this->register_status = LOGIN_SUCCESS;
596
    } catch (Exception $e) {
597
      db_mysql::db_transaction_rollback();
598
599
      // Если старое имя занято
600
      self::$user = null;
601
      if ($this->register_status == LOGIN_UNDEFINED) {
602
        $this->register_status = $e->getMessage();
0 ignored issues
show
Documentation Bug introduced by
The property $register_status was declared of type integer, but $e->getMessage() is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
603
      }
604
    }
605
  }
606
607
608
  /**
609
   * Проверяет доступ авторизированных аккаунтов к заявленным в трансляции юзерам
610
   * @version 4.5
611
   */
612
  // OK v4.5
613
  protected function get_accessible_user_list() {
614
    // Пробиваем все ИД игроков по базе - есть ли вообще такие записи
615
    // Вообще-то это не особо нужно - у нас по определению стоят ограничения
616
    // Зато так мы узнаем максимальный authlevel, проверим права имперсонейта и вытащим все записи юзеров
617
    foreach ($this->user_id_to_provider as $user_id => $cork) {
618
      $user = db_user_by_id($user_id);
0 ignored issues
show
Deprecated Code introduced by
The function db_user_by_id() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

618
      $user = /** @scrutinizer ignore-deprecated */ db_user_by_id($user_id);
Loading history...
619
      // Если записи игрока в БД не существует?
620
      if (empty($user['id'])) {
621
        // Удаляем этого и переходим к следующему
622
        unset($this->user_id_to_provider[$user_id]);
623
        // Де-регистрируем игрока из таблицы трансляции игроков
624
        PlayerToAccountTranslate::db_translate_unregister_user($user_id);
625
      } else {
626
        $this->accessible_user_row_list[$user['id']] = $user;
627
        $this->auth_level_max_local                  = max($this->auth_level_max_local, $user['authlevel']);
628
      }
629
      unset($user);
630
    }
631
  }
632
633
  /**
634
   * Выбирает активного игрока из куки или из списка доступных игроков
635
   *
636
   * @version 4.5
637
   */
638
  // OK v4.5
639
  protected function get_active_user() {
640
    // Проверяем куку "текущего игрока" из браузера
641
    if (
642
      // Кука не пустая
643
      ($_COOKIE[SN_COOKIE_U] = trim($_COOKIE[SN_COOKIE_U])) && !empty($_COOKIE[SN_COOKIE_U])
644
      // И в куке находится ID
645
      && is_id($_COOKIE[SN_COOKIE_U])
646
      // И у аккаунтов есть права на данного игрока
647
      && (
648
        // Есть прямые права из `account_translate`
649
        !empty($this->accessible_user_row_list[$_COOKIE[SN_COOKIE_U]])
650
        // Или есть доступ через имперсонейт
651
        || (
652
          // Максимальные права всех доступных записей игроков - не ниже администраторских
653
          $this->auth_level_max_local >= AUTH_LEVEL_ADMINISTRATOR
654
          // И права игрока, в которого пытаются зайти - меньше текущих максимальных прав
655
          && $this->accessible_user_row_list[$_COOKIE[SN_COOKIE_U]]['authlevel'] < $this->auth_level_max_local
656
        )
657
      )
658
    ) {
659
      // Берем текущим юзером юзера с ИД из куки
660
      self::$user = db_user_by_id($_COOKIE[SN_COOKIE_U]);
0 ignored issues
show
Deprecated Code introduced by
The function db_user_by_id() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

660
      self::$user = /** @scrutinizer ignore-deprecated */ db_user_by_id($_COOKIE[SN_COOKIE_U]);
Loading history...
661
    }
662
663
    // В куке нет валидного ИД записи игрока, доступной с текущих аккаунтов
664
    if (empty(self::$user['id'])) {
665
      // Берем первого из доступных
666
      // TODO - default_user
667
      self::$user = reset($this->accessible_user_row_list);
668
    }
669
  }
670
671
672
673
  /**
674
   * Генерирует набор данных для возврата в основной код
675
   * @noinspection SpellCheckingInspection
676
   */
677
  // OK v4.5
678
  protected function make_return_array() {
679
    global $config;
680
681
    $user_id = !empty(self::$user['id']) ? self::$user['id'] : 0;
682
    // if(!empty($user_id) && !$user_impersonator) {
683
    // $user_id не может быть пустым из-за ключей в таблице SPE
684
    // self::db_security_entry_insert();
685
    self::$device->db_security_entry_insert($user_id);
0 ignored issues
show
Deprecated Code introduced by
The function RequestInfo::db_security_entry_insert() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

685
    /** @scrutinizer ignore-deprecated */ self::$device->db_security_entry_insert($user_id);
Loading history...
686
687
    $result = array();
688
689
    if ($user_id && empty($this->is_impersonating)) {
690
      // self::db_counter_insert();
691
      self::$device->db_counter_insert($user_id);
692
693
      $user = &self::$user;
694
695
      sys_user_options_unpack($user);
696
697
      if ($user['banaday'] && $user['banaday'] <= SN_TIME_NOW) {
698
        $user['banaday']  = 0;
699
        $user['vacation'] = SN_TIME_NOW;
700
      }
701
702
      /** @noinspection SpellCheckingInspection */
703
      $user['user_lastip'] = self::$device->ip_v4_string;// $ip['ip'];
704
      $user['user_proxy']  = self::$device->ip_v4_proxy_chain; //$ip['proxy_chain'];
705
706
      $result[F_BANNED_STATUS]   = $user['banaday'];
707
      $result[F_VACATION_STATUS] = $user['vacation'];
708
709
      $proxy_safe = static::$db->db_escape(self::$device->ip_v4_proxy_chain);
710
711
      doquery("LOCK TABLES {{users}} WRITE;");
0 ignored issues
show
Deprecated Code introduced by
The function doquery() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

711
      /** @scrutinizer ignore-deprecated */ doquery("LOCK TABLES {{users}} WRITE;");
Loading history...
712
      /** @noinspection SpellCheckingInspection */
713
      db_user_set_by_id($user['id'], "`onlinetime` = " . SN_TIME_NOW . ",
0 ignored issues
show
Deprecated Code introduced by
The function db_user_set_by_id() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

713
      /** @scrutinizer ignore-deprecated */ db_user_set_by_id($user['id'], "`onlinetime` = " . SN_TIME_NOW . ",
Loading history...
714
      `banaday` = " . static::$db->db_escape($user['banaday']) . ", `vacation` = " . static::$db->db_escape($user['vacation']) . ",
715
      `user_lastip` = '" . static::$db->db_escape($user['user_lastip']) . "', `user_last_proxy` = '{$proxy_safe}', `user_last_browser_id` = " . self::$device->browser_id
716
      );
717
      doquery("UNLOCK TABLES;");
0 ignored issues
show
Deprecated Code introduced by
The function doquery() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

717
      /** @scrutinizer ignore-deprecated */ doquery("UNLOCK TABLES;");
Loading history...
718
    }
719
720
    if ($extra = $config->security_ban_extra) {
721
      $extra = explode(',', $extra);
722
      array_walk($extra, 'trim');
723
      in_array(self::$device->device_id, $extra) and die();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
724
    }
725
726
    if (self::$login_message) {
727
      $result[F_LOGIN_MESSAGE] = self::$login_message;
728
    }
729
730
    $result[F_LOGIN_STATUS]           = self::$login_status = empty($this->providers_authorised) ? self::$login_status : LOGIN_SUCCESS;
731
    $result[F_PLAYER_REGISTER_STATUS] = $this->register_status;
732
    $result[F_USER]                   = self::$user;
733
734
    // $result[AUTH_LEVEL] = isset(self::$user['authlevel']) ? self::$user['authlevel'] : AUTH_LEVEL_ANONYMOUS;
735
    $result[AUTH_LEVEL] = $this->auth_level_max_local;
736
737
    $result[F_IMPERSONATE_STATUS]   = $this->is_impersonating;
738
    $result[F_IMPERSONATE_OPERATOR] = $this->impersonator_username;
739
    // TODO
740
//    self::$hidden[F_IMPERSONATE_OPERATOR] = $found_provider->data[F_IMPERSONATE_OPERATOR];
741
742
    //TODO Сол и Парол тоже вкинуть в хидден
743
    $result[F_ACCOUNTS_AUTHORISED] = $this->providers_authorised;
744
745
    return $result;
746
  }
747
748
749
  // ХЕЛПЕРЫ ===========================================================================================================
750
  /**
751
   * Функция проверяет корректность имени игрока при регистрации
752
   *
753
   * @param $player_name_unsafe
754
   *
755
   * @throws Exception
756
   */
757
  // OK v4
758
  // TODO - вынести в отдельный хелпер
759
  protected function register_player_name_validate($player_name_unsafe) {
760
    // TODO - переделать под RAW-строки
761
    // Если имя игрока пустое - NO GO!
762
    if (trim($player_name_unsafe) == '') {
763
      throw new Exception(REGISTER_ERROR_PLAYER_NAME_EMPTY, ERR_ERROR);
764
    }
765
    // Проверяем, что бы в начале и конце не было пустых символов
766
    if ($player_name_unsafe != trim($player_name_unsafe)) {
767
      throw new Exception(REGISTER_ERROR_PLAYER_NAME_TRIMMED, ERR_ERROR);
768
    }
769
    // Если логин имеет запрещенные символы - NO GO!
770
    if (strpbrk($player_name_unsafe, LOGIN_REGISTER_CHARACTERS_PROHIBITED)) {
771
      // TODO - выдавать в сообщение об ошибке список запрещенных символов
772
      // TODO - заранее извещать игрока, какие символы являются запрещенными
773
      throw new Exception(REGISTER_ERROR_PLAYER_NAME_RESTRICTED_CHARACTERS, ERR_ERROR);
774
    }
775
    // Если логин меньше минимальной длины - NO GO!
776
    if (strlen($player_name_unsafe) < LOGIN_LENGTH_MIN) {
777
      // TODO - выдавать в сообщение об ошибке минимальную длину имени игрока
778
      // TODO - заранее извещать игрока, какая минимальная и максимальная длина имени
779
      throw new Exception(REGISTER_ERROR_PLAYER_NAME_SHORT, ERR_ERROR);
780
    }
781
782
    // TODO проверка на максимальную длину имени игрока
783
  }
784
785
//  /**
786
//   * Генерирует случайный код для сброса пароля
787
//   *
788
//   * @return string
789
//   */
790
//  // OK v4
791
//  public static function make_password_reset_code() {
792
//    return sys_random_string(LOGIN_PASSWORD_RESET_CONFIRMATION_LENGTH, SN_SYS_SEC_CHARS_CONFIRMATION);
793
//  }
794
  /**
795
   * Генерирует случайный пароль
796
   *
797
   * @return string
798
   */
799
  // OK v4
800
  public static function make_random_password() {
801
    return sys_random_string(LOGIN_PASSWORD_RESET_CONFIRMATION_LENGTH, SN_SYS_SEC_CHARS_CONFIRMATION);
802
  }
803
  /**
804
   * Просаливает пароль
805
   *
806
   * @param $password
807
   * @param $salt
808
   *
809
   * @return string
810
   */
811
  // OK v4
812
  public static function password_encode($password, $salt) {
813
    return md5($password . $salt);
814
  }
815
  /**
816
   * Генерирует соль
817
   *
818
   * @return string
819
   */
820
  // OK v4
821
  public static function password_salt_generate() {
822
    // НЕ ПЕРЕКРЫВАТЬ
823
    // TODO ВКЛЮЧИТЬ ГЕНЕРАЦИЮ СОЛИ !!!
824
    return ''; // sys_random_string(16);
825
  }
826
827
  // OK v4.5
828
  // TODO - REMEMBER_ME
829
  protected static function cookie_set($value, $impersonate = false, $period = null) {
830
    sn_setcookie($impersonate ? SN_COOKIE_U_I : SN_COOKIE_U, $value, $period === null ? SN_TIME_NOW + PERIOD_YEAR : $period);
831
  }
832
833
  protected static function flog($message, $die = false) {
834
    if (!defined('DEBUG_AUTH') || !DEBUG_AUTH) {
0 ignored issues
show
Bug introduced by
The constant DEBUG_AUTH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
835
      return;
836
    }
837
    list($called, $caller) = debug_backtrace(false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $options of debug_backtrace(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

837
    list($called, $caller) = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
838
    $caller_name =
839
      (!empty($caller['class']) ? $caller['class'] : '') .
840
      (!empty($caller['type']) ? $caller['type'] : '') .
841
      (!empty($caller['function']) ? $caller['function'] : '') .
842
      (!empty($called['line']) ? ':' . $called['line'] : '');
843
844
    if ($_SERVER['SERVER_NAME'] == 'localhost') {
845
      print("<div class='debug'>$message - $caller_name\r\n</div>");
846
    }
847
848
    SN::log_file("$message - $caller_name");
849
    if ($die) {
850
      $die && die("<div class='negative'>СТОП! Функция {$caller_name} при вызове в " . get_called_class() . " (располагается в " . get_class() . "). СООБЩИТЕ АДМИНИСТРАЦИИ!</div>");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
851
    }
852
  }
853
854
}
855