From 20f3ade430c06d87d3a41adeb7fcd6eca4258b05 Mon Sep 17 00:00:00 2001 From: wangyidong Date: Fri, 21 Jul 2023 14:22:41 +0800 Subject: [PATCH] * Rewrite user login page. --- lib/captcha/captcha.class.php | 6 +- module/user/css/login.ui.css | 27 ++++++ module/user/js/login.ui.js | 89 +++++++++++++++++ module/user/ui/login.html.php | 178 ++++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 module/user/css/login.ui.css create mode 100644 module/user/js/login.ui.js create mode 100644 module/user/ui/login.html.php diff --git a/lib/captcha/captcha.class.php b/lib/captcha/captcha.class.php index 8e1da97f5d..32814cbea3 100644 --- a/lib/captcha/captcha.class.php +++ b/lib/captcha/captcha.class.php @@ -135,7 +135,7 @@ class captcha } else { $this->builder = $builder; } - + $this->phrase = is_string($phrase) ? $phrase : $this->builder->build($phrase); } @@ -357,7 +357,7 @@ class captcha $w = $box[2] - $box[0]; $angle = $this->rand(-$this->maxAngle, $this->maxAngle); $offset = $this->rand(-$this->maxOffset, $this->maxOffset); - \imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $symbol); + \imagettftext($image, $size, $angle, (int)$x, (int)($y + $offset), $col, $font, $symbol); $x += $w; } @@ -787,7 +787,7 @@ class PhraseBuilder { return self::doNiceize($str); } - + /** * A static helper to niceize */ diff --git a/module/user/css/login.ui.css b/module/user/css/login.ui.css new file mode 100644 index 0000000000..5962b74a9f --- /dev/null +++ b/module/user/css/login.ui.css @@ -0,0 +1,27 @@ +body {background: #1183fb linear-gradient(-90deg, #0a48d1 0%, #1183fb 100%); background-color: #1183fb;} +#login {max-width: 600px !important; margin: 0 auto; margin-top: 5%; padding: 0 48px;} +#loginPanel {background: #fff; overflow: hidden; box-shadow: 0 0 20px 0 rgba(0,0,0,.1); border-radius: 3px;} +#loginPanel > .header {padding: 20px; border-bottom: 1px #eee solid; position: relative;} +#loginPanel > .header > h2 {font-size: 16px; margin: 0; line-height: 32px; max-width: 83%;} +#loginPanel > .header > .actions {position: absolute; right: 20px; top: 20px;} +#loginPanel > .loginBody {margin: 20px 0;} +#loginPanel > .loginBody .loginExpired {margin-bottom: 8px;} +#loginPanel form {margin-right: 40px;} +#login #info {margin-top: 10px; color: #fff; text-align:center;} +#login #info a {display: inline-block; margin-left: 5px;} +.actions.btn {min-width: 0; width: 85px;} +.langsDropMenu.dropdown-menu {min-width: 85px;} + +.form-actions .btn {min-width: unset;} +.form-actions .resetPassword {box-shadow: unset; background-color: unset; color: unset;} +.form-actions .resetPassword:before {background-color: unset;} + +#logo-box img {margin-left: 40px; width: 130px;} +.captchaBox .input-group-addon {width: 110px; padding: 0px;} + +.showNotice {color: yellow;} +.showNotice:hover {color: yellow;} + +.table-row-extension .alert {margin-bottom: 0px !important;} +.table-row-extension .content {color: #3c4353;} +.table-row-extension .expired-tips {cursor: pointer;} diff --git a/module/user/js/login.ui.js b/module/user/js/login.ui.js new file mode 100644 index 0000000000..530a35405c --- /dev/null +++ b/module/user/js/login.ui.js @@ -0,0 +1,89 @@ +window.adjustPanelPos = function() +{ + let $login = $('#login'); + let bestTop = Math.max(0, Math.floor($(window).height() - $login.outerHeight())/2); + $login.css('margin-top', bestTop); +}; + +window.refreshCapcha = function(obj) +{ + let $this = $(obj) + let captchaLink = $.createLink('misc', 'captcha', "sessionVar=captcha"); + captchaLink += captchaLink.indexOf('?') < 0 ? '?' : '&'; + captchaLink += 's=' + Math.random(); + + $this.attr('src', captchaLink); +}; + +/** + * Show notice for one click package use weak password. + * + * @access public + * @return void + */ +window.showNotice = function() +{ + if(typeof(process4Safe) == 'string') zui.Modal.alert(process4Safe); +}; + +window.switchLang = function(lang) +{ + selectLang(lang); +}; + +adjustPanelPos(); +$(window).on('resize', adjustPanelPos); + +timeoutID = null; +window.safeSubmit = function(e) +{ + let account = $('#account').val().trim(); + let password = $('input#password[type="password"]').val().trim(); + let passwordStrength = computePasswordStrength(password); + + let hasMD5 = typeof(md5) == 'function'; + let referer = $('[name=referer]').val(); + let link = $.createLink('user', 'login'); + let keepLogin = $('#keepLoginon').prop('checked') ? 1 : 0; + let captcha = $('#captcha').length == 1 ? $('#captcha').val() : ''; + let timeout = true; + + clearTimeout(timeoutID); + timeoutID = setTimeout(function() + { + if(timeout) zui.Modal.alert(loginTimeoutTip); + }, 4000); + + $.get($.createLink('user', 'refreshRandom'), function(rand) + { + if(password != '') password = hasMD5 ? md5(md5(password) + rand) : password, + timeout = false; + + $.post(link, + { + "account" : account, + "password" : password, + 'passwordStrength' : passwordStrength, + 'referer' : referer, + 'verifyRand' : rand, + 'keepLogin' : keepLogin, + 'captcha' : captcha + }, + function(data) + { + data = JSON.parse(data); + if(data.result == 'fail') + { + zui.Modal.alert(data.message); + if($('.captchaBox').length == 1) refreshCapcha($('.captchaBox .input-group .input-group-addon img')); + return false; + } + + location.href = data.locate; + }); + }); + + return false; +}; + +document.getElementById("account").focus(); diff --git a/module/user/ui/login.html.php b/module/user/ui/login.html.php new file mode 100644 index 0000000000..4d61ec612a --- /dev/null +++ b/module/user/ui/login.html.php @@ -0,0 +1,178 @@ + + * @package user + * @link https://www.zentao.net + */ +namespace zin; + +if(empty($config->notMd5Pwd)) h::import($config->webRoot . 'js/md5.js', 'js'); + +$resetLink = (isset($this->config->resetPWDByMail) and $this->config->resetPWDByMail) ? inlink('forgetPassword') : inlink('reset'); +$zentaodirName = basename($this->app->getBasePath()); +$clientLang = $app->getClientLang(); +$langItems = array(); +foreach($config->langs as $key => $value) $langItems[] = array('text' => $value, 'data-on' => 'click', 'data-call' => 'switchLang', 'data-params' => $key); + +$pluginTips = ''; +$expiredPlugins = implode('、', $plugins['expired']); +$expiringPlugins = implode('、', $plugins['expiring']); +$expiredTips = sprintf($lang->misc->expiredPluginTips, $expiredPlugins); +$expiringTips = sprintf($lang->misc->expiringPluginTips, $expiringPlugins); +if($expiredPlugins) $pluginTips = $expiredTips; +if($expiringPlugins) $pluginTips = $expiringTips; +if($expiredPlugins and $expiringPlugins) $pluginTips = $expiredTips . $pluginTips; +$pluginTotal = count($plugins['expired']) + count($plugins['expiring']); +$expiredCountTips = sprintf($lang->misc->expiredCountTips, $pluginTips, $pluginTotal); + +$demoUserItems = array(); +if(!empty($this->config->global->showDemoUsers)) +{ + $demoPassword = '123456'; + $md5Password = md5('123456'); + $demoUsers = 'productManager,projectManager,dev1,dev2,dev3,tester1,tester2,tester3,testManager'; + $demoUsers = $this->dao->select('account,password,realname')->from(TABLE_USER)->where('account')->in($demoUsers)->andWhere('deleted')->eq(0)->andWhere('password')->eq($md5Password)->fetchAll('account'); + + $link = inlink('login'); + $link .= strpos($link, '?') !== false ? '&' : '?'; + foreach($demoUsers as $demoAccount => $demoUser) + { + if($demoUser->password != $md5Password) continue; + $demoUserItems[] = a(set::href($link . "account={$demoAccount}&password=" . md5($md5Password . $this->session->rand)), $demoUser->realname); + } +} + +if($unsafeSites and !empty($unsafeSites[$zentaodirName])) +{ + $paths = array(); + $databases = array(); + $isXampp = false; + foreach($unsafeSites as $webRoot => $site) + { + $path = $site['path']; + if(strpos($path, 'xampp') !== false) $isXampp = true; + + $paths[] = $site['path']; + $databases[] = $site['database']; + } + + $process4Safe = $isXampp ? $lang->user->process4DB : $lang->user->process4DIR; + $process4Safe = sprintf($process4Safe, join(' ', $isXampp ? $databases : $paths)); + jsVar('process4Safe', $process4Safe); +} +jsVar('loginTimeoutTip', $lang->user->error->loginTimeoutTip); + +set::zui(true); +div +( + setID('main'), + setClass('no-padding'), + div + ( + setID('login'), + div + ( + setID('loginPanel'), + div + ( + setClass('header'), + h2(setClass('font-bold'), sprintf($lang->welcome, $app->company->name)), + dropdown + ( + setClass('actions btn'), + to('trigger', btn($config->langs[$clientLang])), + to('title', 'Change Language/更换语言/更換語言'), + set::items($langItems), + set::menuClass('langsDropMenu'), + set::staticMenu(true), + set::trigger('hover'), + ) + ), + div + ( + setClass('flex items-start loginBody'), + cell + ( + set::width('1/3'), + setID('logo-box'), + h::img(set::src($config->webRoot . 'theme/default/images/main/' . $this->lang->logoImg)), + ), + cell + ( + set::width('2/3'), + $loginExpired ? p(setClass('text-danger loginExpired'), $lang->user->loginExpired) : null, + form + ( + on::click('#submit', 'safeSubmit'), + formGroup + ( + set::label($lang->user->account), + set::strong(true), + set::control(array('type' => 'text', 'name' => 'account')), + ), + formGroup + ( + set::label($lang->user->password), + set::strong(true), + set::control(array('type' => 'password', 'name' => 'password')), + ), + !empty($this->config->safe->loginCaptcha) ? formGroup + ( + set::label($lang->user->captcha), + div + ( + setClass('captchaBox'), + inputGroup + ( + input(set::name('captcha')), + span(setClass('input-group-addon'), h::img(set::src($this->createLink('misc', 'captcha', "sessionVar=captcha"), on::click('refreshCapcha(e.target)')))), + ) + ) + ) : null, + formGroup + ( + set::label(''), + set::control(array('type' => 'checkList', 'items' => $lang->user->keepLogin, 'name' => 'keepLogin', 'value' => $keepLogin)), + ), + formHidden('referer', $referer), + set::actions(array + ( + array('text' => $lang->login, 'id' => 'submit', 'class' => 'primary'), + array('text' => $lang->user->resetPassword, 'class' => 'resetPassword', 'url' => $resetLink), + )), + ), + ), + ), + (count($plugins['expired']) > 0 || count($plugins['expiring']) > 0) ? div + ( + setClass('table-row-extension'), + div + ( + setID('notice'), + setClass('alert secondary'), + div(setClass('content'), icon(setClass('text-secondary'), 'exclamation-sign'), $expiredCountTips), + ) + ) : null, + empty($demoUsers) ? null : div + ( + span($lang->user->loginWithDemoUser), + demoUserItems, + ), + ), + div + ( + setID('info'), + div + ( + setID('poweredby'), + ($unsafeSites && !empty($unsafeSites[$zentaodirName])) ? div(a(setClass('showNotice'), set::href('###'), on::click('showNotice'), $lang->user->notice4Safe)) : null, + ), + ) + ) +); + +render('pagebase');