4c6905e81bbb9a14885ebe4bcef791acb185b0db
[Packages/TYPO3.CMS.git] / typo3 / sysext / felogin / Classes / Controller / FrontendLoginController.php
1 <?php
2 namespace TYPO3\CMS\Felogin\Controller;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2007-2011 Steffen Kamper <info@sk-typo3.de>
8 * Based on Newloginbox (c) 2002-2004 Kasper Skårhøj <kasper@typo3.com>
9 *
10 * All rights reserved
11 *
12 * This script is part of the TYPO3 project. The TYPO3 project is
13 * free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * The GNU General Public License can be found at
19 * http://www.gnu.org/copyleft/gpl.html.
20 *
21 * This script is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU General Public License for more details.
25 *
26 * This copyright notice MUST APPEAR in all copies of the script!
27 *
28 * The code was adapted from newloginbox, see manual for detailed description
29 ***************************************************************/
30 /**
31 * Plugin 'Website User Login' for the 'felogin' extension.
32 *
33 * @author Steffen Kamper <info@sk-typo3.de>
34 * @package TYPO3
35 * @subpackage felogin
36 */
37 class FrontendLoginController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin {
38
39 // Same as class name
40 /**
41 * @todo Define visibility
42 */
43 public $prefixId = 'tx_felogin_pi1';
44
45 // Path to this script relative to the extension dir.
46 /**
47 * @todo Define visibility
48 */
49 public $scriptRelPath = 'pi1/class.tx_felogin_pi1.php';
50
51 // The extension key.
52 /**
53 * @todo Define visibility
54 */
55 public $extKey = 'felogin';
56
57 public $pi_checkCHash = FALSE;
58
59 public $pi_USER_INT_obj = TRUE;
60
61 // Is user logged in?
62 protected $userIsLoggedIn;
63
64 // holds the template for FE rendering
65 protected $template;
66
67 // upload dir, used for flexform template files
68 protected $uploadDir;
69
70 // URL for the redirect
71 protected $redirectUrl;
72
73 // flag for disable the redirect
74 protected $noRedirect = FALSE;
75
76 // logintype (given as GPvar), possible: login, logout
77 protected $logintype;
78
79 /**
80 * The main method of the plugin
81 *
82 * @param string $content The PlugIn content
83 * @param array $conf The PlugIn configuration
84 * @return string The content that is displayed on the website
85 */
86 public function main($content, $conf) {
87 // Loading TypoScript array into object variable:
88 $this->conf = $conf;
89 $this->uploadDir = 'uploads/tx_felogin/';
90 // Loading default pivars
91 $this->pi_setPiVarDefaults();
92 // Loading language-labels
93 $this->pi_loadLL();
94 // Init FlexForm configuration for plugin:
95 $this->pi_initPIflexForm();
96 $this->mergeflexFormValuesIntoConf();
97 // Get storage PIDs:
98 if ($this->conf['storagePid']) {
99 if (intval($this->conf['recursive'])) {
100 $this->spid = $this->pi_getPidList($this->conf['storagePid'], intval($this->conf['recursive']));
101 } else {
102 $this->spid = $this->conf['storagePid'];
103 }
104 } else {
105 $pids = $GLOBALS['TSFE']->getStorageSiterootPids();
106 $this->spid = $pids['_STORAGE_PID'];
107 }
108 // GPvars:
109 $this->logintype = \TYPO3\CMS\Core\Utility\GeneralUtility::_GP('logintype');
110 $this->referer = $this->validateRedirectUrl(\TYPO3\CMS\Core\Utility\GeneralUtility::_GP('referer'));
111 $this->noRedirect = $this->piVars['noredirect'] || $this->conf['redirectDisable'];
112 // If config.typolinkLinkAccessRestrictedPages is set, the var is return_url
113 $returnUrl = \TYPO3\CMS\Core\Utility\GeneralUtility::_GP('return_url');
114 if ($returnUrl) {
115 $this->redirectUrl = $returnUrl;
116 } else {
117 $this->redirectUrl = \TYPO3\CMS\Core\Utility\GeneralUtility::_GP('redirect_url');
118 }
119 $this->redirectUrl = $this->validateRedirectUrl($this->redirectUrl);
120 // Get Template
121 $templateFile = $this->conf['templateFile'] ? $this->conf['templateFile'] : 'EXT:felogin/template.html';
122 $this->template = $this->cObj->fileResource($templateFile);
123 // Is user logged in?
124 $this->userIsLoggedIn = $GLOBALS['TSFE']->loginUser;
125 // Redirect
126 if (($this->conf['redirectMode'] && !$this->conf['redirectDisable']) && !$this->noRedirect) {
127 $redirectUrl = $this->processRedirect();
128 if (count($redirectUrl)) {
129 $this->redirectUrl = $this->conf['redirectFirstMethod'] ? array_shift($redirectUrl) : array_pop($redirectUrl);
130 } else {
131 $this->redirectUrl = '';
132 }
133 }
134 // What to display
135 $content = '';
136 if ($this->piVars['forgot']) {
137 $content .= $this->showForgot();
138 } elseif ($this->piVars['forgothash']) {
139 $content .= $this->changePassword();
140 } else {
141 if ($this->userIsLoggedIn && !$this->logintype) {
142 $content .= $this->showLogout();
143 } else {
144 $content .= $this->showLogin();
145 }
146 }
147 // Process the redirect
148 if ((($this->logintype === 'login' || $this->logintype === 'logout') && $this->redirectUrl) && !$this->noRedirect) {
149 if (!$GLOBALS['TSFE']->fe_user->cookieId) {
150 $content .= $this->cObj->stdWrap($this->pi_getLL('cookie_warning', '', 1), $this->conf['cookieWarning_stdWrap.']);
151 } else {
152 // Add hook for extra processing before redirect
153 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['beforeRedirect']) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['beforeRedirect'])) {
154 $_params = array(
155 'loginType' => $this->logintype,
156 'redirectUrl' => &$this->redirectUrl
157 );
158 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['beforeRedirect'] as $_funcRef) {
159 if ($_funcRef) {
160 \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($_funcRef, $_params, $this);
161 }
162 }
163 }
164 \TYPO3\CMS\Core\Utility\HttpUtility::redirect($this->redirectUrl);
165 }
166 }
167 // Adds hook for processing of extra item markers / special
168 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['postProcContent']) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['postProcContent'])) {
169 $_params = array(
170 'content' => $content
171 );
172 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['postProcContent'] as $_funcRef) {
173 $content = \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($_funcRef, $_params, $this);
174 }
175 }
176 return $this->conf['wrapContentInBaseClass'] ? $this->pi_wrapInBaseClass($content) : $content;
177 }
178
179 /**
180 * Shows the forgot password form
181 *
182 * @return string Content
183 */
184 protected function showForgot() {
185 $subpart = $this->cObj->getSubpart($this->template, '###TEMPLATE_FORGOT###');
186 $subpartArray = ($linkpartArray = array());
187 $postData = \TYPO3\CMS\Core\Utility\GeneralUtility::_POST($this->prefixId);
188 if ($postData['forgot_email']) {
189 // Get hashes for compare
190 $postedHash = $postData['forgot_hash'];
191 $hashData = $GLOBALS['TSFE']->fe_user->getKey('ses', 'forgot_hash');
192 if ($postedHash === $hashData['forgot_hash']) {
193 $row = FALSE;
194 // Look for user record
195 $data = $GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['forgot_email'], 'fe_users');
196 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid, username, password, email', 'fe_users', (((((('(email=' . $data) . ' OR username=') . $data) . ') AND pid IN (') . $GLOBALS['TYPO3_DB']->cleanIntList($this->spid)) . ') ') . $this->cObj->enableFields('fe_users'));
197 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
198 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
199 }
200 $error = NULL;
201 if ($row) {
202 // Generate an email with the hashed link
203 $error = $this->generateAndSendHash($row);
204 } elseif ($this->conf['exposeNonexistentUserInForgotPasswordDialog']) {
205 $error = $this->pi_getLL('ll_forgot_reset_message_error');
206 }
207 // Generate message
208 if ($error) {
209 $markerArray['###STATUS_MESSAGE###'] = $this->cObj->stdWrap($error, $this->conf['forgotErrorMessage_stdWrap.']);
210 } else {
211 $markerArray['###STATUS_MESSAGE###'] = $this->cObj->stdWrap($this->pi_getLL('ll_forgot_reset_message_emailSent', '', 1), $this->conf['forgotResetMessageEmailSentMessage_stdWrap.']);
212 }
213 $subpartArray['###FORGOT_FORM###'] = '';
214 } else {
215 // Wrong email
216 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('forgot_reset_message', $this->conf['forgotMessage_stdWrap.']);
217 $markerArray['###BACKLINK_LOGIN###'] = '';
218 }
219 } else {
220 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('forgot_reset_message', $this->conf['forgotMessage_stdWrap.']);
221 $markerArray['###BACKLINK_LOGIN###'] = '';
222 }
223 $markerArray['###BACKLINK_LOGIN###'] = $this->getPageLink($this->pi_getLL('ll_forgot_header_backToLogin', '', 1), array());
224 $markerArray['###STATUS_HEADER###'] = $this->getDisplayText('forgot_header', $this->conf['forgotHeader_stdWrap.']);
225 $markerArray['###LEGEND###'] = $this->pi_getLL('legend', $this->pi_getLL('reset_password', '', 1), 1);
226 $markerArray['###ACTION_URI###'] = $this->getPageLink('', array($this->prefixId . '[forgot]' => 1), TRUE);
227 $markerArray['###EMAIL_LABEL###'] = $this->pi_getLL('your_email', '', 1);
228 $markerArray['###FORGOT_PASSWORD_ENTEREMAIL###'] = $this->pi_getLL('forgot_password_enterEmail', '', 1);
229 $markerArray['###FORGOT_EMAIL###'] = $this->prefixId . '[forgot_email]';
230 $markerArray['###SEND_PASSWORD###'] = $this->pi_getLL('reset_password', '', 1);
231 $markerArray['###DATA_LABEL###'] = $this->pi_getLL('ll_enter_your_data', '', 1);
232 $markerArray = array_merge($markerArray, $this->getUserFieldMarkers());
233 // Generate hash
234 $hash = md5($this->generatePassword(3));
235 $markerArray['###FORGOTHASH###'] = $hash;
236 // Set hash in feuser session
237 $GLOBALS['TSFE']->fe_user->setKey('ses', 'forgot_hash', array('forgot_hash' => $hash));
238 return $this->cObj->substituteMarkerArrayCached($subpart, $markerArray, $subpartArray, $linkpartArray);
239 }
240
241 /**
242 * This function checks the hash from link and checks the validity. If it's valid it shows the form for
243 * changing the password and process the change of password after submit, if not valid it returns the error message
244 *
245 * @return string The content.
246 */
247 protected function changePassword() {
248 $subpartArray = ($linkpartArray = array());
249 $done = FALSE;
250 $minLength = intval($this->conf['newPasswordMinLength']) ? intval($this->conf['newPasswordMinLength']) : 6;
251 $subpart = $this->cObj->getSubpart($this->template, '###TEMPLATE_CHANGEPASSWORD###');
252 $markerArray['###STATUS_HEADER###'] = $this->getDisplayText('change_password_header', $this->conf['changePasswordHeader_stdWrap.']);
253 $markerArray['###STATUS_MESSAGE###'] = sprintf($this->getDisplayText('change_password_message', $this->conf['changePasswordMessage_stdWrap.']), $minLength);
254 $markerArray['###BACKLINK_LOGIN###'] = '';
255 $uid = $this->piVars['user'];
256 $piHash = $this->piVars['forgothash'];
257 $hash = explode('|', $piHash);
258 if (intval($uid) == 0) {
259 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('change_password_notvalid_message', $this->conf['changePasswordNotValidMessage_stdWrap.']);
260 $subpartArray['###CHANGEPASSWORD_FORM###'] = '';
261 } else {
262 $user = $this->pi_getRecord('fe_users', intval($uid));
263 $userHash = $user['felogin_forgotHash'];
264 $compareHash = explode('|', $userHash);
265 if ((((!$compareHash || !$compareHash[1]) || $compareHash[0] < time()) || $hash[0] != $compareHash[0]) || md5($hash[1]) != $compareHash[1]) {
266 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('change_password_notvalid_message', $this->conf['changePasswordNotValidMessage_stdWrap.']);
267 $subpartArray['###CHANGEPASSWORD_FORM###'] = '';
268 } else {
269 // All is fine, continue with new password
270 $postData = \TYPO3\CMS\Core\Utility\GeneralUtility::_POST($this->prefixId);
271 if (isset($postData['changepasswordsubmit'])) {
272 if (strlen($postData['password1']) < $minLength) {
273 $markerArray['###STATUS_MESSAGE###'] = sprintf($this->getDisplayText('change_password_tooshort_message', $this->conf['changePasswordTooShortMessage_stdWrap.']), $minLength);
274 } elseif ($postData['password1'] != $postData['password2']) {
275 $markerArray['###STATUS_MESSAGE###'] = sprintf($this->getDisplayText('change_password_notequal_message', $this->conf['changePasswordNotEqualMessage_stdWrap.']), $minLength);
276 } else {
277 $newPass = $postData['password1'];
278 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['password_changed']) {
279 $_params = array(
280 'user' => $user,
281 'newPassword' => $newPass
282 );
283 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['password_changed'] as $_funcRef) {
284 if ($_funcRef) {
285 \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($_funcRef, $_params, $this);
286 }
287 }
288 $newPass = $_params['newPassword'];
289 }
290 // Save new password and clear DB-hash
291 $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery('fe_users', 'uid=' . $user['uid'], array('password' => $newPass, 'felogin_forgotHash' => ''));
292 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('change_password_done_message', $this->conf['changePasswordDoneMessage_stdWrap.']);
293 $done = TRUE;
294 $subpartArray['###CHANGEPASSWORD_FORM###'] = '';
295 $markerArray['###BACKLINK_LOGIN###'] = $this->getPageLink($this->pi_getLL('ll_forgot_header_backToLogin', '', 1), array($this->prefixId . '[redirectReferrer]' => 'off'));
296 }
297 }
298 if (!$done) {
299 // Change password form
300 $markerArray['###ACTION_URI###'] = $this->pi_getPageLink($GLOBALS['TSFE']->id, '', array(
301 $this->prefixId . '[user]' => $user['uid'],
302 $this->prefixId . '[forgothash]' => $piHash
303 ));
304 $markerArray['###LEGEND###'] = $this->pi_getLL('change_password', '', 1);
305 $markerArray['###NEWPASSWORD1_LABEL###'] = $this->pi_getLL('newpassword_label1', '', 1);
306 $markerArray['###NEWPASSWORD2_LABEL###'] = $this->pi_getLL('newpassword_label2', '', 1);
307 $markerArray['###NEWPASSWORD1###'] = $this->prefixId . '[password1]';
308 $markerArray['###NEWPASSWORD2###'] = $this->prefixId . '[password2]';
309 $markerArray['###STORAGE_PID###'] = $this->spid;
310 $markerArray['###SEND_PASSWORD###'] = $this->pi_getLL('change_password', '', 1);
311 $markerArray['###FORGOTHASH###'] = $piHash;
312 }
313 }
314 }
315 return $this->cObj->substituteMarkerArrayCached($subpart, $markerArray, $subpartArray, $linkpartArray);
316 }
317
318 /**
319 * Generates a hashed link and send it with email
320 *
321 * @param array $user Contains user data
322 * @return string Empty string with success, error message with no success
323 */
324 protected function generateAndSendHash($user) {
325 $hours = intval($this->conf['forgotLinkHashValidTime']) > 0 ? intval($this->conf['forgotLinkHashValidTime']) : 24;
326 $validEnd = time() + 3600 * $hours;
327 $validEndString = date($this->conf['dateFormat'], $validEnd);
328 $hash = md5(\TYPO3\CMS\Core\Utility\GeneralUtility::generateRandomBytes(64));
329 $randHash = ($validEnd . '|') . $hash;
330 $randHashDB = ($validEnd . '|') . md5($hash);
331 // Write hash to DB
332 $res = $GLOBALS['TYPO3_DB']->exec_UPDATEquery('fe_users', 'uid=' . $user['uid'], array('felogin_forgotHash' => $randHashDB));
333 // Send hashlink to user
334 $this->conf['linkPrefix'] = -1;
335 $isAbsRelPrefix = !empty($GLOBALS['TSFE']->absRefPrefix);
336 $isBaseURL = !empty($GLOBALS['TSFE']->baseUrl);
337 $isFeloginBaseURL = !empty($this->conf['feloginBaseURL']);
338 $link = $this->pi_getPageLink($GLOBALS['TSFE']->id, '', array(
339 $this->prefixId . '[user]' => $user['uid'],
340 $this->prefixId . '[forgothash]' => $randHash
341 ));
342 // Prefix link if necessary
343 if ($isFeloginBaseURL) {
344 // First priority, use specific base URL
345 // "absRefPrefix" must be removed first, otherwise URL will be prepended twice
346 if (!empty($GLOBALS['TSFE']->absRefPrefix)) {
347 $link = substr($link, strlen($GLOBALS['TSFE']->absRefPrefix));
348 }
349 $link = $this->conf['feloginBaseURL'] . $link;
350 } elseif ($isAbsRelPrefix) {
351 // Second priority
352 // absRefPrefix must not necessarily contain a hostname and URL scheme, so add it if needed
353 $link = \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl($link);
354 } elseif ($isBaseURL) {
355 // Third priority
356 // Add the global base URL to the link
357 $link = $GLOBALS['TSFE']->baseUrlWrap($link);
358 } else {
359 // No prefix is set, return the error
360 return $this->pi_getLL('ll_change_password_nolinkprefix_message');
361 }
362 $msg = sprintf($this->pi_getLL('ll_forgot_validate_reset_password', '', 0), $user['username'], $link, $validEndString);
363 // Add hook for extra processing of mail message
364 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['forgotPasswordMail']) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['forgotPasswordMail'])) {
365 $params = array(
366 'message' => &$msg,
367 'user' => &$user
368 );
369 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['forgotPasswordMail'] as $reference) {
370 if ($reference) {
371 \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($reference, $params, $this);
372 }
373 }
374 }
375 // no RDCT - Links for security reasons
376 $oldSetting = $GLOBALS['TSFE']->config['config']['notification_email_urlmode'];
377 $GLOBALS['TSFE']->config['config']['notification_email_urlmode'] = 0;
378 // Send the email
379 $this->cObj->sendNotifyEmail($msg, $user['email'], '', $this->conf['email_from'], $this->conf['email_fromName'], $this->conf['replyTo']);
380 // Restore settings
381 $GLOBALS['TSFE']->config['config']['notification_email_urlmode'] = $oldSetting;
382 return '';
383 }
384
385 /**
386 * Shows logout form
387 *
388 * @return string The content.
389 */
390 protected function showLogout() {
391 $subpart = $this->cObj->getSubpart($this->template, '###TEMPLATE_LOGOUT###');
392 $subpartArray = ($linkpartArray = array());
393 $markerArray['###STATUS_HEADER###'] = $this->getDisplayText('status_header', $this->conf['logoutHeader_stdWrap.']);
394 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('status_message', $this->conf['logoutMessage_stdWrap.']);
395 $this->cObj->stdWrap($this->flexFormValue('message', 's_status'), $this->conf['logoutMessage_stdWrap.']);
396 $markerArray['###LEGEND###'] = $this->pi_getLL('logout', '', 1);
397 $markerArray['###ACTION_URI###'] = $this->getPageLink('', array(), TRUE);
398 $markerArray['###LOGOUT_LABEL###'] = $this->pi_getLL('logout', '', 1);
399 $markerArray['###NAME###'] = htmlspecialchars($GLOBALS['TSFE']->fe_user->user['name']);
400 $markerArray['###STORAGE_PID###'] = $this->spid;
401 $markerArray['###USERNAME###'] = htmlspecialchars($GLOBALS['TSFE']->fe_user->user['username']);
402 $markerArray['###USERNAME_LABEL###'] = $this->pi_getLL('username', '', 1);
403 $markerArray['###NOREDIRECT###'] = $this->noRedirect ? '1' : '0';
404 $markerArray['###PREFIXID###'] = $this->prefixId;
405 $markerArray = array_merge($markerArray, $this->getUserFieldMarkers());
406 if ($this->redirectUrl) {
407 // Use redirectUrl for action tag because of possible access restricted pages
408 $markerArray['###ACTION_URI###'] = htmlspecialchars($this->redirectUrl);
409 $this->redirectUrl = '';
410 }
411 return $this->cObj->substituteMarkerArrayCached($subpart, $markerArray, $subpartArray, $linkpartArray);
412 }
413
414 /**
415 * Shows login form
416 *
417 * @return string Content
418 */
419 protected function showLogin() {
420 $subpart = $this->cObj->getSubpart($this->template, '###TEMPLATE_LOGIN###');
421 $subpartArray = ($linkpartArray = ($markerArray = array()));
422 $gpRedirectUrl = '';
423 $markerArray['###LEGEND###'] = $this->pi_getLL('oLabel_header_welcome', '', 1);
424 if ($this->logintype === 'login') {
425 if ($this->userIsLoggedIn) {
426 // login success
427 $markerArray['###STATUS_HEADER###'] = $this->getDisplayText('success_header', $this->conf['successHeader_stdWrap.']);
428 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('success_message', $this->conf['successMessage_stdWrap.']);
429 $markerArray = array_merge($markerArray, $this->getUserFieldMarkers());
430 $subpartArray['###LOGIN_FORM###'] = '';
431 // Hook for general actions after after login has been confirmed (by Thomas Danzl <thomas@danzl.org>)
432 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['login_confirmed']) {
433 $_params = array();
434 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['login_confirmed'] as $_funcRef) {
435 if ($_funcRef) {
436 \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($_funcRef, $_params, $this);
437 }
438 }
439 }
440 // show logout form directly
441 if ($this->conf['showLogoutFormAfterLogin']) {
442 $this->redirectUrl = '';
443 return $this->showLogout();
444 }
445 } else {
446 // Hook for general actions on login error
447 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['login_error']) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['login_error'])) {
448 $params = array();
449 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['login_error'] as $funcRef) {
450 if ($funcRef) {
451 \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($funcRef, $params, $this);
452 }
453 }
454 }
455 // login error
456 $markerArray['###STATUS_HEADER###'] = $this->getDisplayText('error_header', $this->conf['errorHeader_stdWrap.']);
457 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('error_message', $this->conf['errorMessage_stdWrap.']);
458 $gpRedirectUrl = \TYPO3\CMS\Core\Utility\GeneralUtility::_GP('redirect_url');
459 }
460 } else {
461 if ($this->logintype === 'logout') {
462 // login form after logout
463 $markerArray['###STATUS_HEADER###'] = $this->getDisplayText('logout_header', $this->conf['logoutHeader_stdWrap.']);
464 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('logout_message', $this->conf['logoutMessage_stdWrap.']);
465 } else {
466 // login form
467 $markerArray['###STATUS_HEADER###'] = $this->getDisplayText('welcome_header', $this->conf['welcomeHeader_stdWrap.']);
468 $markerArray['###STATUS_MESSAGE###'] = $this->getDisplayText('welcome_message', $this->conf['welcomeMessage_stdWrap.']);
469 }
470 }
471 // Hook (used by kb_md5fepw extension by Kraft Bernhard <kraftb@gmx.net>)
472 // This hook allows to call User JS functions.
473 // The methods should also set the required JS functions to get included
474 $onSubmit = '';
475 $extraHidden = '';
476 $onSubmitAr = array();
477 $extraHiddenAr = array();
478 // Check for referer redirect method. if present, save referer in form field
479 if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList($this->conf['redirectMode'], 'referer') || \TYPO3\CMS\Core\Utility\GeneralUtility::inList($this->conf['redirectMode'], 'refererDomains')) {
480 $referer = $this->referer ? $this->referer : \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('HTTP_REFERER');
481 if ($referer) {
482 $extraHiddenAr[] = ('<input type="hidden" name="referer" value="' . htmlspecialchars($referer)) . '" />';
483 if ($this->piVars['redirectReferrer'] === 'off') {
484 $extraHiddenAr[] = ('<input type="hidden" name="' . $this->prefixId) . '[redirectReferrer]" value="off" />';
485 }
486 }
487 }
488 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['loginFormOnSubmitFuncs'])) {
489 $_params = array();
490 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['loginFormOnSubmitFuncs'] as $funcRef) {
491 list($onSub, $hid) = \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($funcRef, $_params, $this);
492 $onSubmitAr[] = $onSub;
493 $extraHiddenAr[] = $hid;
494 }
495 }
496 if (count($onSubmitAr)) {
497 $onSubmit = implode('; ', $onSubmitAr) . '; return true;';
498 }
499 if (count($extraHiddenAr)) {
500 $extraHidden = implode(LF, $extraHiddenAr);
501 }
502 if (!$gpRedirectUrl && $this->redirectUrl) {
503 $gpRedirectUrl = $this->redirectUrl;
504 }
505 // Login form
506 $markerArray['###ACTION_URI###'] = $this->getPageLink('', array(), TRUE);
507 // Used by kb_md5fepw extension...
508 $markerArray['###EXTRA_HIDDEN###'] = $extraHidden;
509 $markerArray['###LEGEND###'] = $this->pi_getLL('login', '', 1);
510 $markerArray['###LOGIN_LABEL###'] = $this->pi_getLL('login', '', 1);
511 // Used by kb_md5fepw extension...
512 $markerArray['###ON_SUBMIT###'] = $onSubmit;
513 $markerArray['###PASSWORD_LABEL###'] = $this->pi_getLL('password', '', 1);
514 $markerArray['###STORAGE_PID###'] = $this->spid;
515 $markerArray['###USERNAME_LABEL###'] = $this->pi_getLL('username', '', 1);
516 $markerArray['###REDIRECT_URL###'] = htmlspecialchars($gpRedirectUrl);
517 $markerArray['###NOREDIRECT###'] = $this->noRedirect ? '1' : '0';
518 $markerArray['###PREFIXID###'] = $this->prefixId;
519 $markerArray = array_merge($markerArray, $this->getUserFieldMarkers());
520 if ($this->flexFormValue('showForgotPassword', 'sDEF') || $this->conf['showForgotPasswordLink']) {
521 $linkpartArray['###FORGOT_PASSWORD_LINK###'] = explode('|', $this->getPageLink('|', array($this->prefixId . '[forgot]' => 1)));
522 $markerArray['###FORGOT_PASSWORD###'] = $this->pi_getLL('ll_forgot_header', '', 1);
523 } else {
524 $subpartArray['###FORGOTP_VALID###'] = '';
525 }
526 // The permanent login checkbox should only be shown if permalogin is not deactivated (-1), not forced to be always active (2) and lifetime is greater than 0
527 if (($this->conf['showPermaLogin'] && \TYPO3\CMS\Core\Utility\GeneralUtility::inList('0,1', $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'])) && $GLOBALS['TYPO3_CONF_VARS']['FE']['lifetime'] > 0) {
528 $markerArray['###PERMALOGIN###'] = $this->pi_getLL('permalogin', '', 1);
529 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 1) {
530 $markerArray['###PERMALOGIN_HIDDENFIELD_ATTRIBUTES###'] = 'disabled="disabled"';
531 $markerArray['###PERMALOGIN_CHECKBOX_ATTRIBUTES###'] = 'checked="checked"';
532 } else {
533 $markerArray['###PERMALOGIN_HIDDENFIELD_ATTRIBUTES###'] = '';
534 $markerArray['###PERMALOGIN_CHECKBOX_ATTRIBUTES###'] = '';
535 }
536 } else {
537 $subpartArray['###PERMALOGIN_VALID###'] = '';
538 }
539 return $this->cObj->substituteMarkerArrayCached($subpart, $markerArray, $subpartArray, $linkpartArray);
540 }
541
542 /**
543 * Process redirect methods. The function searches for a redirect url using all configured methods.
544 *
545 * @return string Redirect url
546 */
547 protected function processRedirect() {
548 $redirect_url = array();
549 if ($this->conf['redirectMode']) {
550 $redirectMethods = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $this->conf['redirectMode'], TRUE);
551 foreach ($redirectMethods as $redirMethod) {
552 if ($GLOBALS['TSFE']->loginUser && $this->logintype === 'login') {
553 // Logintype is needed because the login-page wouldn't be accessible anymore after a login (would always redirect)
554 switch ($redirMethod) {
555 case 'groupLogin':
556 // taken from dkd_redirect_at_login written by Ingmar Schlecht; database-field changed
557 $groupData = $GLOBALS['TSFE']->fe_user->groupData;
558 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('felogin_redirectPid', $GLOBALS['TSFE']->fe_user->usergroup_table, ('felogin_redirectPid<>\'\' AND uid IN (' . implode(',', $groupData['uid'])) . ')');
559 if ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res)) {
560 // take the first group with a redirect page
561 $redirect_url[] = $this->pi_getPageLink($row[0]);
562 }
563 break;
564 case 'userLogin':
565 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('felogin_redirectPid', $GLOBALS['TSFE']->fe_user->user_table, (($GLOBALS['TSFE']->fe_user->userid_column . '=') . $GLOBALS['TSFE']->fe_user->user['uid']) . ' AND felogin_redirectPid<>\'\'');
566 if ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res)) {
567 $redirect_url[] = $this->pi_getPageLink($row[0]);
568 }
569 break;
570 case 'login':
571 if ($this->conf['redirectPageLogin']) {
572 $redirect_url[] = $this->pi_getPageLink(intval($this->conf['redirectPageLogin']));
573 }
574 break;
575 case 'getpost':
576 $redirect_url[] = $this->redirectUrl;
577 break;
578 case 'referer':
579 // Avoid redirect when logging in after changing password
580 if ($this->piVars['redirectReferrer'] !== 'off') {
581 // Avoid forced logout, when trying to login immediately after a logout
582 $redirect_url[] = preg_replace('/[&?]logintype=[a-z]+/', '', $this->referer);
583 }
584 break;
585 case 'refererDomains':
586 // Auto redirect.
587 // Feature to redirect to the page where the user came from (HTTP_REFERER).
588 // Allowed domains to redirect to, can be configured with plugin.tx_felogin_pi1.domains
589 // Thanks to plan2.net / Martin Kutschker for implementing this feature.
590 // also avoid redirect when logging in after changing password
591 if ($this->conf['domains'] && $this->piVars['redirectReferrer'] !== 'off') {
592 $url = $this->referer;
593 // Is referring url allowed to redirect?
594 $match = array();
595 if (preg_match('/^http://([[:alnum:]._-]+)//', $url, $match)) {
596 $redirect_domain = $match[1];
597 $found = FALSE;
598 foreach (\TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $this->conf['domains'], TRUE) as $d) {
599 if (preg_match(('/(^|\\.)/' . $d) . '$', $redirect_domain)) {
600 $found = TRUE;
601 break;
602 }
603 }
604 if (!$found) {
605 $url = '';
606 }
607 }
608 // Avoid forced logout, when trying to login immediately after a logout
609 if ($url) {
610 $redirect_url[] = preg_replace('/[&?]logintype=[a-z]+/', '', $url);
611 }
612 }
613 break;
614 }
615 } elseif ($this->logintype === 'login') {
616 // after login-error
617 switch ($redirMethod) {
618 case 'loginError':
619 if ($this->conf['redirectPageLoginError']) {
620 $redirect_url[] = $this->pi_getPageLink(intval($this->conf['redirectPageLoginError']));
621 }
622 break;
623 }
624 } elseif (($this->logintype == '' && $redirMethod == 'login') && $this->conf['redirectPageLogin']) {
625 // If login and page not accessible
626 $this->cObj->typolink('', array(
627 'parameter' => $this->conf['redirectPageLogin'],
628 'linkAccessRestrictedPages' => TRUE
629 ));
630 $redirect_url[] = $this->cObj->lastTypoLinkUrl;
631 } elseif ((($this->logintype == '' && $redirMethod == 'logout') && $this->conf['redirectPageLogout']) && $GLOBALS['TSFE']->loginUser) {
632 // If logout and page not accessible
633 $redirect_url[] = $this->pi_getPageLink(intval($this->conf['redirectPageLogout']));
634 } elseif ($this->logintype === 'logout') {
635 // after logout
636 // Hook for general actions after after logout has been confirmed
637 if ($this->logintype === 'logout' && $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['logout_confirmed']) {
638 $_params = array();
639 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['felogin']['logout_confirmed'] as $_funcRef) {
640 if ($_funcRef) {
641 \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($_funcRef, $_params, $this);
642 }
643 }
644 }
645 switch ($redirMethod) {
646 case 'logout':
647 if ($this->conf['redirectPageLogout']) {
648 $redirect_url[] = $this->pi_getPageLink(intval($this->conf['redirectPageLogout']));
649 }
650 break;
651 }
652 } else {
653 // not logged in
654 // Placeholder for maybe future options
655 switch ($redirMethod) {
656 case 'getpost':
657 // Preserve the get/post value
658 $redirect_url[] = $this->redirectUrl;
659 break;
660 }
661 }
662 }
663 }
664 // Remove empty values
665 if (count($redirect_url)) {
666 return \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', implode(',', $redirect_url), TRUE);
667 } else {
668 return array();
669 }
670 }
671
672 /**
673 * Reads flexform configuration and merge it with $this->conf
674 *
675 * @return void
676 */
677 protected function mergeflexFormValuesIntoConf() {
678 $flex = array();
679 if ($this->flexFormValue('showForgotPassword', 'sDEF')) {
680 $flex['showForgotPassword'] = $this->flexFormValue('showForgotPassword', 'sDEF');
681 }
682 if ($this->flexFormValue('showPermaLogin', 'sDEF')) {
683 $flex['showPermaLogin'] = $this->flexFormValue('showPermaLogin', 'sDEF');
684 }
685 if ($this->flexFormValue('showLogoutFormAfterLogin', 'sDEF')) {
686 $flex['showLogoutFormAfterLogin'] = $this->flexFormValue('showLogoutFormAfterLogin', 'sDEF');
687 }
688 if ($this->flexFormValue('pages', 'sDEF')) {
689 $flex['pages'] = $this->flexFormValue('pages', 'sDEF');
690 }
691 if ($this->flexFormValue('recursive', 'sDEF')) {
692 $flex['recursive'] = $this->flexFormValue('recursive', 'sDEF');
693 }
694 if ($this->flexFormValue('templateFile', 'sDEF')) {
695 $flex['templateFile'] = $this->uploadDir . $this->flexFormValue('templateFile', 'sDEF');
696 }
697 if ($this->flexFormValue('redirectMode', 's_redirect')) {
698 $flex['redirectMode'] = $this->flexFormValue('redirectMode', 's_redirect');
699 }
700 if ($this->flexFormValue('redirectFirstMethod', 's_redirect')) {
701 $flex['redirectFirstMethod'] = $this->flexFormValue('redirectFirstMethod', 's_redirect');
702 }
703 if ($this->flexFormValue('redirectDisable', 's_redirect')) {
704 $flex['redirectDisable'] = $this->flexFormValue('redirectDisable', 's_redirect');
705 }
706 if ($this->flexFormValue('redirectPageLogin', 's_redirect')) {
707 $flex['redirectPageLogin'] = $this->flexFormValue('redirectPageLogin', 's_redirect');
708 }
709 if ($this->flexFormValue('redirectPageLoginError', 's_redirect')) {
710 $flex['redirectPageLoginError'] = $this->flexFormValue('redirectPageLoginError', 's_redirect');
711 }
712 if ($this->flexFormValue('redirectPageLogout', 's_redirect')) {
713 $flex['redirectPageLogout'] = $this->flexFormValue('redirectPageLogout', 's_redirect');
714 }
715 $pid = $flex['pages'] ? $this->pi_getPidList($flex['pages'], $flex['recursive']) : 0;
716 if ($pid > 0) {
717 $flex['storagePid'] = $pid;
718 }
719 $this->conf = array_merge($this->conf, $flex);
720 }
721
722 /**
723 * Loads a variable from the flexform
724 *
725 * @param string $var Name of variable
726 * @param string $sheet Name of sheet
727 * @return string Value of var
728 */
729 protected function flexFormValue($var, $sheet) {
730 return $this->pi_getFFvalue($this->cObj->data['pi_flexform'], $var, $sheet);
731 }
732
733 /**
734 * Generate link with typolink function
735 *
736 * @param string $label Linktext
737 * @param array $piVars Link vars
738 * @param boolean $returnUrl TRUE: returns only url FALSE (default) returns the link)
739 * @return string Link or url
740 */
741 protected function getPageLink($label, $piVars, $returnUrl = FALSE) {
742 $additionalParams = '';
743 if (count($piVars)) {
744 foreach ($piVars as $key => $val) {
745 $additionalParams .= (('&' . $key) . '=') . $val;
746 }
747 }
748 // Should GETvars be preserved?
749 if ($this->conf['preserveGETvars']) {
750 $additionalParams .= $this->getPreserveGetVars();
751 }
752 $this->conf['linkConfig.']['parameter'] = $GLOBALS['TSFE']->id;
753 if ($additionalParams) {
754 $this->conf['linkConfig.']['additionalParams'] = $additionalParams;
755 }
756 if ($returnUrl) {
757 return htmlspecialchars($this->cObj->typolink_url($this->conf['linkConfig.']));
758 } else {
759 return $this->cObj->typolink($label, $this->conf['linkConfig.']);
760 }
761 }
762
763 /**
764 * Is used by TS-setting preserveGETvars
765 * possible values are "all" or a commaseperated list of GET-vars
766 * they are used as additionalParams for link generation
767 *
768 * @return string additionalParams-string
769 */
770 protected function getPreserveGetVars() {
771 $params = '';
772 $preserveVars = !($this->conf['preserveGETvars'] || $this->conf['preserveGETvars'] == 'all' ? array() : implode(',', (array) $this->conf['preserveGETvars']));
773 $getVars = \TYPO3\CMS\Core\Utility\GeneralUtility::_GET();
774 foreach ($getVars as $key => $val) {
775 if (stristr($key, $this->prefixId) === FALSE) {
776 if (is_array($val)) {
777 foreach ($val as $key1 => $val1) {
778 if ($this->conf['preserveGETvars'] == 'all' || in_array((($key . '[') . $key1) . ']', $preserveVars)) {
779 $params .= (((('&' . $key) . '[') . $key1) . ']=') . $val1;
780 }
781 }
782 } else {
783 if (!in_array($key, array('id', 'no_cache', 'logintype', 'redirect_url', 'cHash'))) {
784 $params .= (('&' . $key) . '=') . $val;
785 }
786 }
787 }
788 }
789 return $params;
790 }
791
792 /**
793 * Is used by forgot password - function with md5 option.
794 *
795 * @author Bernhard Kraft
796 * @param integer $len Length of new password
797 * @return string New password
798 */
799 protected function generatePassword($len) {
800 $pass = '';
801 while ($len--) {
802 $char = rand(0, 35);
803 if ($char < 10) {
804 $pass .= '' . $char;
805 } else {
806 $pass .= chr(($char - 10) + 97);
807 }
808 }
809 return $pass;
810 }
811
812 /**
813 * Returns the header / message value from flexform if present, else from locallang.xml
814 *
815 * @param string $label label name
816 * @param string $stdWrapArray TS stdWrap array
817 * @return string label text
818 */
819 protected function getDisplayText($label, $stdWrapArray = array()) {
820 $text = $this->flexFormValue($label, 's_messages') ? $this->cObj->stdWrap($this->flexFormValue($label, 's_messages'), $stdWrapArray) : $this->cObj->stdWrap($this->pi_getLL('ll_' . $label, '', 1), $stdWrapArray);
821 $replace = $this->getUserFieldMarkers();
822 return strtr($text, $replace);
823 }
824
825 /**
826 * Returns Array of markers filled with user fields
827 *
828 * @return array Marker array
829 */
830 protected function getUserFieldMarkers() {
831 $marker = array();
832 // replace markers with fe_user data
833 if ($GLOBALS['TSFE']->fe_user->user) {
834 // All fields of fe_user will be replaced, scheme is ###FEUSER_FIELDNAME###
835 foreach ($GLOBALS['TSFE']->fe_user->user as $field => $value) {
836 $marker[('###FEUSER_' . \TYPO3\CMS\Core\Utility\GeneralUtility::strtoupper($field)) . '###'] = $this->cObj->stdWrap($value, $this->conf['userfields.'][$field . '.']);
837 }
838 // Add ###USER### for compatibility
839 $marker['###USER###'] = $marker['###FEUSER_USERNAME###'];
840 }
841 return $marker;
842 }
843
844 /**
845 * Returns a valid and XSS cleaned url for redirect, checked against configuration "allowedRedirectHosts"
846 *
847 * @param string $url
848 * @return string cleaned referer or empty string if not valid
849 */
850 protected function validateRedirectUrl($url) {
851 $url = strval($url);
852 if ($url === '') {
853 return '';
854 }
855 $decodedUrl = rawurldecode($url);
856 $sanitizedUrl = \TYPO3\CMS\Core\Utility\GeneralUtility::removeXSS($decodedUrl);
857 if ($decodedUrl !== $sanitizedUrl || preg_match('#["<>\\\\]+#', $url)) {
858 \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog(sprintf($this->pi_getLL('xssAttackDetected'), $url), 'felogin', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_WARNING);
859 return '';
860 }
861 // Validate the URL:
862 if (($this->isRelativeUrl($url) || $this->isInCurrentDomain($url)) || $this->isInLocalDomain($url)) {
863 return $url;
864 }
865 // URL is not allowed
866 \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog(sprintf($this->pi_getLL('noValidRedirectUrl'), $url), 'felogin', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_WARNING);
867 return '';
868 }
869
870 /**
871 * Determines whether the URL is on the current host
872 * and belongs to the current TYPO3 installation.
873 *
874 * @param string $url URL to be checked
875 * @return boolean Whether the URL belongs to the current TYPO3 installation
876 */
877 protected function isInCurrentDomain($url) {
878 return \TYPO3\CMS\Core\Utility\GeneralUtility::isOnCurrentHost($url) && \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($url, \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_URL'));
879 }
880
881 /**
882 * Determines whether the URL matches a domain
883 * in the sys_domain database table.
884 *
885 * @param string $url Absolute URL which needs to be checked
886 * @return boolean Whether the URL is considered to be local
887 */
888 protected function isInLocalDomain($url) {
889 $result = FALSE;
890 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isValidUrl($url)) {
891 $parsedUrl = parse_url($url);
892 if ($parsedUrl['scheme'] === 'http' || $parsedUrl['scheme'] === 'https') {
893 $host = $parsedUrl['host'];
894 // Removes the last path segment and slash sequences like /// (if given):
895 $path = preg_replace('#/+[^/]*$#', '', $parsedUrl['path']);
896 $localDomains = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('domainName', 'sys_domain', '1=1' . $this->cObj->enableFields('sys_domain'));
897 if (is_array($localDomains)) {
898 foreach ($localDomains as $localDomain) {
899 // strip trailing slashes (if given)
900 $domainName = rtrim($localDomain['domainName'], '/');
901 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr(($host . $path) . '/', $domainName . '/')) {
902 $result = TRUE;
903 break;
904 }
905 }
906 }
907 }
908 }
909 return $result;
910 }
911
912 /**
913 * Determines whether the URL is relative to the
914 * current TYPO3 installation.
915 *
916 * @param string $url URL which needs to be checked
917 * @return boolean Whether the URL is considered to be relative
918 */
919 protected function isRelativeUrl($url) {
920 $parsedUrl = @parse_url($url);
921 if (($parsedUrl !== FALSE && !isset($parsedUrl['scheme'])) && !isset($parsedUrl['host'])) {
922 // If the relative URL starts with a slash, we need to check if it's within the current site path
923 return !\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($parsedUrl['path'], '/') || \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($parsedUrl['path'], \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
924 }
925 return FALSE;
926 }
927
928 }
929
930
931 ?>