Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.21% |
67 / 106 |
|
57.14% |
8 / 14 |
CRAP | |
0.00% |
0 / 1 |
ILS | |
63.21% |
67 / 106 |
|
57.14% |
8 / 14 |
124.72 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCatalog | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCatalog | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
authenticate | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
supportsPasswordChange | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getPasswordPolicy | |
44.44% |
4 / 9 |
|
0.00% |
0 / 1 |
6.74 | |||
updatePassword | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
4.00 | |||
getILSLoginMethod | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getDelegateAuthMethod | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
handleLogin | |
26.92% |
7 / 26 |
|
0.00% |
0 / 1 |
58.22 | |||
processILSUser | |
76.19% |
16 / 21 |
|
0.00% |
0 / 1 |
6.49 | |||
validatePasswordUpdate | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getLoggedInPatron | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getUsernameField | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * ILS authentication module. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Authentication |
25 | * @author Franck Borel <franck.borel@gbv.de> |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki |
29 | */ |
30 | |
31 | namespace VuFind\Auth; |
32 | |
33 | use Laminas\Http\PhpEnvironment\Request; |
34 | use VuFind\Db\Entity\UserEntityInterface; |
35 | use VuFind\Db\Service\UserServiceInterface; |
36 | use VuFind\Exception\Auth as AuthException; |
37 | use VuFind\Exception\ILS as ILSException; |
38 | |
39 | use function get_class; |
40 | |
41 | /** |
42 | * ILS authentication module. |
43 | * |
44 | * @category VuFind |
45 | * @package Authentication |
46 | * @author Franck Borel <franck.borel@gbv.de> |
47 | * @author Demian Katz <demian.katz@villanova.edu> |
48 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
49 | * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki |
50 | */ |
51 | class ILS extends AbstractBase |
52 | { |
53 | /** |
54 | * Catalog connection |
55 | * |
56 | * @var \VuFind\ILS\Connection |
57 | */ |
58 | protected $catalog = null; |
59 | |
60 | /** |
61 | * Constructor |
62 | * |
63 | * @param \VuFind\ILS\Connection $connection ILS connection to set |
64 | * @param \VuFind\Auth\ILSAuthenticator $authenticator ILS authenticator |
65 | * @param ?EmailAuthenticator $emailAuthenticator Email authenticator |
66 | */ |
67 | public function __construct( |
68 | \VuFind\ILS\Connection $connection, |
69 | protected \VuFind\Auth\ILSAuthenticator $authenticator, |
70 | protected ?EmailAuthenticator $emailAuthenticator = null |
71 | ) { |
72 | $this->setCatalog($connection); |
73 | } |
74 | |
75 | /** |
76 | * Get the ILS driver associated with this object (or load the default from |
77 | * the service manager. |
78 | * |
79 | * @return \VuFind\ILS\Connection |
80 | */ |
81 | public function getCatalog() |
82 | { |
83 | return $this->catalog; |
84 | } |
85 | |
86 | /** |
87 | * Set the ILS connection for this object. |
88 | * |
89 | * @param \VuFind\ILS\Connection $connection ILS connection to set |
90 | * |
91 | * @return void |
92 | */ |
93 | public function setCatalog(\VuFind\ILS\Connection $connection) |
94 | { |
95 | $this->catalog = $connection; |
96 | } |
97 | |
98 | /** |
99 | * Attempt to authenticate the current user. Throws exception if login fails. |
100 | * |
101 | * @param Request $request Request object containing account credentials. |
102 | * |
103 | * @throws AuthException |
104 | * @return UserEntityInterface Object representing logged-in user. |
105 | */ |
106 | public function authenticate($request) |
107 | { |
108 | $username = trim($request->getPost()->get('username', '')); |
109 | $password = trim($request->getPost()->get('password', '')); |
110 | $loginMethod = $this->getILSLoginMethod(); |
111 | $rememberMe = (bool)$request->getPost()->get('remember_me', false); |
112 | |
113 | return $this->handleLogin($username, $password, $loginMethod, $rememberMe); |
114 | } |
115 | |
116 | /** |
117 | * Does this authentication method support password changing |
118 | * |
119 | * @return bool |
120 | */ |
121 | public function supportsPasswordChange() |
122 | { |
123 | try { |
124 | return false !== $this->getCatalog()->checkFunction( |
125 | 'changePassword', |
126 | ['patron' => $this->authenticator->getStoredCatalogCredentials()] |
127 | ); |
128 | } catch (ILSException $e) { |
129 | return false; |
130 | } |
131 | } |
132 | |
133 | /** |
134 | * Password policy for a new password (e.g. minLength, maxLength) |
135 | * |
136 | * @return array |
137 | */ |
138 | public function getPasswordPolicy() |
139 | { |
140 | $policy = $this->getCatalog()->getPasswordPolicy($this->getLoggedInPatron()); |
141 | if ($policy === false) { |
142 | return parent::getPasswordPolicy(); |
143 | } |
144 | if (isset($policy['pattern']) && empty($policy['hint'])) { |
145 | $policy['hint'] = $this->getCannedPolicyHint( |
146 | 'password', |
147 | $policy['pattern'] |
148 | ); |
149 | } |
150 | return $policy; |
151 | } |
152 | |
153 | /** |
154 | * Update a user's password from the request. |
155 | * |
156 | * @param Request $request Request object containing new account details. |
157 | * |
158 | * @throws AuthException |
159 | * @return UserEntityInterface Updated user entity. |
160 | */ |
161 | public function updatePassword($request) |
162 | { |
163 | // Ensure that all expected parameters are populated to avoid notices |
164 | // in the code below. |
165 | $params = []; |
166 | foreach (['oldpwd', 'password', 'password2'] as $param) { |
167 | $params[$param] = $request->getPost()->get($param, ''); |
168 | } |
169 | |
170 | // Connect to catalog: |
171 | if (!($patron = $this->authenticator->storedCatalogLogin())) { |
172 | throw new AuthException('authentication_error_technical'); |
173 | } |
174 | |
175 | // Validate Input |
176 | $this->validatePasswordUpdate($params); |
177 | |
178 | $result = $this->getCatalog()->changePassword( |
179 | [ |
180 | 'patron' => $patron, |
181 | 'oldPassword' => $params['oldpwd'], |
182 | 'newPassword' => $params['password'], |
183 | ] |
184 | ); |
185 | if (!$result['success']) { |
186 | throw new AuthException($result['status']); |
187 | } |
188 | |
189 | // Update the user and send it back to the caller: |
190 | $username = $patron[$this->getUsernameField()]; |
191 | $user = $this->getOrCreateUserByUsername($username); |
192 | $this->authenticator->saveUserCatalogCredentials($user, $patron['cat_username'], $params['password']); |
193 | return $user; |
194 | } |
195 | |
196 | /** |
197 | * What login method does the ILS use (password, email, vufind) |
198 | * |
199 | * @param string $target Login target (MultiILS only) |
200 | * |
201 | * @return string |
202 | */ |
203 | public function getILSLoginMethod($target = '') |
204 | { |
205 | $config = $this->getCatalog()->checkFunction( |
206 | 'patronLogin', |
207 | ['patron' => ['cat_username' => "$target.login"]] |
208 | ); |
209 | return $config['loginMethod'] ?? 'password'; |
210 | } |
211 | |
212 | /** |
213 | * Returns any authentication method this request should be delegated to. |
214 | * |
215 | * @param Request $request Request object. |
216 | * |
217 | * @return string|bool |
218 | */ |
219 | public function getDelegateAuthMethod(Request $request) |
220 | { |
221 | return (null !== $this->emailAuthenticator |
222 | && $this->emailAuthenticator->isValidLoginRequest($request)) |
223 | ? 'Email' : false; |
224 | } |
225 | |
226 | /** |
227 | * Handle the actual login with the ILS. |
228 | * |
229 | * @param string $username User name |
230 | * @param string $password Password |
231 | * @param string $loginMethod Login method |
232 | * @param bool $rememberMe Whether to remember the login |
233 | * |
234 | * @throws AuthException |
235 | * @return UserEntityInterface Processed User object. |
236 | */ |
237 | protected function handleLogin($username, $password, $loginMethod, $rememberMe) |
238 | { |
239 | if ($username == '' || ('password' === $loginMethod && $password == '')) { |
240 | throw new AuthException('authentication_error_blank'); |
241 | } |
242 | |
243 | // Connect to catalog: |
244 | try { |
245 | $patron = $this->getCatalog()->patronLogin($username, $password); |
246 | } catch (AuthException $e) { |
247 | // Pass Auth exceptions through |
248 | throw $e; |
249 | } catch (\Exception $e) { |
250 | throw new AuthException('authentication_error_technical'); |
251 | } |
252 | |
253 | // Did the patron successfully log in? |
254 | if ('email' === $loginMethod) { |
255 | if (null === $this->emailAuthenticator) { |
256 | throw new \Exception('Email authenticator not set'); |
257 | } |
258 | if ($patron) { |
259 | $class = get_class($this); |
260 | if ($p = strrpos($class, '\\')) { |
261 | $class = substr($class, $p + 1); |
262 | } |
263 | $this->emailAuthenticator->sendAuthenticationLink( |
264 | $patron['email'], |
265 | [ |
266 | 'userData' => $patron, |
267 | 'rememberMe' => $rememberMe, |
268 | ], |
269 | ['auth_method' => $class] |
270 | ); |
271 | } |
272 | // Don't reveal the result |
273 | throw new \VuFind\Exception\AuthInProgress('email_login_link_sent'); |
274 | } |
275 | if ($patron) { |
276 | return $this->processILSUser($patron); |
277 | } |
278 | |
279 | // If we got this far, we have a problem: |
280 | throw new AuthException('authentication_error_invalid'); |
281 | } |
282 | |
283 | /** |
284 | * Update the database using details from the ILS, then return the User object. |
285 | * |
286 | * @param array $info User details returned by ILS driver. |
287 | * |
288 | * @throws AuthException |
289 | * @return UserEntityInterface Processed User object. |
290 | */ |
291 | protected function processILSUser($info) |
292 | { |
293 | // Figure out which field of the response to use as an identifier; fail |
294 | // if the expected field is missing or empty: |
295 | $usernameField = $this->getUsernameField(); |
296 | if (!isset($info[$usernameField]) || empty($info[$usernameField])) { |
297 | throw new AuthException('authentication_error_technical'); |
298 | } |
299 | |
300 | // Check to see if we already have an account for this user: |
301 | $userService = $this->getUserService(); |
302 | if (!empty($info['id'])) { |
303 | $user = $userService->getUserByCatId($info['id']); |
304 | if (empty($user)) { |
305 | $user = $this->getOrCreateUserByUsername($info[$usernameField]); |
306 | $user->setCatId($info['id']); |
307 | $this->getDbService(UserServiceInterface::class)->persistEntity($user); |
308 | } |
309 | } else { |
310 | $user = $this->getOrCreateUserByUsername($info[$usernameField]); |
311 | } |
312 | |
313 | // No need to store the ILS password in VuFind's main password field: |
314 | $user->setRawPassword(''); |
315 | |
316 | // Update user information based on ILS data: |
317 | $fields = ['firstname', 'lastname', 'major', 'college']; |
318 | foreach ($fields as $field) { |
319 | $this->setUserValueByField($user, $field, $info[$field] ?? ' '); |
320 | } |
321 | $userService->updateUserEmail($user, $info['email'] ?? ''); |
322 | |
323 | // Update the user in the database, then return it to the caller: |
324 | $this->authenticator->saveUserCatalogCredentials( |
325 | $user, |
326 | $info['cat_username'] ?? ' ', |
327 | $info['cat_password'] ?? ' ' |
328 | ); |
329 | |
330 | return $user; |
331 | } |
332 | |
333 | /** |
334 | * Make sure passwords match and fulfill ILS policy |
335 | * |
336 | * @param array $params request parameters |
337 | * |
338 | * @return void |
339 | */ |
340 | protected function validatePasswordUpdate($params) |
341 | { |
342 | // Needs a password |
343 | if (trim($params['password']) == '') { |
344 | throw new AuthException('Password cannot be blank'); |
345 | } |
346 | // Passwords don't match |
347 | if ($params['password'] != $params['password2']) { |
348 | throw new AuthException('Passwords do not match'); |
349 | } |
350 | |
351 | $this->validatePasswordAgainstPolicy($params['password']); |
352 | } |
353 | |
354 | /** |
355 | * Get the Currently Logged-In Patron |
356 | * |
357 | * @throws AuthException |
358 | * |
359 | * @return array|null Patron or null if no credentials exist |
360 | */ |
361 | protected function getLoggedInPatron() |
362 | { |
363 | $patron = $this->authenticator->storedCatalogLogin(); |
364 | return $patron ? $patron : null; |
365 | } |
366 | |
367 | /** |
368 | * Gets the configured username field. |
369 | * |
370 | * @return string |
371 | */ |
372 | protected function getUsernameField() |
373 | { |
374 | $config = $this->getConfig(); |
375 | return $config->Authentication->ILS_username_field ?? 'cat_username'; |
376 | } |
377 | } |