]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Util/Password.php
Code quality changes
[galette.git] / galette / lib / Galette / Util / Password.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Password checks
7 *
8 * PHP version 5
9 *
10 * Copyright © 2020-2023 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Util
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2020-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.9.4
35 */
36
37 namespace Galette\Util;
38
39 use Analog\Analog;
40 use Galette\Core\Preferences;
41 use Galette\Entity\Adherent;
42
43 /**
44 * Password checks
45 *
46 * @category Util
47 * @name Password
48 * @package Galette
49 * @author Johan Cwiklinski <johan@x-tnd.be>
50 * @copyright 2020-2023 The Galette Team
51 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
52 * @link http://galette.tuxfamily.org
53 * @see https://github.com/rollerworks/PasswordStrengthValidator
54 * @since Available since 0.9.4
55 */
56 class Password
57 {
58 protected $preferences;
59 protected $errors = [];
60 protected $strength_errors = [];
61 protected $strength = null;
62 protected $blacklisted = false;
63 protected $personal_infos = [];
64
65 /**
66 * Default constructor
67 *
68 * @param Preferences $prefs Preferences instance
69 */
70 public function __construct(Preferences $prefs)
71 {
72 $this->preferences = $prefs;
73 }
74
75 /**
76 * Does password suits requirements?
77 *
78 * @param string $password Password to test
79 *
80 * @return boolean
81 */
82 public function isValid($password)
83 {
84 $this->errors = []; //reset
85
86 if ($this->isBlacklisted($password)) {
87 $this->errors[] = _T('Password is blacklisted!');
88 //no need here to check lenght/strength
89 $this->strength = 0;
90 return false;
91 }
92
93 if (mb_strlen($password) < $this->preferences->pref_password_length) {
94 $this->errors[] = str_replace(
95 ['%lenght', '%count'],
96 [$this->preferences->pref_password_length, mb_strlen($password)],
97 _T('Too short (%lenght characters minimum, %count found)')
98 );
99 }
100
101 $this->strength = $this->calculateStrength($password);
102 if ($this->strength < $this->preferences->pref_password_strength) {
103 $this->errors = array_merge($this->errors, $this->strength_errors);
104 }
105
106 if ($this->preferences->pref_password_strength > Preferences::PWD_NONE) {
107 //check also against personal information
108 if (in_array(mb_strtolower($password), $this->personal_infos)) {
109 $this->errors[] = _T('Do not use any of your personal information as password!');
110 }
111 }
112
113 return (count($this->errors) === 0);
114 }
115
116 /**
117 * Is password blacklisted?
118 *
119 * @param string $password Password to check
120 *
121 * @return boolean
122 */
123 public function isBlacklisted($password)
124 {
125 if (!$this->preferences->pref_password_blacklist) {
126 return false;
127 }
128
129 return in_array(
130 mb_strtolower($password),
131 $this->getBlacklistedPasswords()
132 );
133 }
134
135 /**
136 * Calculate pasword strength
137 *
138 * @param string $password Password to check
139 *
140 * @return integer
141 */
142 public function calculateStrength($password)
143 {
144 $strength = 0;
145
146 if (preg_match('/\p{L}/u', $password)) {
147 ++$strength;
148
149 if (!preg_match('/\p{Ll}/u', $password)) {
150 $this->strength_errors[] = _T('Does not contains lowercase letters');
151 } elseif (preg_match('/\p{Lu}/u', $password)) {
152 ++$strength;
153 } else {
154 $this->strength_errors[] = _T('Does not contains uppercase letters');
155 }
156 } else {
157 $this->strength_errors[] = _T('Does not contains letters');
158 }
159
160 if (preg_match('/\p{N}/u', $password)) {
161 ++$strength;
162 } else {
163 $this->strength_errors[] = _T('Does not contains numbers');
164 }
165
166 if (preg_match('/[^\p{L}\p{N}]/u', $password)) {
167 ++$strength;
168 } else {
169 $this->strength_errors[] = _T('Does not contains special characters');
170 }
171
172 return $strength;
173 }
174
175 /**
176 * Get current strength
177 *
178 * @return integer
179 */
180 public function getStrenght()
181 {
182 return $this->strength;
183 }
184
185 /**
186 * Get errors
187 *
188 * @return array
189 */
190 public function getErrors()
191 {
192 return $this->errors;
193 }
194
195 /**
196 * Get strength errors
197 *
198 * @return array
199 */
200 public function getStrenghtErrors()
201 {
202 return $this->strength_errors;
203 }
204
205 /**
206 * Build password blacklist
207 *
208 * @return array
209 */
210 public function getBlacklistedPasswords()
211 {
212 $file = GALETTE_DATA_PATH . '/blacklist.txt';
213
214 if (!file_exists($file)) {
215 //copy default provided list
216 $worst500 = explode(PHP_EOL, file_get_contents(GALETTE_ROOT . 'includes/fields_defs/pass_blacklist'));
217 file_put_contents($file, implode(PHP_EOL, $worst500));
218 }
219
220 $blacklist = explode(PHP_EOL, file_get_contents($file));
221 $blacklist[] = 'galette'; //that one should always be blacklisted... :)
222
223 return $blacklist;
224 }
225
226 /**
227 * Add personal information to check against
228 *
229 * @param array $infos Personal information
230 *
231 * @return array
232 */
233 public function addPersonalInformation(array $infos)
234 {
235 $this->personal_infos = array_merge(
236 $this->personal_infos,
237 array_map('mb_strtolower', array_values($infos))
238 );
239 return $this->personal_infos;
240 }
241
242 /**
243 * Set member and calculate personal information to blacklist
244 *
245 * @param Adherent $adh Adherent instance
246 *
247 * @return Password
248 */
249 public function setAdherent(Adherent $adh)
250 {
251 $infos = [
252 $adh->name,
253 $adh->surname,
254 $adh->birthdate ?? '', //locale formatted
255 $adh->rbirthdate, //raw
256 $adh->nickname,
257 $adh->town,
258 $adh->login,
259 $adh->email
260 ];
261
262 //handle date formats
263 $bdate = \DateTime::createFromFormat('Y-m-d', $adh->rbirthdate);
264 if ($bdate !== false) {
265 $infos[] = $bdate->format('Y-m-d'); //standard format
266 //TRANS: see https://www.php.net/manual/datetime.format.php
267 $infos[] = $bdate->format(__('Y-m-d')); //localized format
268 $infos[] = $bdate->format('Ymd');
269 $infos[] = $bdate->format('dmY');
270 $infos[] = $bdate->format('Ydm');
271 }
272
273 //some possible combinations
274 foreach ([$adh->surname, $adh->nickname, $adh->login] as $surname) {
275 if ($surname === null) {
276 continue;
277 }
278 $infos[] = mb_substr($surname, 0, 1) . $adh->name;
279 $infos[] = $adh->name . mb_substr($surname, 0, 1);
280 $infos[] = $surname . $adh->name;
281 $infos[] = $adh->name . $surname;
282
283 //compound surnames
284 $parts = preg_split('/[- _]/', $surname);
285 if (count($parts) > 1) {
286 $letters = '';
287 foreach ($parts as $part) {
288 $letters .= mb_substr($part, 0, 1);
289 }
290 $infos[] = $letters . $adh->name;
291 $infos[] = $adh->name . $letters;
292 }
293 }
294
295 $this->addPersonalInformation($infos);
296
297 return $this;
298 }
299 }