Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3713
patch
Public
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Authored By
•
bzimport
Nov 21 2014, 9:36 PM
2014-11-21 21:36:17 (UTC+0)
Size
52 KB
Referenced Files
None
Subscribers
None
patch
View Options
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 09eea7d..9c671e1 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -254,6 +254,7 @@ $wgAutoloadLocalClasses = array(
'UserArrayFromResult' => 'includes/UserArray.php',
'UserBlockedError' => 'includes/Exception.php',
'UserMailer' => 'includes/UserMailer.php',
+ 'UserMessage' => 'includes/UserMessage.php',
'UserRightsProxy' => 'includes/UserRightsProxy.php',
'ViewCountUpdate' => 'includes/ViewCountUpdate.php',
'WantedQueryPage' => 'includes/QueryPage.php',
@@ -635,6 +636,7 @@ $wgAutoloadLocalClasses = array(
'JSParser' => 'includes/libs/jsminplus.php',
# includes/logging
+ 'AuthLogFormatter' => 'includes/logging/LogFormatter.php',
'DatabaseLogEntry' => 'includes/logging/LogEntry.php',
'DeleteLogFormatter' => 'includes/logging/LogFormatter.php',
'LegacyLogFormatter' => 'includes/logging/LogFormatter.php',
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index c2606ce..19bbc79 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -3254,6 +3254,52 @@ $wgPasswordResetRoutes = array(
);
/**
+ * Turns on or off logging of authentication attempts.
+ */
+$wgAuthLogging = true;
+
+/**
+ * Whether to log successful logins in addition to failed logins.
+ */
+$wgAuthLoggingStoreValid = true;
+
+/**
+ * Whether to store IP addresses with all login attempts.
+ */
+$wgAuthLoggingStoreIP = true;
+
+/**
+ * Whether to display message notifications to the user any time an invalid login
+ * attempt is made. Similar to talk page messages.
+ */
+$wgAuthLoggingNotifications = true;
+
+/**
+ * If either $wgAuthLoggingNotifications is set to false or if a user does not log
+ * in for an extended period of time, set the conditions for which the user will be
+ * sent an email warning of invalid logins. If there are more than $threshold invalid
+ * logins in $period amount of time, the user will be emailed. Set to false to disable.
+ */
+$wgAuthLoggingEmail = array( 'period' => 72, 'threshold' => 10 );
+
+/**
+ * Whether to allow the user to customize his/her notifications and email settings for
+ * invalid logins. Strict sysadmins can set this to false to force users to be notified
+ * of invalid logins. Note that users can only turn notifications and emails on/off, they
+ * cannot customize the options in $wgAuthLoggingEmail.
+ */
+$wgAuthLoggingUserPref = true;
+
+/**
+ * Grace period in seconds for authentication logging. If an IP address
+ * makes a failed login attempt, but logs in successfully before this
+ * grace period ends, the user will not be notified. This stops the site
+ * from warning users of their own accidental invalid logins.
+ */
+$wgAuthLoggingGracePeriod = 300;
+
+
+/**
* Maximum number of Unicode characters in signature
*/
$wgMaxSigChars = 255;
@@ -3567,6 +3613,7 @@ $wgGroupPermissions['user']['reupload-shared'] = true;
$wgGroupPermissions['user']['minoredit'] = true;
$wgGroupPermissions['user']['purge'] = true; // can use ?action=purge without clicking "ok"
$wgGroupPermissions['user']['sendemail'] = true;
+$wgGroupPermissions['user']['authlog-own'] = true;
// Implicit group for accounts that pass $wgAutoConfirmAge
$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true;
@@ -3623,6 +3670,7 @@ $wgGroupPermissions['sysop']['suppressredirect'] = true;
// Permission to change users' group assignments
$wgGroupPermissions['bureaucrat']['userrights'] = true;
$wgGroupPermissions['bureaucrat']['noratelimit'] = true;
+$wgGroupPermissions['bureaucrat']['authlog'] = true;
// Permission to change users' groups assignments across wikis
#$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true;
// Permission to export pages including linked pages regardless of $wgExportMaxLinkDepth
@@ -5145,6 +5193,7 @@ $wgLogTypes = array(
'patrol',
'merge',
'suppress',
+ 'auth'
);
/**
@@ -5155,7 +5204,8 @@ $wgLogTypes = array(
* Format: logtype => permissiontype
*/
$wgLogRestrictions = array(
- 'suppress' => 'suppressionlog'
+ 'suppress' => 'suppressionlog',
+ 'auth' => array( 'all' => 'authlog', 'own' => 'authlog-own' )
);
/**
@@ -5203,6 +5253,7 @@ $wgLogNames = array(
'patrol' => 'patrol-log-page',
'merge' => 'mergelog',
'suppress' => 'suppressionlog',
+ 'auth' => 'authlogpage'
);
/**
@@ -5226,6 +5277,7 @@ $wgLogHeaders = array(
'patrol' => 'patrol-log-header',
'merge' => 'mergelogpagetext',
'suppress' => 'suppressionlogtext',
+ 'auth' => 'authlogtext'
);
/**
@@ -5269,6 +5321,7 @@ $wgLogActionsHandlers = array(
'suppress/event' => 'DeleteLogFormatter',
'suppress/delete' => 'DeleteLogFormatter',
'patrol/patrol' => 'PatrolLogFormatter',
+ 'auth/*' => 'AuthLogFormatter'
);
/**
diff --git a/includes/Preferences.php b/includes/Preferences.php
index bf63d65..8b77263 100644
--- a/includes/Preferences.php
+++ b/includes/Preferences.php
@@ -156,7 +156,8 @@ class Preferences {
global $wgAuth, $wgContLang, $wgParser, $wgCookieExpiration, $wgLanguageCode,
$wgDisableTitleConversion, $wgDisableLangConversion, $wgMaxSigChars,
$wgEnableEmail, $wgEmailConfirmToEdit, $wgEnableUserEmail, $wgEmailAuthentication,
- $wgEnotifWatchlist, $wgEnotifUserTalk, $wgEnotifRevealEditorAddress;
+ $wgEnotifWatchlist, $wgEnotifUserTalk, $wgEnotifRevealEditorAddress,
+ $wgAuthLoggingNotifications, $wgAuthLoggingUserPref, $wgAuthLoggingEmail;
## User info #####################################
// Information panel
@@ -270,6 +271,15 @@ class Preferences {
'section' => 'personal/info',
);
}
+
+ if( $wgAuthLoggingNotifications ) {
+ $defaultPreferences['invalidloginnotification'] = array(
+ 'type' => 'toggle',
+ 'label' => $context->msg( 'prefs-authnotify' )->text(),
+ 'section' => 'personal/info',
+ 'disabled' => !$wgAuthLoggingUserPref
+ );
+ }
// Language
$languages = Language::fetchLanguageNames( null, 'mw' );
@@ -482,6 +492,16 @@ class Preferences {
);
}
}
+
+ if( $wgAuthLoggingEmail ) {
+ $defaultPreferences['invalidloginemail'] = array(
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label' => $context->msg( 'prefs-authemail' )->numParams(
+ $wgAuthLoggingEmail['threshold'], $wgAuthLoggingEmail['period'] )->text(),
+ 'disabled' => $disableEmailPrefs || !$wgAuthLoggingUserPref
+ );
+ }
}
}
diff --git a/includes/Skin.php b/includes/Skin.php
index 8d47b83..5ee66da 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -1336,55 +1336,48 @@ abstract class Skin extends ContextSource {
* @return MediaWiki message or if no new talk page messages, nothing
*/
function getNewtalks() {
- $out = $this->getOutput();
-
- $newtalks = $this->getUser()->getNewMessageLinks();
- $ntl = '';
-
- if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) {
- $userTitle = $this->getUser()->getUserPage();
- $userTalkTitle = $userTitle->getTalkPage();
-
- if ( !$userTalkTitle->equals( $out->getTitle() ) ) {
- $newMessagesLink = Linker::linkKnown(
- $userTalkTitle,
- $this->msg( 'newmessageslink' )->escaped(),
- array(),
- array( 'redirect' => 'no' )
- );
-
- $newMessagesDiffLink = Linker::linkKnown(
- $userTalkTitle,
- $this->msg( 'newmessagesdifflink' )->escaped(),
- array(),
- array( 'diff' => 'cur' )
- );
-
- $ntl = $this->msg(
- 'youhavenewmessages',
- $newMessagesLink,
- $newMessagesDiffLink
- )->text();
- # Disable Squid cache
- $out->setSquidMaxage( 0 );
+ global $wgAuthLoggingUserPref, $wgAuthLogging, $wgAuthLoggingNotifications;
+ $user = $this->getUser();
+ $title = $this->getOutput()->getTitle();
+ $messages = UserMessage::getUserMessages( $user );
+
+ $ntls = array();
+ foreach( $messages as $message ) {
+ if( $message->getType() == UserMessage::MSG_TALK ) {
+ $page = $user->getUserPage()->getTalkPage();
+ } elseif( $message->getType() == UserMessage::MSG_AUTH ) {
+ if( !$wgAuthLoggingNotifications || $wgAuthLoggingUserPref && !$user->getOption( "invalidloginnotification", false ) ) {
+ continue;
+ }
+ $page = Title::makeTitle( NS_SPECIAL, 'Log/auth' );
+ } else {
+ continue;
}
- } elseif ( count( $newtalks ) ) {
- // _>" " for BC <= 1.16
- $sep = str_replace( '_', ' ', $this->msg( 'newtalkseparator' )->escaped() );
- $msgs = array();
-
- foreach ( $newtalks as $newtalk ) {
- $msgs[] = Xml::element(
- 'a',
- array( 'href' => $newtalk['link'] ), $newtalk['wiki']
- );
+
+ if( $page->equals( $title ) ) {
+ $message->update( UserMessage::NOTSET );
+ $user->invalidateCache();
+ continue;
}
- $parts = implode( $sep, $msgs );
- $ntl = $this->msg( 'youhavenewmessagesmulti' )->rawParams( $parts )->escaped();
- $out->setSquidMaxage( 0 );
+
+ $link = Linker::linkKnown(
+ $page,
+ $message->getMessage()->escaped(),
+ array(),
+ array( 'redirect' => 'no' )
+ );
+
+ $ntls[] = $this->msg(
+ 'newmessages-here',
+ $link
+ )->text();
+
+ # Disable Squid cache
+ $this->getOutput()->setSquidMaxage( 0 );
}
- return $ntl;
+ $break = Html::element( 'br' );
+ return implode( $break, $ntls );
}
/**
diff --git a/includes/User.php b/includes/User.php
index 01407b1..6d70390 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -1740,154 +1740,6 @@ class User {
}
/**
- * Check if the user has new messages.
- * @return Bool True if the user has new messages
- */
- public function getNewtalk() {
- $this->load();
-
- # Load the newtalk status if it is unloaded (mNewtalk=-1)
- if( $this->mNewtalk === -1 ) {
- $this->mNewtalk = false; # reset talk page status
-
- # Check memcached separately for anons, who have no
- # entire User object stored in there.
- if( !$this->mId ) {
- global $wgMemc;
- $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
- $newtalk = $wgMemc->get( $key );
- if( strval( $newtalk ) !== '' ) {
- $this->mNewtalk = (bool)$newtalk;
- } else {
- // Since we are caching this, make sure it is up to date by getting it
- // from the master
- $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
- $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
- }
- } else {
- $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
- }
- }
-
- return (bool)$this->mNewtalk;
- }
-
- /**
- * Return the talk page(s) this user has new messages on.
- * @return Array of String page URLs
- */
- public function getNewMessageLinks() {
- $talks = array();
- if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) )
- return $talks;
-
- if( !$this->getNewtalk() )
- return array();
- $up = $this->getUserPage();
- $utp = $up->getTalkPage();
- return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) );
- }
-
- /**
- * Internal uncached check for new messages
- *
- * @see getNewtalk()
- * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
- * @param $id String|Int User's IP address for anonymous users, User ID otherwise
- * @param $fromMaster Bool true to fetch from the master, false for a slave
- * @return Bool True if the user has new messages
- */
- protected function checkNewtalk( $field, $id, $fromMaster = false ) {
- if ( $fromMaster ) {
- $db = wfGetDB( DB_MASTER );
- } else {
- $db = wfGetDB( DB_SLAVE );
- }
- $ok = $db->selectField( 'user_newtalk', $field,
- array( $field => $id ), __METHOD__ );
- return $ok !== false;
- }
-
- /**
- * Add or update the new messages flag
- * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
- * @param $id String|Int User's IP address for anonymous users, User ID otherwise
- * @return Bool True if successful, false otherwise
- */
- protected function updateNewtalk( $field, $id ) {
- $dbw = wfGetDB( DB_MASTER );
- $dbw->insert( 'user_newtalk',
- array( $field => $id ),
- __METHOD__,
- 'IGNORE' );
- if ( $dbw->affectedRows() ) {
- wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
- return true;
- } else {
- wfDebug( __METHOD__ . " already set ($field, $id)\n" );
- return false;
- }
- }
-
- /**
- * Clear the new messages flag for the given user
- * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
- * @param $id String|Int User's IP address for anonymous users, User ID otherwise
- * @return Bool True if successful, false otherwise
- */
- protected function deleteNewtalk( $field, $id ) {
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'user_newtalk',
- array( $field => $id ),
- __METHOD__ );
- if ( $dbw->affectedRows() ) {
- wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
- return true;
- } else {
- wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
- return false;
- }
- }
-
- /**
- * Update the 'You have new messages!' status.
- * @param $val Bool Whether the user has new messages
- */
- public function setNewtalk( $val ) {
- if( wfReadOnly() ) {
- return;
- }
-
- $this->load();
- $this->mNewtalk = $val;
-
- if( $this->isAnon() ) {
- $field = 'user_ip';
- $id = $this->getName();
- } else {
- $field = 'user_id';
- $id = $this->getId();
- }
- global $wgMemc;
-
- if( $val ) {
- $changed = $this->updateNewtalk( $field, $id );
- } else {
- $changed = $this->deleteNewtalk( $field, $id );
- }
-
- if( $this->isAnon() ) {
- // Anons have a separate memcached space, since
- // user records aren't kept for them.
- $key = wfMemcKey( 'newtalk', 'ip', $id );
- $wgMemc->set( $key, $val ? 1 : 0, 1800 );
- }
- if ( $changed ) {
- $this->invalidateCache();
- }
- }
-
- /**
* Generate a current or new-future timestamp to be stored in the
* user_touched field when we update things.
* @return String Timestamp in TS_MW format
@@ -2661,7 +2513,8 @@ class User {
$title->getText() == $this->getName() ) {
if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
return;
- $this->setNewtalk( false );
+ $msg = new UserMessage( $this, UserMessage::MSG_TALK );
+ $msg->update( UserMessage::NOTSET );
}
if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
@@ -2696,7 +2549,8 @@ class User {
public function clearAllNotifications() {
global $wgUseEnotif, $wgShowUpdatedMarker;
if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
- $this->setNewtalk( false );
+ $msg = new UserMessage( $this, UserMessage::MSG_TALK );
+ $msg->update( UserMessage::NOTSET );
return;
}
$id = $this->getId();
diff --git a/includes/UserMessage.php b/includes/UserMessage.php
new file mode 100644
index 0000000..2d4f7ff
--- /dev/null
+++ b/includes/UserMessage.php
@@ -0,0 +1,303 @@
+<?php
+
+/**
+ * Implements wrapper for user messages in the user_newtalk table.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * The UserMessage object represents a new message for a user.
+ * This does not represent an actual message, but rather whether
+ * the user has any new messages. For example, if a user has new
+ * talk page messages, a UserMessage with type 'talk' is made and
+ * stored in the database.
+ */
+class UserMessage {
+ const NOTLOADED = -1;
+ const NOTSET = 0;
+ const SET = 1;
+
+ const MSG_TALK = 0;
+ const MSG_AUTH = 1;
+
+ private static $typeNames = array(
+ UserMessage::MSG_TALK => 'talk',
+ UserMessage::MSG_AUTH => 'auth'
+ );
+
+ /**
+ * User the message is for
+ * @var User $user
+ */
+ private $user;
+
+ /**
+ * Type of the message
+ * @var string $type
+ */
+ private $type;
+
+ /**
+ * Timestamp the message was created
+ * @var string $timestamp
+ */
+ private $timestamp;
+
+ /**
+ * Status of the message. Can be NOTLOADED if the status has
+ * not been set yet, NOTSET if the user doesn't have this specific
+ * message type, or SET if the user has the message.
+ * @var int $status
+ */
+ private $status;
+
+ /**
+ * Get an array of all messages the given user has.
+ * @param User|string $user User or IP address to get messages for
+ * @return Array List of UserMessage objects
+ */
+ public static function getUserMessages( $user ) {
+ global $wgMemc;
+ $db = wfGetDB( DB_SLAVE );
+
+ if( is_string( $user ) ) {
+ $field = 'user_ip';
+ $value = $user;
+ } else {
+ $field = 'user_id';
+ $value = $user->getId();
+ }
+
+ $messages = array();
+
+ // Check memcached first.
+ $memKey = wfMemcKey( 'user', 'message', $value );
+ $types = $wgMemc->get( $memKey );
+ if( is_array( $types ) ) {
+ foreach( $types as $type => $timestamp ) {
+ $messages[] = new UserMessage( $user, $type, $timestamp, UserMessage::SET );
+ }
+ } else {
+ // Cache miss. Go to database.
+ $res = $db->select(
+ 'user_newtalk',
+ array( 'user_last_timestamp', 'user_msg_type' ),
+ array( $field => $value )
+ );
+
+ $toCache = array();
+ foreach( $res as $row ) {
+ $messages[] = new UserMessage(
+ $user,
+ $row->user_msg_type,
+ $row->user_last_timestamp,
+ UserMessage::SET
+ );
+ $toCache[$row->user_msg_type] = $row->user_last_timestamp;
+ }
+ $wgMemc->set( $memKey, $toCache, 72 * 3600 );
+ }
+ return $messages;
+ }
+
+ /**
+ * Clear all messages for the user.
+ */
+ public static function clearMessages( $user ) {
+ global $wgMemc;
+ $db = wfGetDB( DB_SLAVE );
+
+ if( is_string( $user ) ) {
+ $field = 'user_ip';
+ $value = $user;
+ } else {
+ $field = 'user_id';
+ $value = $user->getId();
+ }
+
+ // Clear memcached (but don't delete key as this will cause an unnecessary
+ // DB query next time).
+ $memKey = wfMemcKey( 'user', 'message', $value );
+ $wgMemc->replace( $memKey, array() );
+
+ // Clear database.
+ $res = $db->delete( 'user_newtalk', array( $field => $value ) );
+ }
+
+ /**
+ * Put together a new UserMessage object by storing the user, type,
+ * and timestamp.
+ */
+ public function __construct( $user, $type, $timestamp = false, $status = UserMessage::NOTLOADED ) {
+ $this->user = $user;
+ $this->type = $type;
+ $this->timestamp = $timestamp;
+ $this->status = $status;
+ }
+
+ /**
+ * Get the user associated with the message.
+ * @return User|string $user User or IP address to get messages for
+ */
+ public function getUser() {
+ return $this->user;
+ }
+
+ /**
+ * Get the type of the message.
+ * @return string The message type
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * Get the timestamp of the message in the specified format.
+ * @param string $type Output type of the timestamp
+ * @return string Timestamp for the message
+ */
+ public function getTimestamp( $type ) {
+ if( $this->timestamp === false ) {
+ return false;
+ } else {
+ return wfTimestamp( $type, $this->timestamp );
+ }
+ }
+
+ /**
+ * Get the status of the message, i.e., whether the user has this
+ * message yet.
+ * @return int Whether the user has the message or not (see class constants)
+ */
+ public function getStatus() {
+ return $this->status;
+ }
+
+ /**
+ * Get the string message of this message, which is whatever message
+ * is stored under the key newmessages-type-$type.
+ * @return string The message
+ */
+ public function getMessage() {
+ $type = UserMessage::$typeNames[$this->type];
+ $msg = "newmessages-type-$type";
+ return RequestContext::getMain()->msg( $msg );
+ }
+
+ /**
+ * Load the message from the database. If the user doesn't have this
+ * specific message, set the timestamp to false and status to NOTSET.
+ * Otherwise, store the timestamp of the message and set the status to SET.
+ */
+ public function load() {
+ global $wgMemc;
+ if( is_string( $this->user ) ) {
+ $field = 'user_ip';
+ $value = $this->user;
+ } else {
+ $field = 'user_id';
+ $value = $this->user->getId();
+ }
+
+ // Try memcached first, then database.
+ $memKey = wfMemcKey( 'user', 'message', $value );
+ $types = $wgMemc->get( $memKey );
+ if( is_array( $types ) ) {
+ // Memcached is valid.
+ if( array_key_exists( $this->type, $types ) ) {
+ $this->status = self::SET;
+ $this->timestamp = $types[$this->type];
+ } else {
+ $this->status = self::NOTSET;
+ $this->timestamp = false;
+ }
+ } else {
+ // Cache miss. Get from DB.
+ $db = wfGetDB( DB_SLAVE );
+ $res = $db->selectField(
+ 'user_newtalk',
+ 'user_last_timestamp',
+ array( $field => $value, 'user_msg_type' => $this->type ),
+ array( 'IGNORE' )
+ );
+
+ $this->status = $res === false ? UserMessage::NOTSET : UserMessage::SET;
+ $this->timestamp = $res;
+ }
+ }
+
+ /**
+ * Update the status of the message. If the status is updated from
+ * SET to NOTSET, delete it from the database. If vice-versa, add it
+ * to the database. Otherwise, no action.
+ * @param int Whether the user has the message or not (see class constants)
+ * @return bool True if the status changed, false otherwise
+ */
+ public function update( $status ) {
+ global $wgMemc;
+ if( $status == $this->status ) {
+ return false;
+ }
+
+ $this->status = $status;
+
+ if( is_string( $this->user ) ) {
+ $field = 'user_ip';
+ $value = $this->user;
+ } else {
+ $field = 'user_id';
+ $value = $this->user->getId();
+ }
+
+ $db = wfGetDB( DB_MASTER );
+ $memKey = wfMemcKey( 'user', 'message', $value );
+ $types = $wgMemc->get( $memKey );
+
+ if( $status == UserMessage::SET ) {
+ $this->timestamp = wfTimestamp( TS_MW );
+ $indices = array( array( 'user_msg_type', $field ) );
+ $row = array(
+ $field => $value,
+ 'user_last_timestamp' => $this->timestamp,
+ 'user_msg_type' => $this->type
+ );
+
+ $db->replace( 'user_newtalk', $indices, $row, __METHOD__ );
+
+ if( is_array( $types ) ) {
+ $types[$this->type] = $this->timestamp;
+ } else {
+ $types = array( $this->type => $this->timestamp );
+ }
+ } else {
+ $this->timestamp = false;
+ $where = array( $field => $value, 'user_msg_type' => $this->type );
+ $db->delete( 'user_newtalk', $where, __METHOD__ );
+
+ if( is_array( $types ) && array_key_exists( $this->type, $types ) ) {
+ unset( $types[$this->type] );
+ }
+ }
+
+ $wgMemc->set( $memKey, $types );
+
+ $this->user->invalidateCache();
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/includes/WikiPage.php b/includes/WikiPage.php
index 40e0ec3..56b66cb 100644
--- a/includes/WikiPage.php
+++ b/includes/WikiPage.php
@@ -1832,13 +1832,14 @@ class WikiPage extends Page {
) {
if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) {
$other = User::newFromName( $shortTitle, false );
+ $msg = new UserMessage( $other, UserMessage::MSG_TALK );
if ( !$other ) {
wfDebug( __METHOD__ . ": invalid username\n" );
} elseif ( User::isIP( $shortTitle ) ) {
// An anonymous user
- $other->setNewtalk( true );
+ $msg->update( UserMessage::SET );
} elseif ( $other->isLoggedIn() ) {
- $other->setNewtalk( true );
+ $msg->update( UserMessage::SET );
} else {
wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
}
@@ -2538,8 +2539,9 @@ class WikiPage extends Page {
# User talk pages
if ( $title->getNamespace() == NS_USER_TALK ) {
$user = User::newFromName( $title->getText(), false );
+ $msg = new UserMessage( $user, UserMessage::MSG_TALK );
if ( $user ) {
- $user->setNewtalk( false );
+ $msg->update( UserMessage::NOTSET );
}
}
diff --git a/includes/db/Database.php b/includes/db/Database.php
index 0a1f988..ac2a0bc 100644
--- a/includes/db/Database.php
+++ b/includes/db/Database.php
@@ -1846,20 +1846,20 @@ abstract class DatabaseBase implements DatabaseType {
// Don't necessarily assume the single key is 0; we don't
// enforce linear numeric ordering on other arrays here.
$value = array_values( $value );
- $list .= $field . " = " . $this->addQuotes( $value[0] );
+ $list .= $this->addIdentifierQuotes( $field ) . " = " . $this->addQuotes( $value[0] );
} else {
- $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
+ $list .= $this->addIdentifierQuotes( $field ) . " IN (" . $this->makeList( $value ) . ") ";
}
} elseif ( $value === null ) {
if ( $mode == LIST_AND || $mode == LIST_OR ) {
- $list .= "$field IS ";
+ $list .= $this->addIdentifierQuotes( $field ) . " IS ";
} elseif ( $mode == LIST_SET ) {
- $list .= "$field = ";
+ $list .= $this->addIdentifierQuotes( $field ) . " = ";
}
$list .= 'NULL';
} else {
if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
- $list .= "$field = ";
+ $list .= $this->addIdentifierQuotes( $field ) . " = ";
}
$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
}
diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php
index e453b01..8292d5e 100644
--- a/includes/installer/MysqlUpdater.php
+++ b/includes/installer/MysqlUpdater.php
@@ -213,6 +213,7 @@ class MysqlUpdater extends DatabaseUpdater {
array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
+ array( 'addField', 'user_newtalk', 'user_newtalk_type', 'patch-user_newtalk_type.sql' ),
);
}
diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php
index aa3c334..2c72492 100644
--- a/includes/installer/OracleUpdater.php
+++ b/includes/installer/OracleUpdater.php
@@ -69,6 +69,7 @@ class OracleUpdater extends DatabaseUpdater {
//1.20
array( 'addTable', 'config', 'patch-config.sql' ),
+ array( 'addField', 'user_newtalk', 'user_newtalk_type', 'patch-user_newtalk_type.sql' ),
// KEEP THIS AT THE BOTTOM!!
array( 'doRebuildDuplicateFunction' ),
diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php
index 43005a8..3d5dbbb 100644
--- a/includes/installer/PostgresUpdater.php
+++ b/includes/installer/PostgresUpdater.php
@@ -330,6 +330,7 @@ class PostgresUpdater extends DatabaseUpdater {
# r81574
array( 'addInterwikiType' ),
# end
+ array( 'addPgField', 'user_newtalk', 'user_newtalk_type', 'patch-user_newtalk_type.sql' ),
array( 'tsearchFixes' ),
);
}
diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php
index 8146274..cabe3f0 100644
--- a/includes/installer/SqliteUpdater.php
+++ b/includes/installer/SqliteUpdater.php
@@ -92,6 +92,7 @@ class SqliteUpdater extends DatabaseUpdater {
array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
+ array( 'addField', 'user_newtalk', 'user_newtalk_type', 'patch-user_newtalk_type.sql' ),
);
}
diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php
index 04df226..65824ae 100644
--- a/includes/logging/LogEventsList.php
+++ b/includes/logging/LogEventsList.php
@@ -726,21 +726,48 @@ class LogEventsList {
* @return Mixed: string or false
*/
public static function getExcludeClause( $db, $audience = 'public' ) {
- global $wgLogRestrictions, $wgUser;
+ global $wgLogRestrictions, $wgUser, $wgRequest;
// Reset the array, clears extra "where" clauses when $par is used
$hiddenLogs = array();
+ $ownOnlyLogs = array();
// Don't show private logs to unprivileged users
- foreach( $wgLogRestrictions as $logType => $right ) {
- if( $audience == 'public' || !$wgUser->isAllowed($right) ) {
- $safeType = $db->strencode( $logType );
- $hiddenLogs[] = $safeType;
+ foreach( $wgLogRestrictions as $index => $val ) {
+ if( $audience == 'public' ) {
+ $hiddenLogs[] = $index;
+ } elseif( is_array( $val ) && !$wgUser->isAllowed( $val['all'] ) ) {
+ if( $wgUser->isAllowed( $val['own'] ) ) {
+ $ownOnlyLogs[] = $index;
+ } else {
+ $hiddenLogs[] = $index;
+ }
+ } elseif( is_string( $val ) && !$wgUser->isAllowed( $val ) ) {
+ $hiddenLogs[] = $index;
}
}
+
+ $cond = '';
if( count($hiddenLogs) == 1 ) {
- return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
+ $cond .= $db->addIdentifierQuotes( 'log_type' ) . ' != ' . $db->addQuotes( $hiddenLogs[0] );
} elseif( $hiddenLogs ) {
- return 'log_type NOT IN (' . $db->makeList($hiddenLogs) . ')';
+ $cond .= $db->addIdentifierQuotes( 'log_type' ) . ' NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
+ } else {
+ $cond .= '';
+ }
+
+ if( $ownOnlyLogs ) {
+ $userConds = array( 'log_namespace' => NS_USER, 'log_title' => $wgUser->getName() );
+ if( count( $ownOnlyLogs ) == 1 ) {
+ $typeCond = $db->addIdentifierQuotes( 'log_type' ) . ' = ' . $db->addQuotes( $ownOnlyLogs[0] );
+ } else {
+ $typeCond = $db->addIdentifierQuotes( 'log_type' ) . 'NOT IN (' . $db->makeList( $ownOnlyLogs ) . ')';
+ }
+
+ if( $cond ) {
+ $cond .= ' AND ';
+ }
+ $cond .= 'NOT ( ' . $typeCond . ' AND NOT ( ' . $db->makeList( $userConds, LIST_AND ) . ' ) )';
}
- return false;
+
+ return $cond;
}
}
diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php
index 1ba6a3b..be3efad 100644
--- a/includes/logging/LogFormatter.php
+++ b/includes/logging/LogFormatter.php
@@ -431,7 +431,12 @@ class LogFormatter {
public function getPerformerElement() {
if ( $this->canView( LogPage::DELETED_USER ) ) {
$performer = $this->entry->getPerformer();
- $element = $this->makeUserLink( $performer );
+ if( $performer->getName() == '0.0.0.0' ) {
+ $element = wfMsg( 'log-anonymous' );
+ } else {
+ $element = $this->makeUserLink( $performer );
+ }
+
if ( $this->entry->isDeleted( LogPage::DELETED_USER ) ) {
$element = $this->styleRestricedElement( $element );
}
@@ -736,3 +741,15 @@ class NewUsersLogFormatter extends LogFormatter {
return array();
}
}
+
+
+/**
+ * This class formats auth log entries.
+ */
+ class AuthLogFormatter extends LogFormatter {
+ protected function getMessageParameters() {
+ $params = parent::getMessageParameters();
+ $params[3] = $params[3] ? wfMsg( 'authlog-success' ) : wfMsg( 'authlog-failed' );
+ return $params;
+ }
+}
\ No newline at end of file
diff --git a/includes/logging/LogPage.php b/includes/logging/LogPage.php
index 3891f34..d222c18 100644
--- a/includes/logging/LogPage.php
+++ b/includes/logging/LogPage.php
@@ -628,10 +628,15 @@ class LogPage {
* @return string
* @since 1.19
*/
- public function getRestriction() {
+ public function getRestriction($own = false) {
global $wgLogRestrictions;
if ( isset( $wgLogRestrictions[$this->type] ) ) {
- $restriction = $wgLogRestrictions[$this->type];
+ if( is_array( $wgLogRestrictions[$this->type] ) ) {
+ $key = $own ? 'own' : 'all';
+ $restriction = $wgLogRestrictions[$this->type][$key];
+ } else {
+ $restriction = $wgLogRestrictions[$this->type];
+ }
} else {
// '' always returns true with $user->isAllowed()
$restriction = '';
diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php
index ea1be8e..a4409a0 100644
--- a/includes/logging/LogPager.php
+++ b/includes/logging/LogPager.php
@@ -100,8 +100,12 @@ class LogPager extends ReverseChronologicalPager {
// Don't even show header for private logs; don't recognize it...
$needReindex = false;
foreach ( $types as $type ) {
- if( isset( $wgLogRestrictions[$type] )
- && !$this->getUser()->isAllowed($wgLogRestrictions[$type])
+ if( !isset( $wgLogRestrictions[$type] ) ) {
+ continue;
+ } elseif( is_string( $wgLogRestrictions[$type] )
+ && !$this->getUser()->isAllowed($wgLogRestrictions[$type]) ||
+ is_array( $wgLogRestrictions[$type] )
+ && !$this->getUser()->isAllowed($wgLogRestrictions[$type]['own'])
) {
$needReindex = true;
$types = array_diff( $types, array( $type ) );
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
index 8eee22d..01f60bd 100644
--- a/includes/specials/SpecialLog.php
+++ b/includes/specials/SpecialLog.php
@@ -39,6 +39,7 @@ class SpecialLog extends SpecialPage {
'block',
'newusers',
'rights',
+ 'auth'
);
public function __construct() {
@@ -51,7 +52,7 @@ class SpecialLog extends SpecialPage {
$this->setHeaders();
$this->outputHeader();
- $opts = new FormOptions;
+ $opts = new FormOptions();
$opts->add( 'type', '' );
$opts->add( 'user', '' );
$opts->add( 'page', '' );
@@ -78,9 +79,10 @@ class SpecialLog extends SpecialPage {
// Reset the log type to default (nothing) if it's invalid or if the
// user does not possess the right to view it
$type = $opts->getValue( 'type' );
+ $restriction = is_array( $wgLogRestrictions[$type] ) ? $wgLogRestrictions[$type]['own'] : $wgLogRestrictions[$type];
if ( !LogPage::isLogType( $type )
|| ( isset( $wgLogRestrictions[$type] )
- && !$this->getUser()->isAllowed( $wgLogRestrictions[$type] ) )
+ && !$this->getUser()->isAllowed( $restriction ) )
) {
$opts->setValue( 'type', '' );
}
diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php
index ded2721..35cebbf 100644
--- a/includes/specials/SpecialUserlogin.php
+++ b/includes/specials/SpecialUserlogin.php
@@ -482,7 +482,9 @@ class LoginForm extends SpecialPage {
* @return int
*/
public function authenticateUserData() {
- global $wgUser, $wgAuth;
+ global $wgUser, $wgAuth, $wgRequest;
+ global $wgAuthLogging, $wgAuthLoggingStoreIP, $wgAuthLoggingStoreValid;
+ global $wgAuthLoggingEmail, $wgAuthLoggingUserPref, $wgAuthLoggingGracePeriod;
$this->load();
@@ -616,6 +618,77 @@ class LoginForm extends SpecialPage {
$retval = self::SUCCESS;
}
+
+ // If logging is enabled, and either valid login logging is enabled or this is an invalid login.
+ if( $wgAuthLogging && ( $wgAuthLoggingStoreValid || $retval != self::SUCCESS ) ) {
+ $res = ( $retval == self::SUCCESS ) ? true : false;
+ $anon = new User();
+ $anon->setName( '0.0.0.0' );
+ $params = array( '4:bool:result' => $res );
+
+ $entry = new ManualLogEntry( "auth", "login" );
+ $entry->setTarget( Title::makeTitle( NS_USER, $this->mUsername ) );
+ $entry->setPerformer( $wgAuthLoggingStoreIP ? $wgUser : $anon );
+ $entry->setParameters( $params );
+ $entry->insert();
+ }
+
+ // If the login wasn't successful, either send an email if above the threshold or set a notification.
+ if( $retval != self::SUCCESS ) {
+ // Get user's auth message.
+ $msg = new UserMessage( $u, UserMessage::MSG_AUTH );
+ $msg->load();
+
+ // First check if emailing is enabled and if user set preferences to enable it (or if preferences are disabled).
+ // Also check if the AUTH message is set. If it's not set, then the number of invalid logins is 0.
+ if( $wgAuthLoggingEmail
+ && ( !$wgAuthLoggingUserPref || $u->getOption( "invalidloginemail", false ) )
+ && $msg->getStatus() == UserMessage::SET ) {
+ $db = wfGetDB( DB_SLAVE );
+ $queryData = DatabaseLogEntry::getSelectQueryData();
+ $conds = $queryData['conds'];
+ $options = $queryData['options'];
+
+ // Get all invalid logins that happened after the AUTH message timestamp or the configured
+ // time period, whatever is less.
+ $baseline = max( $msg->getTimestamp( TS_UNIX ), wfTimestamp( TS_UNIX ) - $wgAuthLoggingEmail['period'] * 3600 );
+ $time_limit = wfTimestamp( TS_MW, $baseline );
+ $conds['log_namespace'] = NS_USER;
+ $conds['log_title'] = $this->mUsername;
+ $conds[] = 'log_timestamp > ' . $db->addQuotes( $time_limit );
+ // No need to get more than the threshold. Being above it is an automatic trigger.
+ $options['LIMIT'] = $wgAuthLoggingEmail['threshold'];
+
+ // Now we need to filter for only log lines that were invalid logins.
+ $res = $db->select( 'logging', 'log_params', $conds, $options );
+ $numLogins = 0;
+ if( $wgAuthLoggingStoreValid ) {
+ foreach( $res as $row ) {
+ $entry = DatabaseLogEntry::newFromRow( $row );
+ $params = $entry->getParameters();
+ if( $params['4:bool:result'] == false ) {
+ $numLogins++;
+ }
+ }
+ } else {
+ // If valid login logging isn't enabled, then they all must be invalid.
+ $numLogins = $res->numRows();
+ }
+
+ if( $numLogins >= $wgAuthLoggingEmail['threshold'] ) {
+ $subject = wfMsg( 'enotif-auth-subject' );
+ $body = wfMsg( 'enotif-auth-body', $u->getName(), $wgAuthLoggingEmail['threshold'], $wgAuthLoggingEmail['period'] );
+ $u->sendMail( $subject, $body );
+ $msg = new UserMessage( $u, UserMessage::MSG_AUTH );
+ $msg->update( UserMessage::NOTSET );
+ }
+ } else {
+ $msg = new UserMessage( $u, UserMessage::MSG_AUTH );
+ $msg->update( UserMessage::SET );
+ $u->invalidateCache();
+ }
+ }
+
wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) );
return $retval;
}
diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php
index b7275f8..7944a12 100644
--- a/languages/messages/MessagesEn.php
+++ b/languages/messages/MessagesEn.php
@@ -903,11 +903,11 @@ See [[Special:Version|version page]].',
'pagetitle-view-mainpage' => '{{SITENAME}}', # only translate this message to other languages if you have to change it
'backlinksubtitle' => '← $1', # only translate this message to other languages if you have to change it
'retrievedfrom' => 'Retrieved from "$1"',
-'youhavenewmessages' => 'You have $1 ($2).',
-'newmessageslink' => 'new messages',
-'newmessagesdifflink' => 'last change',
-'youhavenewmessagesmulti' => 'You have new messages on $1',
-'newtalkseparator' => ', ', # do not translate or duplicate this message to other languages
+'newmessages-here' => 'You have new $1.',
+'newmessages-there' => 'You have new $1 on $2.',
+'newmessages-type-talk' => 'talk page messages',
+'newmessages-type-auth' => 'failed logins',
+'newmessages-sep' => ', ', # do not translate or duplicate this message to other languages
'editsection' => 'edit',
'editsection-brackets' => '[$1]', # only translate this message to other languages if you have to change it
'editold' => 'edit',
@@ -1172,6 +1172,10 @@ Please wait before trying again.',
* Nederlands|nl', # do not translate or duplicate this message to other languages
'suspicious-userlogout' => 'Your request to log out was denied because it looks like it was sent by a broken browser or caching proxy.',
+# Auth log
+'authlogpage' => 'Auth log',
+'authlogtext' => 'This is a list of authentication events, i.e., login attempts.',
+
# E-mail sending
'pear-mail-error' => '$1', # do not translate or duplicate this message to other languages
'php-mail-error' => '$1', # do not translate or duplicate this message to other languages
@@ -1809,6 +1813,8 @@ Note that their indexes of {{SITENAME}} content may be out of date.',
'prefs-setemail' => 'Set an e-mail address',
'prefs-email' => 'E-mail options',
'prefs-rendering' => 'Appearance',
+'prefs-authnotify' => 'Enable notifications for invalid logins.',
+'prefs-authemail' => 'Enable e-mail notifications for when $1 invalid logins occur in $2 hours.',
'saveprefs' => 'Save',
'resetprefs' => 'Clear unsaved changes',
'restoreprefs' => 'Restore all default settings',
@@ -2877,6 +2883,22 @@ $UNWATCHURL
Feedback and further assistance:
{{canonicalurl:{{MediaWiki:Helppage}}}}',
+# Auth notifications
+'enotif-auth-subject' => '{{SITENAME}} - Account Security Notification',
+'enotif-auth-body' => 'Dear $1,
+
+Over the past $2 hours, more than $3 invalid login attempts have been made to your account. It is recommended you login to your
+account, check to make sure nobody has accessed your account without your knowledge, and check to make sure your password is secure.
+
+ Your friendly {{SITENAME}} notification system
+
+--
+To change your e-mail notification settings, visit
+{{canonicalurl:{{#special:Preferences}}}}
+
+Feedback and further assistance:
+{{canonicalurl:{{MediaWiki:Helppage}}}}',
+
# Delete
'deletepage' => 'Delete page',
'confirm' => 'Confirm',
@@ -4756,6 +4778,7 @@ This site is experiencing technical difficulties.',
'sqlite-no-fts' => '$1 without full-text search support',
# New logging system
+'log-anonymous' => 'Somebody',
'logentry-delete-delete' => '$1 deleted page $3',
'logentry-delete-restore' => '$1 restored page $3',
'logentry-delete-event' => '$1 changed visibility of {{PLURAL:$5|a log event|$5 log events}} on $3: $4',
@@ -4785,7 +4808,10 @@ This site is experiencing technical difficulties.',
'logentry-newusers-create' => '$1 created a user account',
'logentry-newusers-create2' => '$1 created a user account $3',
'logentry-newusers-autocreate' => 'Account $1 was created automatically',
+'logentry-auth-login' => '$1 $4 logging in to $3.',
'newuserlog-byemail' => 'password sent by e-mail',
+'authlog-success' => 'succeeded',
+'authlog-failed' => 'failed',
# For IRC, see bug 34508. Do not change
'revdelete-logentry' => 'changed revision visibility of "[[$1]]"', # do not translate or duplicate this message to other languages
diff --git a/maintenance/archives/patch-user_newtalk_type.sql b/maintenance/archives/patch-user_newtalk_type.sql
new file mode 100644
index 0000000..6e18fac
--- /dev/null
+++ b/maintenance/archives/patch-user_newtalk_type.sql
@@ -0,0 +1,16 @@
+-- Add the user_msg_type column to user_newtalk and create indices
+
+ALTER TABLE /*_*/user_newtalk
+ ADD user_msg_type int unsigned;
+
+UPDATE /*_*/user_newtalk
+ SET user_msg_type=0;
+
+ALTER TABLE /*_*/user_newtalk
+ MODIFY user_msg_type int unsigned NOT NULL default 0;
+
+DROP INDEX /*i*/un_user_id ON /*_*/user_newtalk;
+DROP INDEX /*i*/un_user_ip ON /*_*/user_newtalk;
+
+CREATE UNIQUE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id, user_msg_type);
+CREATE UNIQUE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip, user_msg_type);
\ No newline at end of file
diff --git a/maintenance/mssql/tables.sql b/maintenance/mssql/tables.sql
index a0c3d17..767673b 100644
--- a/maintenance/mssql/tables.sql
+++ b/maintenance/mssql/tables.sql
@@ -72,9 +72,10 @@ CREATE TABLE /*$wgDBprefix*/user_newtalk (
user_id INT NOT NULL DEFAULT 0 REFERENCES /*$wgDBprefix*/[user](user_id) ON DELETE CASCADE,
user_ip NVARCHAR(40) NOT NULL DEFAULT '',
user_last_timestamp DATETIME NOT NULL DEFAULT '',
+ user_msg_type INT NOT NULL DEFAULT 0
);
-CREATE INDEX /*$wgDBprefix*/user_group_id ON /*$wgDBprefix*/user_newtalk([user_id]);
-CREATE INDEX /*$wgDBprefix*/user_ip ON /*$wgDBprefix*/user_newtalk(user_ip);
+CREATE UNIQUE INDEX /*$wgDBprefix*/user_group_id ON /*$wgDBprefix*/user_newtalk([user_id], user_msg_type);
+CREATE UNIQUE INDEX /*$wgDBprefix*/user_ip ON /*$wgDBprefix*/user_newtalk(user_ip, user_msg_type);
--
-- User preferences and other fun stuff
diff --git a/maintenance/oracle/archives/patch-user_newtalk_type.sql b/maintenance/oracle/archives/patch-user_newtalk_type.sql
new file mode 100644
index 0000000..8be3c9d
--- /dev/null
+++ b/maintenance/oracle/archives/patch-user_newtalk_type.sql
@@ -0,0 +1,8 @@
+-- Add the user_msg_type column to user_newtalk and create indices
+
+ALTER TABLE &mw_prefix.user_newtalk ADD user_msg_type int unsigned;
+UPDATE &mw_prefix.user_newtalk SET user_msg_type=0;
+ALTER TABLE &mw_prefix.user_newtalk MODIFY user_msg_type INTEGER NOT NULL default 0;
+
+DROP INDEX &mw_prefix.un_user_id ON user_newtalk;
+DROP INDEX &mw_prefix.un_user_ip ON user_newtalk;
\ No newline at end of file
diff --git a/maintenance/oracle/tables.sql b/maintenance/oracle/tables.sql
index 3722120..ee7979b 100644
--- a/maintenance/oracle/tables.sql
+++ b/maintenance/oracle/tables.sql
@@ -48,10 +48,11 @@ CREATE TABLE &mw_prefix.user_newtalk (
user_id NUMBER DEFAULT 0 NOT NULL,
user_ip VARCHAR2(40) NULL,
user_last_timestamp TIMESTAMP(6) WITH TIME ZONE
+ user_msg_type NUMBER DEFAULT 0 NOT NULL,
);
ALTER TABLE &mw_prefix.user_newtalk ADD CONSTRAINT &mw_prefix.user_newtalk_fk1 FOREIGN KEY (user_id) REFERENCES &mw_prefix.mwuser(user_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
-CREATE INDEX &mw_prefix.user_newtalk_i01 ON &mw_prefix.user_newtalk (user_id);
-CREATE INDEX &mw_prefix.user_newtalk_i02 ON &mw_prefix.user_newtalk (user_ip);
+CREATE UNIQUE INDEX &mw_prefix.user_newtalk_i01 ON &mw_prefix.user_newtalk (user_id, user_msg_type);
+CREATE UNIQUE INDEX &mw_prefix.user_newtalk_i02 ON &mw_prefix.user_newtalk (user_ip, user_msg_type);
CREATE TABLE &mw_prefix.user_properties (
up_user NUMBER NOT NULL,
diff --git a/maintenance/postgres/archives/patch-user_newtalk_type.sql b/maintenance/postgres/archives/patch-user_newtalk_type.sql
new file mode 100644
index 0000000..277f209
--- /dev/null
+++ b/maintenance/postgres/archives/patch-user_newtalk_type.sql
@@ -0,0 +1,16 @@
+-- Add the user_msg_type column to user_newtalk and create indices
+
+ALTER TABLE user_newtalk
+ ADD user_msg_type int unsigned;
+
+UPDATE user_newtalk
+ SET user_msg_type=0;
+
+ALTER TABLE user_newtalk
+ MODIFY user_msg_type INTEGER NOT NULL default 0;
+
+DROP INDEX un_user_id ON user_newtalk;
+DROP INDEX un_user_ip ON user_newtalk;
+
+CREATE UNIQUE INDEX un_user_id ON user_newtalk (user_id, user_msg_type);
+CREATE UNIQUE INDEX un_user_ip ON user_newtalk (user_ip, user_msg_type);
\ No newline at end of file
diff --git a/maintenance/postgres/tables.sql b/maintenance/postgres/tables.sql
index 1e3eecb..8aba4ba 100644
--- a/maintenance/postgres/tables.sql
+++ b/maintenance/postgres/tables.sql
@@ -62,9 +62,10 @@ CREATE TABLE user_newtalk (
user_id INTEGER NOT NULL REFERENCES mwuser(user_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
user_ip TEXT NULL,
user_last_timestamp TIMESTAMPTZ
+ user_msg_type INTEGER NOT NULL DEFAULT 0
);
-CREATE INDEX user_newtalk_id_idx ON user_newtalk (user_id);
-CREATE INDEX user_newtalk_ip_idx ON user_newtalk (user_ip);
+CREATE UNIQUE INDEX user_newtalk_id_idx ON user_newtalk (user_id, user_msg_type);
+CREATE UNIQUE INDEX user_newtalk_ip_idx ON user_newtalk (user_ip, user_msg_type);
CREATE SEQUENCE page_page_id_seq;
diff --git a/maintenance/sqlite/archives/patch-user_newtalk_type.sql b/maintenance/sqlite/archives/patch-user_newtalk_type.sql
new file mode 100644
index 0000000..6e18fac
--- /dev/null
+++ b/maintenance/sqlite/archives/patch-user_newtalk_type.sql
@@ -0,0 +1,16 @@
+-- Add the user_msg_type column to user_newtalk and create indices
+
+ALTER TABLE /*_*/user_newtalk
+ ADD user_msg_type int unsigned;
+
+UPDATE /*_*/user_newtalk
+ SET user_msg_type=0;
+
+ALTER TABLE /*_*/user_newtalk
+ MODIFY user_msg_type int unsigned NOT NULL default 0;
+
+DROP INDEX /*i*/un_user_id ON /*_*/user_newtalk;
+DROP INDEX /*i*/un_user_ip ON /*_*/user_newtalk;
+
+CREATE UNIQUE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id, user_msg_type);
+CREATE UNIQUE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip, user_msg_type);
\ No newline at end of file
diff --git a/maintenance/tables.sql b/maintenance/tables.sql
index 0a5b2fb..b8bf857 100644
--- a/maintenance/tables.sql
+++ b/maintenance/tables.sql
@@ -181,12 +181,14 @@ CREATE TABLE /*_*/user_newtalk (
user_ip varbinary(40) NOT NULL default '',
-- The highest timestamp of revisions of the talk page viewed
-- by this user
- user_last_timestamp varbinary(14) NULL default NULL
+ user_last_timestamp varbinary(14) NULL default NULL,
+ -- The type of the message
+ user_msg_type int unsigned NOT NULL default 0
) /*$wgDBTableOptions*/;
-- Indexes renamed for SQLite in 1.14
-CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id);
-CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip);
+CREATE UNIQUE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id, user_msg_type);
+CREATE UNIQUE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip, user_msg_type);
--
diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php
index 20181fd..fea3064 100644
--- a/tests/phpunit/includes/MessageTest.php
+++ b/tests/phpunit/includes/MessageTest.php
@@ -30,8 +30,8 @@ class MessageTest extends MediaWikiLangTestCase {
function testMessageParams() {
$this->assertEquals( 'Return to $1.', wfMessage( 'returnto' )->text() );
$this->assertEquals( 'Return to $1.', wfMessage( 'returnto', array() )->text() );
- $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', 'foo', 'bar' )->text() );
- $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', array( 'foo', 'bar' ) )->text() );
+ $this->assertEquals( 'You have new foo.', wfMessage( 'newmessages-here', 'foo' )->text() );
+ $this->assertEquals( 'You have new foo.', wfMessage( 'newmessages-here', array( 'foo' ) )->text() );
}
function testMessageParamSubstitution() {
diff --git a/tests/phpunit/includes/UserMessageTest.php b/tests/phpunit/includes/UserMessageTest.php
new file mode 100644
index 0000000..b74d849
--- /dev/null
+++ b/tests/phpunit/includes/UserMessageTest.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @group Database
+ */
+class UserMessageTest extends MediaWikiTestCase {
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ public function setUp() {
+ global $wgGroupPermissions, $wgRevokePermissions;
+ parent::setUp();
+
+ $this->savedGroupPermissions = $wgGroupPermissions;
+ $this->savedRevokedPermissions = $wgRevokePermissions;
+
+ $this->setUpPermissionGlobals();
+ $this->setUpUser();
+
+ UserMessage::clearMessages( $this->user );
+ }
+
+ public function testDelivery() {
+ $msg = new UserMessage( $this->user, UserMessage::MSG_TALK );
+ $this->assertTrue( $msg->update( UserMessage::SET ) );
+ $this->assertEquals( $msg->getStatus(), UserMessage::SET );
+
+ $msg = new UserMessage( $this->user, UserMessage::MSG_TALK );
+ $msg->load();
+ $this->assertEquals( $msg->getStatus(), UserMessage::SET );
+
+ $msgs = UserMessage::getUserMessages( $this->user );
+ $this->assertGreaterThanOrEqual( count( $msgs ), 1 );
+ $this->assertEquals( $msgs[0]->getStatus(), UserMessage::SET );
+ $this->assertEquals( $msgs[0]->getType(), UserMessage::MSG_TALK );
+ }
+
+ /**
+ * @depends testDelivery
+ */
+ public function testClear() {
+ $msg = new UserMessage( $this->user, UserMessage::MSG_TALK );
+ $this->load();
+ $this->assertEquals( $msg->getStatus(), UserMessage::SET );
+
+ $this->assertTrue( $this->update( UserMessage::NOTSET ) );
+ $this->assertEquals( $msg->getStatus(), UserMessage::NOTSET );
+
+ $msg = new UserMessage( $this->user, UserMessage::MSG_TALK );
+ $this->assertEquals( $msg->getStatus(), UserMessage::NOTSET );
+
+ $msgs = UserMessage::getUserMessages( $this->user );
+ $this->assertEmpty( $msgs );
+ }
+
+ private function setUpUser() {
+ $this->user = new User;
+ $this->user->addGroup( 'unittesters' );
+ }
+
+ private function setUpPermissionGlobals() {
+ global $wgGroupPermissions, $wgRevokePermissions;
+
+ # Data for regular $wgGroupPermissions test
+ $wgGroupPermissions['unittesters'] = array(
+ 'test' => true,
+ 'runtest' => true,
+ 'writetest' => false,
+ 'nukeworld' => false,
+ );
+ $wgGroupPermissions['testwriters'] = array(
+ 'test' => true,
+ 'writetest' => true,
+ 'modifytest' => true,
+ );
+ # Data for regular $wgRevokePermissions test
+ $wgRevokePermissions['formertesters'] = array(
+ 'runtest' => true,
+ );
+ }
+}
\ No newline at end of file
File Metadata
Details
Attached
Mime Type
text/x-diff
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3280
Default Alt Text
patch (52 KB)
Attached To
Mode
T11838: Send notification to account owner on multiple unsuccessful login attempts
Attached
Detach File
Event Timeline
Log In to Comment