1 <?php
  2 
  3 /**
  4  * @package redaxo\core\login
  5  *
  6  * @method null|rex_user getUser()
  7  * @method null|rex_user getImpersonator()
  8  */
  9 class rex_backend_login extends rex_login
 10 {
 11     const SYSTEM_ID = 'backend_login';
 12     const LOGIN_TRIES_1 = 3;
 13     const RELOGIN_DELAY_1 = 5;    // relogin delay after LOGIN_TRIES_1 tries
 14     const LOGIN_TRIES_2 = 50;
 15     const RELOGIN_DELAY_2 = 3600; // relogin delay after LOGIN_TRIES_2 tries
 16 
 17     private $tableName;
 18     private $stayLoggedIn;
 19 
 20     public function __construct()
 21     {
 22         parent::__construct();
 23 
 24         $tableName = rex::getTablePrefix() . 'user';
 25         $this->setSqlDb(1);
 26         $this->setSystemId(self::SYSTEM_ID);
 27         $this->setSessionDuration(rex::getProperty('session_duration'));
 28         $qry = 'SELECT * FROM ' . $tableName;
 29         $this->setUserQuery($qry . ' WHERE id = :id AND status = 1');
 30         $this->setImpersonateQuery($qry . ' WHERE id = :id');
 31         // XXX because with concat the time into the sql query, users of this class should use checkLogin() immediately after creating the object.
 32         $this->setLoginQuery($qry . ' WHERE
 33             status = 1
 34             AND login = :login
 35             AND (login_tries < ' . self::LOGIN_TRIES_1 . '
 36                 OR login_tries < ' . self::LOGIN_TRIES_2 . ' AND lasttrydate < "' . rex_sql::datetime(time() - self::RELOGIN_DELAY_1) . '"
 37                 OR lasttrydate < "' . rex_sql::datetime(time() - self::RELOGIN_DELAY_2) . '"
 38             )'
 39         );
 40         $this->tableName = $tableName;
 41     }
 42 
 43     public function setStayLoggedIn($stayLoggedIn = false)
 44     {
 45         $this->stayLoggedIn = $stayLoggedIn;
 46     }
 47 
 48     public function checkLogin()
 49     {
 50         $sql = rex_sql::factory();
 51         $userId = $this->getSessionVar('UID');
 52         $cookiename = self::getStayLoggedInCookieName();
 53 
 54         if ($cookiekey = rex_cookie($cookiename, 'string')) {
 55             if (!$userId) {
 56                 $sql->setQuery('SELECT id FROM ' . rex::getTable('user') . ' WHERE cookiekey = ? LIMIT 1', [$cookiekey]);
 57                 if ($sql->getRows() == 1) {
 58                     $this->setSessionVar('UID', $sql->getValue('id'));
 59                     rex_response::sendCookie($cookiename, $cookiekey, ['expires' => strtotime('+1 year'), 'samesite' => 'strict']);
 60                 } else {
 61                     self::deleteStayLoggedInCookie();
 62                 }
 63             }
 64             $this->setSessionVar('STAMP', time());
 65         }
 66 
 67         $check = parent::checkLogin();
 68 
 69         if ($check) {
 70             // gelungenen versuch speichern | login_tries = 0
 71             if ($this->userLogin != '' || !$userId) {
 72                 self::regenerateSessionId();
 73                 $params = [];
 74                 $add = '';
 75                 if ($this->stayLoggedIn || $cookiekey) {
 76                     $cookiekey = sha1($this->systemId . time() . $this->userLogin);
 77                     $add = 'cookiekey = ?, ';
 78                     $params[] = $cookiekey;
 79                     rex_response::sendCookie($cookiename, $cookiekey, ['expires' => strtotime('+1 year'), 'samesite' => 'strict']);
 80                 }
 81                 if (self::passwordNeedsRehash($this->user->getValue('password'))) {
 82                     $add .= 'password = ?, ';
 83                     $params[] = self::passwordHash($this->userPassword, true);
 84                 }
 85                 array_push($params, rex_sql::datetime(), rex_sql::datetime(), session_id(), $this->userLogin);
 86                 $sql->setQuery('UPDATE ' . $this->tableName . ' SET ' . $add . 'login_tries=0, lasttrydate=?, lastlogin=?, session_id=? WHERE login=? LIMIT 1', $params);
 87             }
 88 
 89             $this->user = new rex_user($this->user);
 90 
 91             if ($this->impersonator instanceof rex_sql) {
 92                 $this->impersonator = new rex_user($this->impersonator);
 93             }
 94         } else {
 95             // fehlversuch speichern | login_tries++
 96             if ($this->userLogin != '') {
 97                 $sql->setQuery('SELECT login_tries FROM ' . $this->tableName . ' WHERE login=? LIMIT 1', [$this->userLogin]);
 98                 if ($sql->getRows() > 0) {
 99                     $login_tries = $sql->getValue('login_tries');
100                     $sql->setQuery('UPDATE ' . $this->tableName . ' SET login_tries=login_tries+1,session_id="",cookiekey="",lasttrydate=? WHERE login=? LIMIT 1', [rex_sql::datetime(), $this->userLogin]);
101                     if ($login_tries >= self::LOGIN_TRIES_1 - 1) {
102                         $time = $login_tries < self::LOGIN_TRIES_2 ? self::RELOGIN_DELAY_1 : self::RELOGIN_DELAY_2;
103                         $hours = floor($time / 3600);
104                         $mins = floor(($time - ($hours * 3600)) / 60);
105                         $secs = $time % 60;
106                         $formatted = ($hours ? $hours . 'h ' : '') . ($hours || $mins ? $mins . 'min ' : '') . $secs . 's';
107                         $this->message .= ' ' . rex_i18n::rawMsg('login_wait', '<strong data-time="' . $time . '">' . $formatted . '</strong>');
108                     }
109                 }
110             }
111         }
112 
113         if ($this->isLoggedOut() && $userId != '') {
114             $sql->setQuery('UPDATE ' . $this->tableName . ' SET session_id="", cookiekey="" WHERE id=? LIMIT 1', [$userId]);
115             self::deleteStayLoggedInCookie();
116         }
117 
118         return $check;
119     }
120 
121     public static function deleteSession()
122     {
123         self::startSession();
124 
125         unset($_SESSION[static::getSessionNamespace()][self::SYSTEM_ID]);
126         self::deleteStayLoggedInCookie();
127 
128         rex_csrf_token::removeAll();
129     }
130 
131     private static function deleteStayLoggedInCookie()
132     {
133         rex_response::sendCookie(self::getStayLoggedInCookieName(), '');
134     }
135 
136     private static function getStayLoggedInCookieName()
137     {
138         return 'rex_user_' . sha1(rex::getProperty('instname'));
139     }
140 
141     public static function hasSession()
142     {
143         // try to fast-fail, so we dont need to start a session in all cases (which would require a session lock...)
144         if (!isset($_COOKIE[session_name()])) {
145             return false;
146         }
147         self::startSession();
148 
149         $sessionNs = static::getSessionNamespace();
150         return isset($_SESSION[$sessionNs][self::SYSTEM_ID]['UID']) && $_SESSION[$sessionNs][self::SYSTEM_ID]['UID'] > 0;
151     }
152 
153     /**
154      * Creates the user object if it does not already exist.
155      *
156      * Helpful if you want to check permissions of the backend user in frontend.
157      * If you only want to know if there is any backend session, use {@link rex_backend_login::hasSession()}.
158      *
159      * @return rex_user
160      */
161     public static function createUser()
162     {
163         if (!self::hasSession()) {
164             return null;
165         }
166         if ($user = rex::getUser()) {
167             return $user;
168         }
169 
170         $login = new self();
171         rex::setProperty('login', $login);
172         if ($login->checkLogin()) {
173             $user = $login->getUser();
174             rex::setProperty('user', $user);
175             return $user;
176         }
177         return null;
178     }
179 
180     /**
181      * returns the backends session namespace.
182      *
183      * @return string
184      */
185     protected static function getSessionNamespace()
186     {
187         return rex::getProperty('instname'). '_backend';
188     }
189 }
190