<parent>wcf.acp.menu.link.log</parent>
<permissions>admin.system.canViewLog</permissions>
</acpmenuitem>
+
+ <acpmenuitem name="wcf.acp.menu.link.log.authentication.failure">
+ <controller><![CDATA[wcf\acp\page\UserAuthenticationFailureListPage]]></controller>
+ <parent>wcf.acp.menu.link.log</parent>
+ <permissions>admin.system.canViewLog</permissions>
+ <options>enable_user_authentication_failure</options>
+ </acpmenuitem>
<!-- /log -->
<acpmenuitem name="wcf.acp.menu.link.user">
<category name="security.general.session">
<parent>security.general</parent>
</category>
+ <category name="security.general.authentication">
+ <parent>security.general</parent>
+ </category>
<category name="security.blacklist">
<parent>security</parent>
</category>
</option>
<!-- /general.system.proxy -->
- <!-- general.session -->
+ <!-- security.general.session -->
<option name="session_timeout">
<categoryname>security.general.session</categoryname>
<optiontype>integer</optiontype>
<optiontype>boolean</optiontype>
<defaultvalue>1</defaultvalue>
</option>
- <!-- /general.session -->
+ <!-- /security.general.session -->
+
+ <!-- security.general.authentication -->
+ <option name="enable_user_authentication_failure">
+ <categoryname>security.general.authentication</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ <enableoptions>user_authentication_failure_timeout,user_authentication_failure_ip_captcha,user_authentication_failure_ip_block,user_authentication_failure_user_captcha,user_authentication_failure_expiration</enableoptions>
+ </option>
+ <option name="user_authentication_failure_timeout">
+ <categoryname>security.general.authentication</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>7200</defaultvalue>
+ <minvalue>300</minvalue>
+ <maxvalue>86400</maxvalue>
+ </option>
+ <option name="user_authentication_failure_ip_captcha">
+ <categoryname>security.general.authentication</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>3</defaultvalue>
+ <minvalue>0</minvalue>
+ </option>
+ <option name="user_authentication_failure_ip_block">
+ <categoryname>security.general.authentication</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>10</defaultvalue>
+ <minvalue>0</minvalue>
+ </option>
+ <option name="user_authentication_failure_user_captcha">
+ <categoryname>security.general.authentication</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>3</defaultvalue>
+ <minvalue>0</minvalue>
+ </option>
+ <option name="user_authentication_failure_expiration">
+ <categoryname>security.general.authentication</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>30</defaultvalue>
+ <minvalue>1</minvalue>
+ <maxvalue>365</maxvalue>
+ </option>
+ <!-- /security.general.authentication -->
<!-- security.blacklist -->
<option name="blacklist_ip_addresses">
{/hascontent}
{event name='fieldsets'}
+
+ {include file='captcha'}
</div>
<div class="formSubmit">
--- /dev/null
+{if $captchaObjectType}
+ {@$captchaObjectType->getProcessor()->getFormElement()}
+{/if}
--- /dev/null
+<input type="hidden" name="captchaQuestion" value="{$captchaQuestion}" />
+
+{if !$captchaQuestionAnswered}
+ <fieldset>
+ <legend>{lang}wcf.captcha.question.captcha{/lang}</legend>
+ <small>{lang}wcf.captcha.question.captcha.description{/lang}</small>
+
+ <dl{if (($errorType|isset && $errorType|is_array && $errorType[captchaAnswer]|isset) || ($errorField|isset && $errorField == 'captchaAnswer'))} class="formError"{/if}>
+ <dt><label for="captchaAnswer">{lang}{$captchaQuestionObject->question}{/lang}</label></dt>
+ <dd>
+ <input type="text" id="captchaAnswer" name="captchaAnswer" class="medium" />
+ {if (($errorType|isset && $errorType|is_array && $errorType[captchaAnswer]|isset) || ($errorField|isset && $errorField == 'captchaAnswer'))}
+ {if $errorType|is_array && $errorType[captchaAnswer]|isset}
+ {assign var='__errorType' value=$errorType[captchaAnswer]}
+ {else}
+ {assign var='__errorType' value=$errorType}
+ {/if}
+
+ {if $__errorType == 'empty'}
+ <small class="innerError">{lang}wcf.global.form.error.empty{/lang}</small>
+ {else}
+ <small class="innerError">{lang}wcf.captcha.question.answer.error.{$__errorType}{/lang}</small>
+ {/if}
+ {/if}
+ </dd>
+ </dl>
+ </fieldset>
+
+ {if !$ajaxCaptcha|empty}
+ <script data-relocate="true">
+ //<![CDATA[
+ $(function() {
+ WCF.System.Captcha.addCallback('{$captchaID}', function() {
+ return {
+ captchaAnswer: $('#captchaAnswer').val(),
+ captchaQuestion: '{$captchaQuestion}'
+ };
+ });
+ });
+ //]]>
+ </script>
+ {/if}
+{/if}
</fieldset>
{event name='fieldsets'}
+
+ {include file='captcha'}
</div>
<div class="formSubmit">
-<fieldset>
- <legend><label for="recaptcha_response_field">{lang}wcf.recaptcha.title{/lang}</label></legend>
- <small>{lang}wcf.recaptcha.description{/lang}</small>
-
- <dl class="wide reCaptcha{if $errorField == 'recaptchaString'} formError{/if}">
- <script data-relocate="true">
- //<![CDATA[
- var RecaptchaOptions = {
- lang: '{@$recaptchaLanguageCode}',
- theme : 'custom'
- }
- //]]>
- </script>
- <dt class="jsOnly">
- <label for="recaptcha_response_field">reCAPTCHA</label>
- </dt>
- <dd class="jsOnly">
- <div id="recaptcha_image" class="framed"></div>
- <input type="text" id="recaptcha_response_field" name="recaptcha_response_field" class="medium marginTop" />
- {if $errorField == 'recaptchaString'}
- <small class="innerError">
- {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if}
- {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if}
- </small>
- {/if}
- </dd>
-
- {event name='fields'}
-
- <dd class="jsOnly">
- <ul class="buttonList smallButtons">
- <li><a href="javascript:Recaptcha.reload()" class="button small"><span class="icon icon16 icon-repeat"></span> <span>{lang}wcf.recaptcha.reload{/lang}</span></a></li>
- <li class="recaptcha_only_if_image"><a href="javascript:Recaptcha.switch_type('audio')" class="button small"><span class="icon icon16 icon-volume-up"></span> <span>{lang}wcf.recaptcha.audio{/lang}</span></a></li>
- <li class="recaptcha_only_if_audio"><a href="javascript:Recaptcha.switch_type('image')" class="button small"><span class="icon icon16 icon-eye-open"></span> <span>{lang}wcf.recaptcha.image{/lang}</span></a></li>
- <li><a href="javascript:Recaptcha.showhelp()" class="button small"><span class="icon icon16 icon-question-sign"></span> <span>{lang}wcf.recaptcha.help{/lang}</span></a></li>
- {event name='buttons'}
- </ul>
- </dd>
+{if $recaptchaLegacyMode|empty}
+ {include file='captcha'}
+{else}
+ <fieldset>
+ <legend><label for="recaptcha_response_field">{lang}wcf.recaptcha.title{/lang}</label></legend>
+ <small>{lang}wcf.recaptcha.description{/lang}</small>
- <script data-relocate="true" src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/challenge?k={$recaptchaPublicKey}"></script>
- <noscript>
- <dd>
- <iframe src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/noscript?k={$recaptchaPublicKey}" height="300" width="500" seamless="seamless"></iframe><br />
- <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
- <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
+ <dl class="wide reCaptcha{if $errorField|isset && $errorField == 'recaptchaString'} formError{/if}">
+ {if !$ajaxCaptcha|isset || !$ajaxCaptcha}
+ <script data-relocate="true">
+ //<![CDATA[
+ var RecaptchaOptions = {
+ lang: '{@$recaptchaLanguageCode}',
+ theme : 'custom'
+ }
+ //]]>
+ </script>
+ {/if}
+ <dt class="jsOnly">
+ <label for="recaptcha_response_field">reCAPTCHA</label>
+ </dt>
+ <dd class="jsOnly">
+ <div id="recaptcha_image" class="framed"></div>
+ <input type="text" id="recaptcha_response_field" name="recaptcha_response_field" class="medium marginTop" />
+ {if (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
+ {if $errorType|is_array && $errorType[recaptchaString]|isset}
+ {assign var='__errorType' value=$errorType[recaptchaString]}
+ {else}
+ {assign var='__errorType' value=$errorType}
+ {/if}
+ <small class="innerError">
+ {if $__errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.recaptcha.error.recaptchaString.{$__errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+
+ {event name='fields'}
+
+ <dd class="jsOnly">
+ <ul class="buttonList smallButtons">
+ <li><a href="javascript:Recaptcha.reload()" class="button small"><span class="icon icon16 icon-repeat"></span> <span>{lang}wcf.recaptcha.reload{/lang}</span></a></li>
+ <li class="recaptcha_only_if_image"><a href="javascript:Recaptcha.switch_type('audio')" class="button small"><span class="icon icon16 icon-volume-up"></span> <span>{lang}wcf.recaptcha.audio{/lang}</span></a></li>
+ <li class="recaptcha_only_if_audio"><a href="javascript:Recaptcha.switch_type('image')" class="button small"><span class="icon icon16 icon-eye-open"></span> <span>{lang}wcf.recaptcha.image{/lang}</span></a></li>
+ <li><a href="javascript:Recaptcha.showhelp()" class="button small"><span class="icon icon16 icon-question-sign"></span> <span>{lang}wcf.recaptcha.help{/lang}</span></a></li>
+ {event name='buttons'}
+ </ul>
</dd>
- {if $errorField == 'recaptchaString'}
- <small class="innerError">
- {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if}
- {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if}
- </small>
+
+ {if !$ajaxCaptcha|isset || !$ajaxCaptcha}
+ <script data-relocate="true" src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/challenge?k={$recaptchaPublicKey}"></script>
+ <noscript>
+ <dd>
+ <iframe src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/noscript?k={$recaptchaPublicKey}" height="300" width="500" seamless="seamless"></iframe><br />
+ <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
+ <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
+ </dd>
+ {if (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
+ {if $errorType|is_array && $errorType[recaptchaString]|isset}
+ {assign var='__errorType' value=$errorType[recaptchaString]}
+ {else}
+ {assign var='__errorType' value=$errorType}
+ {/if}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.recaptcha.error.recaptchaString.{$__errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </noscript>
+ {else}
+ <script data-relocate="true">
+ //<![CDATA[
+ Recaptcha.create("{$recaptchaPublicKey}", "recaptcha_image", {
+ lang: '{@$recaptchaLanguageCode}',
+ theme : 'custom'
+ });
+
+ WCF.System.Captcha.addCallback('{$captchaID}', function() {
+ return {
+ recaptcha_challenge_field: Recaptcha.get_challenge(),
+ recaptcha_response_field: Recaptcha.get_response()
+ };
+ });
+ //]]>
+ </script>
{/if}
- </noscript>
- </dl>
-</fieldset>
+ </dl>
+ </fieldset>
+{/if}
--- /dev/null
+{include file='header' pageTitle='wcf.acp.user.authentication.failure.list'}
+
+<header class="boxHeadline">
+ <h1>{lang}wcf.acp.user.authentication.failure.list{/lang}</h1>
+</header>
+
+<div class="contentNavigation">
+ {pages print=true assign=pagesLinks controller='UserAuthenticationFailureList' link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}
+
+ {hascontent}
+ <nav>
+ <ul>
+ {content}
+ {event name='contentNavigationButtonsTop'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</div>
+
+{if $objects|count}
+ <div class="tabularBox tabularBoxTitle marginTop">
+ <header>
+ <h2>{lang}wcf.acp.user.authentication.failure.list{/lang} <span class="badge badgeInverse">{#$items}</span></h2>
+ </header>
+
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID columnFailureID{if $sortField == 'failureID'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=failureID&sortOrder={if $sortField == 'failureID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+ <th class="columnText columnEnvironment{if $sortField == 'environment'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=environment&sortOrder={if $sortField == 'environment' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.user.authentication.failure.environment{/lang}</a></th>
+ <th class="columnTitle columnUsername{if $sortField == 'username'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=username&sortOrder={if $sortField == 'username' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.user.username{/lang}</a></th>
+ <th class="columnDate columnTime{if $sortField == 'time'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=time&sortOrder={if $sortField == 'time' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.user.authentication.failure.time{/lang}</a></th>
+ <th class="columnURL columnIpAddress{if $sortField == 'ipAddress'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=ipAddress&sortOrder={if $sortField == 'ipAddress' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.user.ipAddress{/lang}</a></th>
+ <th class="columnText columnUserAgent{if $sortField == 'userAgent'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=userAgent&sortOrder={if $sortField == 'userAgent' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.user.userAgent{/lang}</a></th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {foreach from=$objects item='authenticationFailure'}
+ <tr>
+ <td class="columnID columnFailureID">{@$authenticationFailure->failureID}</td>
+ <td class="columnText columnEnvironment">{lang}wcf.acp.user.authentication.failure.environment.{@$authenticationFailure->environment}{/lang}</td>
+ <td class="columnTitle columnUsername">{if $authenticationFailure->userID}<a href="{link controller='UserEdit' id=$authenticationFailure->userID}{/link}">{$authenticationFailure->username}</a>{else}{$authenticationFailure->username}{/if}</td>
+ <td class="columnDate columnTime">{@$authenticationFailure->time|time}</td>
+ <td class="columnSmallText columnIpAddress">{$authenticationFailure->getIpAddress()}</td>
+ <td class="columnSmallText columnUserAgent" title="{$authenticationFailure->userAgent}">{$authenticationFailure->userAgent|truncate:75|tableWordwrap}</td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ </div>
+
+ <div class="contentNavigation">
+ {@$pagesLinks}
+
+ {hascontent}
+ <nav>
+ <ul>
+ {content}
+ {event name='contentNavigationButtonsBottom'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+ </div>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='footer'}
<?php
namespace wcf\acp\form;
+use wcf\data\user\authentication\failure\UserAuthenticationFailure;
+use wcf\data\user\authentication\failure\UserAuthenticationFailureAction;
use wcf\data\user\User;
-use wcf\form\AbstractForm;
+use wcf\form\AbstractCaptchaForm;
use wcf\system\application\ApplicationHandler;
+use wcf\system\exception\NamedUserException;
use wcf\system\exception\PermissionDeniedException;
use wcf\system\exception\UserInputException;
use wcf\system\request\RequestHandler;
use wcf\system\WCF;
use wcf\util\HeaderUtil;
use wcf\util\StringUtil;
+use wcf\util\UserUtil;
/**
* Shows the acp login form.
* @subpackage acp.form
* @category Community Framework
*/
-class LoginForm extends AbstractForm {
+class LoginForm extends AbstractCaptchaForm {
/**
* given login username
* @var string
* user object
* @var \wcf\data\user\User
*/
- public $user;
+ public $user = null;
/**
* given forward url
*/
public $url = null;
+ /**
+ * @todo
+ * @var unknown
+ */
+ public $useCaptcha = false;
+
/**
* Creates a new LoginForm object.
*/
$this->url = '';
}
}
+
+ // check authentication failures
+ if (ENABLE_USER_AUTHENTICATION_FAILURE) {
+ $failures = UserAuthenticationFailure::countIPFailures(UserUtil::getIpAddress());
+ if (USER_AUTHENTICATION_FAILURE_IP_BLOCK && $failures >= USER_AUTHENTICATION_FAILURE_IP_BLOCK) {
+ throw new NamedUserException(WCF::getLanguage()->getDynamicVariable('wcf.user.login.blocked'));
+ }
+ if (USER_AUTHENTICATION_FAILURE_IP_CAPTCHA && $failures >= USER_AUTHENTICATION_FAILURE_IP_CAPTCHA) {
+ $this->captchaObjectTypeName = REGISTER_CAPTCHA_TYPE;
+ }
+ else if (USER_AUTHENTICATION_FAILURE_USER_CAPTCHA) {
+ if (isset($_POST['username'])) {
+ $user = User::getUserByUsername(StringUtil::trim($_POST['username']));
+ if (!$user->userID) $user = User::getUserByEmail(StringUtil::trim($_POST['username']));
+
+ if ($user->userID) {
+ $failures = UserAuthenticationFailure::countUserFailures($user->userID);
+ if (USER_AUTHENTICATION_FAILURE_USER_CAPTCHA && $failures >= USER_AUTHENTICATION_FAILURE_USER_CAPTCHA) {
+ $this->captchaObjectTypeName = REGISTER_CAPTCHA_TYPE;
+ }
+ }
+ }
+ }
+ }
}
/**
}
}
+ /**
+ * @see \wcf\form\IForm::submit()
+ */
+ public function submit() {
+ parent::submit();
+
+ // save authentication failure
+ if (ENABLE_USER_AUTHENTICATION_FAILURE) {
+ if ($this->errorField == 'username' || $this->errorField == 'password') {
+ $action = new UserAuthenticationFailureAction(array(), 'create', array(
+ 'data' => array(
+ 'environment' => (RequestHandler::getInstance()->isACPRequest() ? 'admin' : 'user'),
+ 'userID' => ($this->user !== null ? $this->user->userID : null),
+ 'username' => $this->username,
+ 'time' => TIME_NOW,
+ 'ipAddress' => UserUtil::getIpAddress(),
+ 'userAgent' => UserUtil::getUserAgent()
+ )
+ ));
+ $action->executeAction();
+
+ if ($this->captchaObjectType) {
+ $this->captchaObjectType->getProcessor()->reset();
+ }
+ }
+ }
+ }
+
/**
* @see \wcf\form\IForm::validate()
*/
--- /dev/null
+<?php
+namespace wcf\acp\page;
+use wcf\page\SortablePage;
+
+/**
+ * Shows a list of user authentication failures.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2014 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage acp.page
+ * @category Community Framework
+ */
+class UserAuthenticationFailureListPage extends SortablePage {
+ /**
+ * @see \wcf\page\AbstractPage::$activeMenuItem
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.log.authentication.failure';
+
+ /**
+ * @see \wcf\page\AbstractPage::$neededPermissions
+ */
+ public $neededPermissions = array('admin.system.canViewLog');
+
+ /**
+ * @see \wcf\page\AbstractPage::$neededModules
+ */
+ public $neededModules = array('ENABLE_USER_AUTHENTICATION_FAILURE');
+
+ /**
+ * @see \wcf\page\SortablePage::$defaultSortField
+ */
+ public $defaultSortField = 'time';
+
+ /**
+ * @see \wcf\page\SortablePage::$defaultSortOrder
+ */
+ public $defaultSortOrder = 'DESC';
+
+ /**
+ * @see \wcf\page\SortablePage::$validSortFields
+ */
+ public $validSortFields = array('failureID', 'environment', 'userID', 'username', 'time', 'ipAddress', 'userAgent');
+
+ /**
+ * @see \wcf\page\MultipleLinkPage::$objectListClassName
+ */
+ public $objectListClassName = 'wcf\data\user\authentication\failure\UserAuthenticationFailureList';
+}
--- /dev/null
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\DatabaseObject;
+use wcf\util\UserUtil;
+use wcf\system\WCF;
+
+/**
+ * Represents a user authentication failure.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2014 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category Community Framework
+ */
+class UserAuthenticationFailure extends DatabaseObject {
+ /**
+ * @see \wcf\data\DatabaseObject::$databaseTableName
+ */
+ protected static $databaseTableName = 'user_authentication_failure';
+
+ /**
+ * @see \wcf\data\DatabaseObject::$databaseTableIndexName
+ */
+ protected static $databaseTableIndexName = 'failureID';
+
+ /**
+ * Returns the ip address and attempts to convert into IPv4.
+ *
+ * @return string
+ */
+ public function getIpAddress() {
+ return UserUtil::convertIPv6To4($this->ipAddress);
+ }
+
+ /**
+ * Returns the number of authentication failures caused by given ip address.
+ *
+ * @param string $ipAddress
+ * @return boolean
+ */
+ public static function countIPFailures($ipAddress) {
+ $sql = "SELECT COUNT(*) AS count
+ FROM wcf".WCF_N."_user_authentication_failure
+ WHERE ipAddress = ?
+ AND time > ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($ipAddress, TIME_NOW - USER_AUTHENTICATION_FAILURE_TIMEOUT));
+ return $statement->fetchColumn();
+ }
+
+ /**
+ * Returns the number of authentication failures for given user account.
+ *
+ * @param integer $userID
+ * @return boolean
+ */
+ public static function countUserFailures($userID) {
+ $sql = "SELECT COUNT(*) AS count
+ FROM wcf".WCF_N."_user_authentication_failure
+ WHERE userID = ?
+ AND time > ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($userID, TIME_NOW - USER_AUTHENTICATION_FAILURE_TIMEOUT));
+ return $statement->fetchColumn();
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes user authentication failure-related actions.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2014 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category Community Framework
+ */
+class UserAuthenticationFailureAction extends AbstractDatabaseObjectAction {
+}
--- /dev/null
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit user authentication failures.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2014 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category Community Framework
+ */
+class UserAuthenticationFailureEditor extends DatabaseObjectEditor {
+ /**
+ * @see \wcf\data\DatabaseObjectDecorator::$baseClass
+ */
+ protected static $baseClass = 'wcf\data\user\authentication\failure\UserAuthenticationFailure';
+}
--- /dev/null
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of user authentication failures.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2014 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category Community Framework
+ */
+class UserAuthenticationFailureList extends DatabaseObjectList { }
(TIME_NOW - 86400)
));
+ // clean up user authentication failure log
+ if (ENABLE_USER_AUTHENTICATION_FAILURE) {
+ $sql = "DELETE FROM wcf".WCF_N."_user_authentication_failure
+ WHERE time < ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ (TIME_NOW - 86400 * USER_AUTHENTICATION_FAILURE_EXPIRATION)
+ ));
+ }
+
// clean up error logs
$files = @glob(WCF_DIR.'log/*.txt');
if (is_array($files)) {
<item name="wcf.acp.menu.link.captcha"><![CDATA[Captchas]]></item>
<item name="wcf.acp.menu.link.captcha.question.add"><![CDATA[Frage hinzufügen]]></item>
<item name="wcf.acp.menu.link.captcha.question.list"><![CDATA[Fragen auflisten]]></item>
+ <item name="wcf.acp.menu.link.log.authentication.failure"><![CDATA[Fehlgeschlagene Anmeldungen]]></item>
</category>
<category name="wcf.acp.notice">
<item name="wcf.acp.option.search_captcha_type"><![CDATA[Suche]]></item>
<item name="wcf.acp.option.message_captcha_type"><![CDATA[Nachrichten]]></item>
<item name="wcf.acp.option.category.security.antispam.captcha"><![CDATA[Captchas]]></item>
+ <item name="wcf.acp.option.category.security.general.authentication"><![CDATA[Benutzer-Authentifikation]]></item>
+ <item name="wcf.acp.option.enable_user_authentication_failure"><![CDATA[Fehlgeschlagene Ammeldeversuche protokollieren]]></item>
+ <item name="wcf.acp.option.enable_user_authentication_failure.description"><![CDATA[TODO]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_timeout"><![CDATA[Zeitraum der Überwachung]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_timeout.description"><![CDATA[TODO]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_ip_captcha"><![CDATA[Captcha bei Anmeldung (IP)]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_ip_captcha.description"><![CDATA[TODO]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_ip_block"><![CDATA[Blockierung der Anmeldung]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_ip_block.description"><![CDATA[TODO]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_user_captcha"><![CDATA[Captcha bei Anmeldung (Benutzer)]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_user_captcha.description"><![CDATA[TODO]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_expiration"><![CDATA[Löschung von alten Protokolleinträgen]]></item>
+ <item name="wcf.acp.option.user_authentication_failure_expiration.description"><![CDATA[TODO]]></item>
</category>
<category name="wcf.acp.package">
Klicken Sie hier, um sich mit Ihrem neuen Kennwort anzumelden: {link controller='Login' isEmail=true}{/link}]]></item>
<item name="wcf.acp.user.sendNewPassword.mail.subject"><![CDATA[Neues Kennwort für Ihr Benutzerkonto auf der Website: {@PAGE_TITLE|language}]]></item>
<item name="wcf.acp.user.sendNewPassword.workerTitle"><![CDATA[Neue Passwörter zusenden]]></item>
+ <item name="wcf.acp.user.authentication.failure.list"><![CDATA[Fehlgeschlagene Anmeldungen]]></item>
+ <item name="wcf.acp.user.authentication.failure.environment"><![CDATA[Umgebung]]></item>
+ <item name="wcf.acp.user.authentication.failure.environment.user"><![CDATA[Benutzer]]></item>
+ <item name="wcf.acp.user.authentication.failure.environment.admin"><![CDATA[Administration]]></item>
+ <item name="wcf.acp.user.authentication.failure.time"><![CDATA[Datum]]></item>
</category>
<category name="wcf.acp.worker">
<item name="wcf.user.enableSignature"><![CDATA[Signatur entsperren]]></item>
<item name="wcf.user.edit"><![CDATA[Benutzer bearbeiten]]></item>
<item name="wcf.user.birthdayToday"><![CDATA[Hat heute Geburtstag]]></item>
+ <item name="wcf.user.login.blocked"><![CDATA[Aufgrund einer hohen Anzahl von fehlgeschlagenen Anmeldeversuchen durch Ihre IP-Adresse steht Ihnen die Anmeldung aus Sicherheitsgründen vorübergehend nicht zur Verfügung. Bitte versuchen Sie es später erneut!]]></item>
</category>
<category name="wcf.user.menu">
KEY (objectTypeID)
);
+DROP TABLE IF EXISTS wcf1_user_authentication_failure;
+CREATE TABLE wcf1_user_authentication_failure (
+ failureID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ environment ENUM('user', 'admin') NOT NULL DEFAULT 'user',
+ userID INT(10),
+ username VARCHAR(255) NOT NULL DEFAULT '',
+ time INT(10) NOT NULL DEFAULT 0,
+ ipAddress VARCHAR(39) NOT NULL DEFAULT '',
+ userAgent VARCHAR(255) NOT NULL DEFAULT '',
+ KEY (ipAddress, time),
+ KEY (time)
+);
+
DROP TABLE IF EXISTS wcf1_user_avatar;
CREATE TABLE wcf1_user_avatar (
avatarID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
ALTER TABLE wcf1_user_activity_point ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
ALTER TABLE wcf1_user_activity_point ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
+ALTER TABLE wcf1_user_authentication_failure ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
+
ALTER TABLE wcf1_user_profile_visitor ADD FOREIGN KEY (ownerID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
ALTER TABLE wcf1_user_profile_visitor ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;