From bec69031088f697e72bd49bc1ec6e6977920bf7a Mon Sep 17 00:00:00 2001 From: Victor Vasiliev Date: Sun, 28 Sep 2008 16:08:18 +0000 Subject: [PATCH] * (bug 674) Allow users to be blocked from editing a specific article ** Also supports blocking user from editing whole namespace * Replace ugly ipboptions parsing code in Title.php with a simple message Requires schema change (I showed it to Tim Starling). --- RELEASE-NOTES | 3 +- includes/AutoLoader.php | 1 + includes/DefaultSettings.php | 8 + includes/LogPage.php | 14 ++ includes/SpecialPage.php | 3 + includes/Title.php | 45 +++-- includes/User.php | 54 ++++- includes/UserRestriction.php | 158 +++++++++++++++ .../specials/SpecialListUserRestrictions.php | 162 +++++++++++++++ .../specials/SpecialRemoveRestrictions.php | 62 ++++++ includes/specials/SpecialRestrictUser.php | 187 ++++++++++++++++++ languages/Language.php | 14 ++ languages/messages/MessagesEn.php | 84 ++++++++ .../archives/patch-user_restrictions.sql | 40 ++++ maintenance/tables.sql | 40 ++++ maintenance/updaters.inc | 5 +- 16 files changed, 857 insertions(+), 23 deletions(-) create mode 100644 includes/UserRestriction.php create mode 100644 includes/specials/SpecialListUserRestrictions.php create mode 100644 includes/specials/SpecialRemoveRestrictions.php create mode 100644 includes/specials/SpecialRestrictUser.php create mode 100644 maintenance/archives/patch-user_restrictions.sql diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 83d8585fbd..ace79ce354 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -144,7 +144,8 @@ The following extensions are migrated into MediaWiki 1.14: query string use the article path, rather than the script path * Added the ability to set the target attribute on external links with $wgExternalLinkTarget - +* (bug 674) Allow admins to block users from editing specific articles and + namespaces === Bug fixes in 1.14 === diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 7349bde9ec..daa3cd8d54 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -197,6 +197,7 @@ $wgAutoloadLocalClasses = array( 'UserArray' => 'includes/UserArray.php', 'UserArrayFromResult' => 'includes/UserArray.php', 'UserMailer' => 'includes/UserMailer.php', + 'UserRestriction' => 'includes/UserRestriction.php', 'UserRightsProxy' => 'includes/UserRightsProxy.php', 'WatchedItem' => 'includes/WatchedItem.php', 'WatchlistEditor' => 'includes/WatchlistEditor.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 3521bf1d72..62559d4283 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1176,6 +1176,7 @@ $wgGroupPermissions['sysop']['suppressredirect'] = true; $wgGroupPermissions['sysop']['apihighlimits'] = true; $wgGroupPermissions['sysop']['browsearchive'] = true; $wgGroupPermissions['sysop']['noratelimit'] = true; +$wgGroupPermissions['sysop']['restrict'] = true; #$wgGroupPermissions['sysop']['mergehistory'] = true; // Permission to change users' group assignments @@ -2661,6 +2662,7 @@ $wgLogTypes = array( '', 'patrol', 'merge', 'suppress', + 'restrict', ); /** @@ -2691,6 +2693,7 @@ $wgLogNames = array( 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', 'suppress' => 'suppressionlog', + 'restrict' => 'restrictionlog', ); /** @@ -2711,6 +2714,7 @@ $wgLogHeaders = array( 'patrol' => 'patrol-log-header', 'merge' => 'mergelogpagetext', 'suppress' => 'suppressionlogtext', + 'restrict' => 'restrictionlogtext', ); /** @@ -2743,6 +2747,8 @@ $wgLogActions = array( 'suppress/event' => 'logdelete-logentry', 'suppress/delete' => 'suppressedarticle', 'suppress/block' => 'blocklogentry', + 'restrict/restrict' => 'restrictentry', + 'restrict/remove' => 'restrictremoveentry', ); /** @@ -2813,6 +2819,8 @@ $wgSpecialPageGroups = array( 'Preferences' => 'users', 'Resetpass' => 'users', 'DeletedContributions' => 'users', + 'ListUserRestrictions' => 'users', + 'RestrictUser' => 'users', 'Mostlinked' => 'highuse', 'Mostlinkedcategories' => 'highuse', diff --git a/includes/LogPage.php b/includes/LogPage.php index 00f2422ade..523559c635 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -193,6 +193,19 @@ class LogPage { } else { $rv = wfMsgForContent( $wgLogActions[$key], $titleLink ); } + } elseif( $type == 'restrict' ) { + if( $params[0] == UserRestriction::PAGE ) + $subj = wfMsgExt( 'restrictlogpage', 'parseinline', $params[1] ); + if( $params[0] == UserRestriction::NAMESPACE ) + $subj = wfMsgExt( 'restrictlognamespace', 'parseinline', $wgLang->getDisplayNsText( $params[1] ) ); + $expiry = ''; + if( $key == 'restrict/restrict' ) + $expiry = $wgLang->translateBlockExpiry( $params[2] ); + if ( $skin ) { + $rv = wfMsg( $wgLogActions[$key], $titleLink, $subj, $expiry ); + } else { + $rv = wfMsgForContent( $wgLogActions[$key], $titleLink, $subj, $expiry ); + } } else { $details = ''; array_unshift( $params, $titleLink ); @@ -260,6 +273,7 @@ class LogPage { } break; case 'rights': + case 'restrict': $text = $wgContLang->ucfirst( $title->getText() ); $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) ); break; diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index fec6c0a88a..b382385e3e 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -126,6 +126,9 @@ class SpecialPage 'Allpages' => 'SpecialAllpages', 'Prefixindex' => 'SpecialPrefixindex', 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), + 'ListUserRestrictions' => array( 'SpecialPage', 'ListUserRestrictions' ), + 'RemoveRestrictions' => array( 'UnlistedSpecialPage', 'RemoveRestrictions', 'restrict' ), + 'RestrictUser' => array( 'SpecialPage', 'RestrictUser', 'restrict' ), 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), 'Contributions' => array( 'SpecialPage', 'Contributions' ), 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), diff --git a/includes/Title.php b/includes/Title.php index ab6a0d0e28..0bef7bc4d0 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1107,9 +1107,7 @@ class Title { } $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); - global $wgContLang; - global $wgLang; - global $wgEmailConfirmToEdit; + global $wgContLang, $wgLang, $wgEmailConfirmToEdit; if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' ) { $errors[] = array( 'confirmedittext' ); @@ -1141,20 +1139,7 @@ class Title { $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true ); if ( $blockExpiry == 'infinity' ) { - // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite' - $scBlockExpiryOptions = wfMsg( 'ipboptions' ); - - foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) { - if ( strpos( $option, ':' ) == false ) - continue; - - list ($show, $value) = explode( ":", $option ); - - if ( $value == 'infinite' || $value == 'indefinite' ) { - $blockExpiry = $show; - break; - } - } + $blockExpiry = wfMsg( 'ipbinfinite' ); } else { $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); } @@ -1164,9 +1149,9 @@ class Title { $errors[] = array( ($block->mAuto ? 'autoblockedtext' : 'blockedtext'), $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ); } - + // Remove the errors being ignored. - + foreach( $errors as $index => $error ) { $error_key = is_array($error) ? $error[0] : $error; @@ -1189,6 +1174,8 @@ class Title { * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems. */ private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) { + global $wgLang; + wfProfileIn( __METHOD__ ); $errors = array(); @@ -1334,6 +1321,26 @@ class Title { $errors[] = $return; } + // Check per-user restrictions + if( $action != 'read' ) { + $r = $user->getRestrictionForPage( $this ); + if( !$r ) + $r = $user->getRestrictionForNamespace( $this->getNamespace() ); + if( $r ) { + $start = $wgLang->timeanddate( $r->getTimestamp() ); + $end = $r->getExpiry() == 'infinity' ? + wfMsg( 'ipbinfinite' ) : + $wgLang->timeanddate( $r->getExpiry() ); + if( $r->isPage() ) + $errors[] = array( 'userrestricted-page', $this->getFullText(), + $r->getBlockerText(), $r->getReason(), $start, $end ); + elseif( $r->isNamespace() ) { + $errors[] = array( 'userrestricted-namespace', $wgLang->getDisplayNsText( $this->getNamespace() ), + $r->getBlockerText(), $r->getReason(), $start, $end ); + } + } + } + wfProfileOut( __METHOD__ ); return $errors; } diff --git a/includes/User.php b/includes/User.php index f7d7880cfc..2efe9f109e 100644 --- a/includes/User.php +++ b/includes/User.php @@ -118,6 +118,8 @@ class User { 'mEditCount', // user_group table 'mGroups', + // user_restrictions table + 'mRestrictions', ); /** @@ -156,6 +158,7 @@ class User { 'proxyunbannable', 'purge', 'read', + 'restrict', 'reupload', 'reupload-shared', 'rollback', @@ -177,7 +180,8 @@ class User { //@{ var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, - $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups; + $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, + $mRestrictions; //@} /** @@ -297,6 +301,7 @@ class User { function saveToCache() { $this->load(); $this->loadGroups(); + $this->loadRestrictions(); if ( $this->isAnon() ) { // Anonymous users are uncached return; @@ -878,6 +883,7 @@ class User { # Initialise user table data $this->loadFromRow( $s ); $this->mGroups = null; // deferred + $this->mRestrictions = null; $this->getEditCount(); // revalidation for nulls return true; } else { @@ -3236,4 +3242,50 @@ class User { return true; } + // Restrictions-related block + + public function loadRestrictions() { + if( !$this->mRestrictions ) + $this->mRestrictions = UserRestriction::fetchForUser( $this->isLoggedIn() ? + intval( $this->getId() ) : $this->getName() ); + } + + public function getRestrictions() { + $this->loadRestrictions(); + + // Check for expired restrictions. Recache if found expired ones + static $checked = false; + if( !$checked ) { + $expired = false; + $old = $this->mRestrictions; + $this->mRestrictions = array(); + foreach( $old as $restriction ) { + if( $restriction->deleteIfExpired() ) + $expired = true; + else + $this->mRestrictions[] = $restriction; + } + if( $expired ) + $this->saveToCache(); + $checked = true; + } + + return $this->mRestrictions; + } + + public function getRestrictionForPage( Title $page ) { + foreach( $this->getRestrictions() as $r ) { + if( $r->isPage() && $page->equals( $r->getPage() ) ) + return $r; + } + return null; + } + + public function getRestrictionForNamespace( $nsid ) { + foreach( $this->getRestrictions() as $r ) { + if( $r->isNamespace() && $r->getNamespace() == $nsid ) + return $r; + } + return null; + } } diff --git a/includes/UserRestriction.php b/includes/UserRestriction.php new file mode 100644 index 0000000000..e90981fa62 --- /dev/null +++ b/includes/UserRestriction.php @@ -0,0 +1,158 @@ +mId = $row->ur_id; + $obj->mType = $row->ur_type; + if( $obj->mType == self::PAGE ) { + $obj->mPage = Title::makeTitle( $row->ur_page_namespace, $row->ur_page_title ); + } elseif( $obj->mType == self::NAMESPACE ) { + $obj->mNamespace = $row->ur_namespace; + } else { + throw new MWException( "Unknown user restriction type: {$row->ur_type}" ); + } + + $obj->mSubjectId = $row->ur_user; + $obj->mSubjectText = $row->ur_user_text; + $obj->mBlockerId = $row->ur_by; + $obj->mBlockerText = $row->ur_by_text; + $obj->mReason = $row->ur_reason; + $obj->mTimestamp = wfTimestamp( TS_MW, $row->ur_timestamp ); + $obj->mExpiry = $row->ur_expiry; + return $obj; + } + + public static function fetchForUser( $user, $forWrite = false ) { + $dbr = wfGetDB( $forWrite ? DB_MASTER : DB_SLAVE ); + if( is_int( $user ) ) + $query = array( 'ur_user' => $user ); + else + $query = array( 'ur_user_text' => $user ); + $res = $dbr->select( 'user_restrictions', '*', $query, __METHOD__ ); + $result = array(); + foreach( $res as $row ) { + $result[] = self::newFromRow( $row ); + } + return $result; + } + + public static function newFromId( $id, $forWrite = false ) { + $dbr = wfGetDB( $forWrite ? DB_MASTER : DB_SLAVE ); + if( !$id || !is_numeric( $id ) ) + return null; + $res = $dbr->selectRow( 'user_restrictions', '*', array( 'ur_id' => $id ), __METHOD__ ); + return self::newFromRow( $res ); + } + + public function getId() { return $this->mId; } + public function setId( $v ) { $this->mId = $v; } + public function getType() { return $this->mType; } + public function setType( $v ) { $this->mType = $v; } + public function getNamespace() { return $this->mNamespace; } + public function setNamespace( $v ) { $this->mNamespace = $v; } + public function getPage() { return $this->mPage; } + public function setPage( $v ) { $this->mPage = $v; } + public function getSubjectId() { return $this->mSubjectId; } + public function setSubjectId( $v ) { $this->mSubjectId = $v; } + public function getSubjectText() { return $this->mSubjectText; } + public function setSubjectText( $v ) { $this->mSubjectText = $v; } + public function getBlockerId() { return $this->mBlockerId; } + public function setBlockerId( $v ) { $this->mBlockerId = $v; } + public function getBlockerText() { return $this->mBlockerText; } + public function setBlockerText( $v ) { $this->mBlockerText = $v; } + public function getReason() { return $this->mReason; } + public function setReason( $v ) { $this->mReason = $v; } + public function getTimestamp() { return $this->mTimestamp; } + public function setTimestamp( $v ) { $this->mTimestamp = $v; } + public function getExpiry() { return $this->mExpiry; } + public function setExpiry( $v ) { $this->mExpiry = $v; } + + public function isPage() { + return $this->mType == self::PAGE; + } + public function isNamespace() { + return $this->mType == self::NAMESPACE; + } + + public function isExpired() { + return is_numeric( $this->mExpiry ) && $this->mExpiry < wfTimestampNow( TS_MW ); + } + + public function deleteIfExpired() { + if( $this->isExpired() ) { + $this->delete(); + return true; + } else { + return false; + } + } + + public function delete() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'user_restrictions', array( 'ur_id' => $this->mId ), __METHOD__ ); + return $dbw->affectedRows(); + } + + public static function purgeExpired() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'user_restrictions', array( 'ur_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); + } + + public function commit() { + $dbw = wfGetDB( DB_MASTER ); + $this->setId( $dbw->nextSequenceValue('user_restrictions_ur_id_val') ); + $row = array( + 'ur_id' => $this->mId, + 'ur_type' => $this->mType, + 'ur_user' => $this->mSubjectId, + 'ur_user_text' => $this->mSubjectText, + 'ur_by' => $this->mBlockerId, + 'ur_by_text' => $this->mBlockerText, + 'ur_reason' => $this->mReason, + 'ur_timestamp' => $dbw->timestamp( $this->mTimestamp ), + 'ur_expiry' => $this->mExpiry, + ); + if( $this->isPage() ) { + $row['ur_page_namespace'] = $this->mPage->getNamespace(); + $row['ur_page_title'] = $this->mPage->getDbKey(); + } + if( $this->isNamespace() ) { + $row['ur_namespace'] = $this->mNamespace; + } + $dbw->insert( 'user_restrictions', $row, __METHOD__ ); + } + + public static function formatType( $type ) { + return wfMsg( 'userrestrictiontype-' . $type ); + } + + /** + * Converts expiry which user input to the internal representation. + * Returns false if invalid expiry is set, Block::infinity() on empty value, + * Block::infinity() on infinity or 14-symbol timestamp + */ + public static function convertExpiry( $expiry ) { + if( !$expiry ) + return Block::infinity(); + if( in_array( $expiry, array( 'infinite', 'infinity', 'indefinite' ) ) ) + return Block::infinity(); + $unix = @strtotime( $expiry ); + if( !$unix || $unix === -1 ) + return false; + else + return wfTimestamp( TS_MW, $unix ); + } +} diff --git a/includes/specials/SpecialListUserRestrictions.php b/includes/specials/SpecialListUserRestrictions.php new file mode 100644 index 0000000000..3c986db9eb --- /dev/null +++ b/includes/specials/SpecialListUserRestrictions.php @@ -0,0 +1,162 @@ +addWikiMsg( 'listuserrestrictions-intro' ); + $f = new SpecialListUserRestrictionsForm(); + $wgOut->addHTML( $f->getHTML() ); + + if( !mt_rand( 0, 10 ) ) + UserRestriction::purgeExpired(); + $pager = new UserRestrictionsPager( $f->getConds() ); + if( $pager->getNumRows() ) + $wgOut->addHTML( $pager->getNavigationBar() . + Xml::tags( 'ul', null, $pager->getBody() ) . + $pager->getNavigationBar() + ); + elseif( $f->getConds() ) + $wgOut->addWikiMsg( 'listuserrestrictions-notfound' ); + else + $wgOut->addWikiMsg( 'listuserrestrictions-empty' ); +} + +class SpecialListUserRestrictionsForm { + public function getHTML() { + global $wgRequest, $wgScript, $wgTitle; + $s = ''; + $legend = wfMsgHtml( 'listuserrestrictions-legend' ); + $s .= "
{$legend}"; + $s .= "
"; + $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); + $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-type' ), 'type' ) . ' ' . + self::typeSelector( 'type', $wgRequest->getVal( 'type' ), 'type' ); + $s .= ' '; + $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-user' ), 'user', 'user', + false, $wgRequest->getVal( 'user' ) ); + $s .= '

'; + $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-namespace' ), 'namespace' ) . ' ' . + Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ), '', false, 'namespace' ); + $s .= ' '; + $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-page' ), 'page', 'page', + false, $wgRequest->getVal( 'page' ) ); + $s .= Xml::submitButton( wfMsgHtml( 'listuserrestrictions-submit' ) ); + $s .= "

"; + return $s; + } + + public static function typeSelector( $name = 'type', $value = '', $id = false ) { + $s = new XmlSelect( $name, $id, $value ); + $s->addOption( wfMsg( 'userrestrictiontype-none' ), '' ); + $s->addOption( wfMsg( 'userrestrictiontype-page' ), UserRestriction::PAGE ); + $s->addOption( wfMsg( 'userrestrictiontype-namespace' ), UserRestriction::NAMESPACE ); + return $s->getHTML(); + } + + public function getConds() { + global $wgRequest; + $conds = array(); + + $type = $wgRequest->getVal( 'type' ); + if( in_array( $type, array( UserRestriction::PAGE, UserRestriction::NAMESPACE ) ) ) + $conds['ur_type'] = $type; + + $user = $wgRequest->getVal( 'user' ); + if( $user ) + $conds['ur_user_text'] = $user; + + $namespace = $wgRequest->getVal( 'namespace' ); + if( $namespace || $namespace === '0' ) + $conds['ur_namespace'] = $namespace; + + $page = $wgRequest->getVal( 'page' ); + $title = Title::newFromText( $page ); + if( $title ) { + $conds['ur_page_namespace'] = $title->getNamespace(); + $conds['ur_page_title'] = $title->getDbKey(); + } + + return $conds; + } +} + +class UserRestrictionsPager extends ReverseChronologicalPager { + public $mConds; + + public function __construct( $conds = array() ) { + $this->mConds = $conds; + parent::__construct(); + } + + public function getStartBody() { + # Copied from Special:Ipblocklist + wfProfileIn( __METHOD__ ); + # Do a link batch query + $this->mResult->seek( 0 ); + $lb = new LinkBatch; + + # Faster way + # Usernames and titles are in fact related by a simple substitution of space -> underscore + # The last few lines of Title::secureAndSplit() tell the story. + foreach( $this->mResult as $row ) { + $name = str_replace( ' ', '_', $row->ur_by_text ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + $name = str_replace( ' ', '_', $row->ur_user_text ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + if( $row->ur_type == UserRestriction::PAGE ) + $lb->add( $row->ur_page_namespace, $row->ur_page_title ); + } + $lb->execute(); + wfProfileOut( __METHOD__ ); + return ''; + } + + public function getQueryInfo() { + return array( + 'tables' => 'user_restrictions', + 'fields' => '*', + 'conds' => $this->mConds, + ); + } + + public function formatRow( $row ) { + return self::formatRestriction( UserRestriction::newFromRow( $row ) ); + } + + // Split off for use on Special:RestrictUser + public static function formatRestriction( $r ) { + global $wgUser, $wgLang; + $sk = $wgUser->getSkin(); + $timestamp = $wgLang->timeanddate( $r->getTimestamp(), true ); + $blockerlink = $sk->userLink( $r->getBlockerId(), $r->getBlockerText() ) . + $sk->userToolLinks( $r->getBlockerId(), $r->getBlockerText() ); + $subjlink = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) . + $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() ); + $expiry = is_numeric( $r->getExpiry() ) ? + wfMsg( 'listuserrestrictions-row-expiry', $wgLang->timeanddate( $r->getExpiry() ) ) : + wfMsg( 'ipbinfinite' ); + $msg = ''; + if( $r->isNamespace() ) { + $msg = wfMsgHtml( 'listuserrestrictions-row-ns', $subjlink, + $wgLang->getDisplayNsText( $r->getNamespace() ), $expiry ); + } + if( $r->isPage() ) { + $pagelink = $sk->link( $r->getPage() ); + $msg = wfMsgHtml( 'listuserrestrictions-row-page', $subjlink, + $pagelink, $expiry ); + } + $reason = $sk->commentBlock( $r->getReason() ); + $removelink = ''; + if( $wgUser->isAllowed( 'restrict' ) ) { + $removelink = '(' . $sk->link( SpecialPage::getTitleFor( 'RemoveRestrictions' ), + wfMsgHtml( 'listuserrestrictions-remove' ), array(), array( 'id' => $r->getId() ) ) . ')'; + } + return "
  • {$timestamp}, {$blockerlink} {$msg} {$reason} {$removelink}
  • \n"; + } + + public function getIndexField() { + return 'ur_timestamp'; + } +} diff --git a/includes/specials/SpecialRemoveRestrictions.php b/includes/specials/SpecialRemoveRestrictions.php new file mode 100644 index 0000000000..2c157aec87 --- /dev/null +++ b/includes/specials/SpecialRemoveRestrictions.php @@ -0,0 +1,62 @@ +getSkin(); + + $id = $wgRequest->getVal( 'id' ); + if( !is_numeric( $id ) ) { + $wgOut->addWikiMsg( 'removerestrictions-noid' ); + return; + } + + UserRestriction::purgeExpired(); + $r = UserRestriction::newFromId( $id, true ); + if( !$r ) { + $wgOut->addWikiMsg( 'removerestrictions-wrongid' ); + return; + } + + $legend = wfMsgHtml( 'removerestrictions-legend' ); + + $form = array(); + $form['removerestrictions-user'] = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) . + $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() ); + $form['removerestrictions-type'] = UserRestriction::formatType( $r->getType() ); + if( $r->isPage() ) + $form['removerestrictions-page'] = $sk->link( $r->getPage() ); + if( $r->isNamespace() ) + $form['removerestrictions-namespace'] = $wgLang->getDisplayNsText( $r->getNamespace() ); + $form['removerestrictions-reason'] = Xml::input( 'reason' ); + + $result = null; + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) + $result = wfSpecialRemoveRestrictionsProcess( $r ); + + $wgOut->addWikiMsg( 'removerestrictions-intro' ); + $wgOut->addHTML( "
    {$legend}" ); + if( $result ) + $wgOut->addHTML( '' . wfMsgExt( 'removerestrictions-success', + 'parseinline', $r->getSubjectText() ) . '' ); + $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl( array( 'id' => $id ) ), + 'method' => 'post' ) ) ); + $wgOut->addHTML( Xml::buildForm( $form, 'removerestrictions-submit' ) ); + $wgOut->addHTML( Xml::hidden( 'id', $r->getId() ) ); + $wgOut->addHTML( Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) ); + $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); + $wgOut->addHTML( "
    " ); +} + +function wfSpecialRemoveRestrictionsProcess( $r ) { + global $wgUser, $wgRequest; + $reason = $wgRequest->getVal( 'reason' ); + $result = $r->delete(); + $log = new LogPage( 'restrict' ); + $params = array( $r->getType() ); + if( $r->isPage() ) + $params[] = $r->getPage()->getPrefixedDbKey(); + if( $r->isNamespace() ) + $params[] = $r->getNamespace(); + $log->addEntry( 'remove', Title::makeTitle( NS_USER, $r->getSubjectText() ), $reason, $params ); + return $result; +} diff --git a/includes/specials/SpecialRestrictUser.php b/includes/specials/SpecialRestrictUser.php new file mode 100644 index 0000000000..8ce4b042db --- /dev/null +++ b/includes/specials/SpecialRestrictUser.php @@ -0,0 +1,187 @@ +getVal( 'user' ) ) { + $userOrig = $wgRequest->getVal( 'user' ); + } else { + $wgOut->addHTML( RestrictUserForm::selectUserForm() ); + return; + } + $isIP = User::isIP( $userOrig ); + if( $isIP ) + $user = $userOrig; + else + $user = User::getCanonicalName( $userOrig ); + $uid = User::idFromName( $user ); + if( !$uid && !$isIP ) { + $err = '' . wfMsgHtml( 'restrictuser-notfound' ) . ''; + $wgOut->addHTML( RestrictUserForm::selectUserForm( $userOrig, $err ) ); + return; + } + $wgOut->addHTML( RestrictUserForm::selectUserForm( $user ) ); + + UserRestriction::purgeExpired(); + $old = UserRestriction::fetchForUser( $user, true ); + + RestrictUserForm::pageRestrictionForm( $uid, $user, $old ); + RestrictUserForm::namespaceRestrictionForm( $uid, $user, $old ); + + $old = UserRestriction::fetchForUser( $user, true ); // Renew it after possible changes in previous two functions + if( $old ) { + $wgOut->addHTML( RestrictUserForm::existingRestrictions( $old ) ); + } +} + +class RestrictUserForm { + public static function selectUserForm( $val = null, $error = null ) { + global $wgScript, $wgTitle; + $legend = wfMsgHtml( 'restrictuser-userselect' ); + $s = "
    {$legend}
    "; + if( $error ) + $s .= '

    ' . $error . '

    '; + $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); + $form = array( 'restrictuser-user' => Xml::input( 'user', false, $val ) ); + $s .= Xml::buildForm( $form, 'restrictuser-go' ); + $s .= "
    "; + return $s; + } + + public static function existingRestrictions( $restrictions ) { + require_once( dirname( __FILE__ ) . '/SpecialListUserRestrictions.php' ); + $legend = wfMsgHtml( 'restrictuser-existing' ); + $s = "
    {$legend}
    "; + return $s; + } + + public static function pageRestrictionForm( $uid, $user, $oldRestrictions ) { + global $wgOut, $wgTitle, $wgRequest, $wgUser; + $error = ''; + $success = false; + if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::PAGE && + $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) { + $title = Title::newFromText( $wgRequest->getVal( 'page' ) ); + if( !$title ) + $error = wfMsgExt( 'restrictuser-badtitle', 'parseinline', $wgRequest->getVal( 'page' ) ); + elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) + $error = wfMsgExt( 'restrictuser-badexpiry', 'parseinline', $wgRequest->getVal( 'expiry' ) ); + else + foreach( $oldRestrictions as $r ) + if( $r->isPage() && $r->getPage()->equals( $title ) ) + $error = wfMsgExt( 'restrictuser-duptitle', 'parse' ); + if( !$error ) { + self::doPageRestriction( $uid, $user ); + $success = true; + } + } + $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::PAGE; + $legend = wfMsgHtml( 'restrictuser-legend-page' ); + $wgOut->addHTML( "
    {$legend}" ); + if( $error ) + $wgOut->addHTML( '' . $error . '' ); + if( $success ) + $wgOut->addHTML( '' . wfMsgExt( 'restrictuser-success', + 'parseinline', $user ) . '' ); + $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(), + 'method' => 'post' ) ) ); + $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::PAGE ) ); + $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); + $wgOut->addHTML( Xml::hidden( 'user', $user ) ); + $form = array(); + $form['restrictuser-title'] = Xml::input( 'page', false, + $useRequestValues ? $wgRequest->getVal( 'page' ) : false ); + $form['restrictuser-expiry'] = Xml::input( 'expiry', false, + $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false ); + $form['restrictuser-reason'] = Xml::input( 'reason', false, + $useRequestValues ? $wgRequest->getVal( 'reason' ) : false ); + $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-sumbit' ) ); + $wgOut->addHTML( "
    " ); + } + + public static function doPageRestriction( $uid, $user ) { + global $wgUser, $wgRequest; + $r = new UserRestriction(); + $r->setType( UserRestriction::PAGE ); + $r->setPage( Title::newFromText( $wgRequest->getVal( 'page' ) ) ); + $r->setSubjectId( $uid ); + $r->setSubjectText( $user ); + $r->setBlockerId( $wgUser->getId() ); + $r->setBlockerText( $wgUser->getName() ); + $r->setReason( $wgRequest->getVal( 'reason' ) ); + $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) ); + $r->setTimestamp( wfTimestampNow( TS_MW ) ); + $r->commit(); + $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity(); + $l = new LogPage( 'restrict' ); + $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(), + array( $r->getType(), $r->getPage()->getFullText(), $logExpiry) ); + } + + public static function namespaceRestrictionForm( $uid, $user, $oldRestrictions ) { + global $wgOut, $wgTitle, $wgRequest, $wgUser, $wgContLang; + $error = ''; + $success = false; + if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE && + $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) { + $ns = $wgRequest->getVal( 'namespace' ); + if( $wgContLang->getNsText( $ns ) === false ) + $error = wfMsgExt( 'restrictuser-badnamespace', 'parseinline' ); + elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) + $error = wfMsgExt( 'restrictuser-badexpiry', 'parseinline', $wgRequest->getVal( 'expiry' ) ); + else + foreach( $oldRestrictions as $r ) + if( $r->isNamespace() && $r->getNamespace() == $ns ) + $error = wfMsgExt( 'restrictuser-dupnamespace', 'parse' ); + if( !$error ) { + self::doNamespaceRestriction( $uid, $user ); + $success = true; + } + } + $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE; + $legend = wfMsgHtml( 'restrictuser-legend-namespace' ); + $wgOut->addHTML( "
    {$legend}" ); + if( $error ) + $wgOut->addHTML( '' . $error . '' ); + if( $success ) + $wgOut->addHTML( '' . wfMsgExt( 'restrictuser-success', + 'parseinline', $user ) . '' ); + $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(), + 'method' => 'post' ) ) ); + $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::NAMESPACE ) ); + $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); + $wgOut->addHTML( Xml::hidden( 'user', $user ) ); + $form = array(); + $form['restrictuser-namespace'] = Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ) ); + $form['restrictuser-expiry'] = Xml::input( 'expiry', false, + $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false ); + $form['restrictuser-reason'] = Xml::input( 'reason', false, + $useRequestValues ? $wgRequest->getVal( 'reason' ) : false ); + $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-sumbit' ) ); + $wgOut->addHTML( "
    " ); + } + + public static function doNamespaceRestriction( $uid, $user ) { + global $wgUser, $wgRequest; + $r = new UserRestriction(); + $r->setType( UserRestriction::NAMESPACE ); + $r->setNamespace( $wgRequest->getVal( 'namespace' ) ); + $r->setSubjectId( $uid ); + $r->setSubjectText( $user ); + $r->setBlockerId( $wgUser->getId() ); + $r->setBlockerText( $wgUser->getName() ); + $r->setReason( $wgRequest->getVal( 'reason' ) ); + $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) ); + $r->setTimestamp( wfTimestampNow( TS_MW ) ); + $r->commit(); + $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity(); + $l = new LogPage( 'restrict' ); + $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(), + array( $r->getType(), $r->getNamespace(), $logExpiry ) ); + } +} diff --git a/languages/Language.php b/languages/Language.php index fa485d1427..29e8a882bf 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -264,6 +264,20 @@ class Language { return strtr($ns, '_', ' '); } + /** + * A convenience function that returns the same thing as + * getFormattedNsText() except with '(Main)' for zero namespace. + * + * @return array + */ + function getDisplayNsText( $index ) { + if( $index == 0 ) { + return wfMsg( 'blanknamespace' ); + } else { + return $this->getFormattedNsText( $index ); + } + } + /** * Get a namespace key by value, case insensitive. * Only matches namespace names for the current language, not the diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 7588d753d5..f374e57b2c 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -439,6 +439,9 @@ $specialPageAliases = array( 'Blankpage' => array( 'BlankPage' ), 'LinkSearch' => array( 'LinkSearch' ), 'DeletedContributions' => array( 'DeletedContributions' ), + 'ListUserRestrictions' => array( 'ListUserRestrictions' ), + 'RemoveRestrictions' => array( 'RemoveRestrictions' ), + 'RestrictUser' => array( 'RestrictUser' ), ); /** @@ -1197,6 +1200,22 @@ It appears to have been deleted.', 'edit-no-change' => 'Your edit was ignored, because no change was made to the text.', 'edit-already-exists' => 'Could not create a new page. It already exists.', +'userrestricted-page' => "'''Your user name or IP address has been restricted from editing page \"$1\".''' + +The restriction was put by [[User:$2|$2]]. +The reason given is ''$3''. + +Restriction was put at $4 and expires at $5. + +You can contact [[User:$2|$2]] or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the restriction.", +'userrestricted-namespace' => "'''Your user name or IP address has been restricted from editing $1 namespace.''' + +The restriction was put by [[User:$2|$2]]. +The reason given is ''$3''. + +Restriction was put at $4 and expires at $5. + +You can contact [[User:$2|$2]] or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the restriction.", # Parser/template warnings 'expensive-parserfunction-warning' => 'Warning: This page contains too many expensive parser function calls. @@ -1637,6 +1656,7 @@ please see math/README to configure.', 'right-userrights' => 'Edit all user rights', 'right-userrights-interwiki' => 'Edit user rights of users on other wikis', 'right-siteadmin' => 'Lock and unlock the database', +'right-restrict' => 'Restrict user from editing certain namespaces and pages', # User rights log 'rightslog' => 'User rights log', @@ -2497,6 +2517,7 @@ Fill in a specific reason below (for example, citing particular pages that were 'ipbsubmit' => 'Block this user', 'ipbother' => 'Other time:', 'ipboptions' => '2 hours:2 hours,1 day:1 day,3 days:3 days,1 week:1 week,2 weeks:2 weeks,1 month:1 month,3 months:3 months,6 months:6 months,1 year:1 year,infinite:infinite', # display1:time1,display2:time2,... +'ipbinfinite' => 'infinite', 'ipbotheroption' => 'other', 'ipbotherreason' => 'Other/additional reason:', 'ipbhidename' => 'Hide username from the block log, active block list and user list', @@ -2569,6 +2590,69 @@ Please contact your Internet service provider or tech support and inform them of You cannot create an account', 'cant-block-while-blocked' => 'You cannot block other users while you are blocked.', +# Special:ListUserRestrictions +'listuserrestrictions' => 'List of user restrictions', +'listuserrestrictions-intro' => 'This list contains all restrictions from editing certain pages and namespaces put on users. +Note that blocks are listed [[Special:Ipblocklist|there]] and are not listed here.', +'listuserrestrictions-row-ns' => 'restricted $1 from editing $2 namespace ($3)', +'listuserrestrictions-row-page' => 'restricted $1 from editing $2 ($3)', +'listuserrestrictions-row-expiry' => 'expires at $1', +'listuserrestrictions-legend' => 'Find a restriction', +'listuserrestrictions-type' => 'Type:', +'listuserrestrictions-user' => 'User:', +'listuserrestrictions-namespace' => 'Namespace:', +'listuserrestrictions-page' => 'Page:', +'listuserrestrictions-submit' => 'Go', +'listuserrestrictions-notfound' => 'There is no restriction that matches specified criteria.', +'listuserrestrictions-empty' => 'This list is empty.', +'listuserrestrictions-remove' => 'remove', +'userrestrictiontype-none' => '(none)', +'userrestrictiontype-namespace' => 'Namespace', +'userrestrictiontype-page' => 'Page', + +# Special:RemoveRestrictions +'removerestrictions' => 'Remove restriction from a user', +'removerestrictions-intro' => 'Use the form below to remove a restriction from a certain user.', +'removerestrictions-noid' => 'No restriction ID was specified.', +'removerestrictions-wrongid' => 'Restriction with that ID not found. Most probably someone has removed it or it expired.', +'removerestrictions-legend' => 'Remove a restriction', +'removerestrictions-user' => 'Restricted user:', +'removerestrictions-type' => 'Restriction type:', +'removerestrictions-page' => 'Page:', +'removerestrictions-namespace' => 'Namespace:', +'removerestrictions-reason' => 'Reason:', +'removerestrictions-submit' => 'Remove the restriction', +'removerestrictions-success' => 'Successfully removed the restriction from [[User:$1|$1]].', + +# Special:RestrictUser +'restrictuser' => 'Restrict user', +'restrictuser-userselect' => 'Select a user', +'restrictuser-user' => 'User:', +'restrictuser-go' => 'Restrict user', +'restrictuser-notfound' => 'User not found', +'restrictuser-existing' => 'Existing restrictions', +'restrictuser-legend-page' => 'Restrict from editing certain page', +'restrictuser-legend-namespace' => 'Restrict from editing certain namespace', +'restrictuser-title' => 'Page to restrict:', +'restrictuser-namespace' => 'Namespace:', +'restrictuser-expiry' => 'Expires:', +'restrictuser-reason' => 'Reason:', +'restrictuser-sumbit' => 'Restrict user', +'restrictuser-badtitle' => 'Invalid title specified: $1.', +'restrictuser-badnamespace' => 'Invalid namespace specified.', +'restrictuser-badexpiry' => 'Invalid expiry specified: $1.', +'restrictuser-duptitle' => 'User is already restricted from editing this title.', +'restrictuser-dupnamespace' => 'User is already restricted from editing this namespace.', +'restrictuser-success' => 'Successfully restricted user $1.', + +# Special:Log/restrict +'restrictionlog' => 'User restriction log', +'restrictionlogtext' => 'This log contains all restrictions put on users by administrators.', +'restrictentry' => 'restricted $1 from editing $2 (expiry set to $3)', +'restrictremoveentry' => 'removed restriction from $1 for editing $2', +'restrictlognamespace' => '$1 namespace', +'restrictlogpage' => '[[$1]]', + # Developer tools 'lockdb' => 'Lock database', 'unlockdb' => 'Unlock database', diff --git a/maintenance/archives/patch-user_restrictions.sql b/maintenance/archives/patch-user_restrictions.sql new file mode 100644 index 0000000000..b78a108251 --- /dev/null +++ b/maintenance/archives/patch-user_restrictions.sql @@ -0,0 +1,40 @@ +-- Allows admins to block user from editing certain namespaces or pages + +CREATE TABLE /*$wgDBprefix*/user_restrictions ( + -- ID of the restriction + ur_id int NOT NULL auto_increment, + + -- Restriction type. Block from either editing namespace or page + ur_type varbinary(255) NOT NULL, + -- Namespace to restrict if ur_type = namespace + ur_namespace int default NULL, + -- Page to restrict if ur_type = page + ur_page_namespace int default NULL, + ur_page_title varchar(255) binary default '', + + -- User that is restricted + ur_user int unsigned NOT NULL, + ur_user_text tinyblob NOT NULL, + + -- User who has done this restriction + ur_by int unsigned NOT NULL, + ur_by_text varchar(255) binary NOT NULL default '', + -- Reason for this restriction + ur_reason tinyblob NOT NULL, + + -- Time when this restriction was made + ur_timestamp varbinary(14) NOT NULL default '', + -- Expiry or "infinity" + ur_expiry varbinary(14) NOT NULL default '', + + PRIMARY KEY ur_id (ur_id), + -- For looking up restrictions for user + INDEX ur_user (ur_user,ur_user_text(255)), + -- For Special:ListUserRestrictions + INDEX ur_type (ur_type(255),ur_timestamp), + INDEX ur_namespace (ur_namespace,ur_timestamp), + INDEX ur_page (ur_page_namespace,ur_page_title,ur_timestamp), + INDEX ur_timestamp (ur_timestamp), + -- For quick removal of expired restrictions + INDEX ur_expiry (ur_expiry) +) /*$wgDBTableOptions*/; diff --git a/maintenance/tables.sql b/maintenance/tables.sql index d4a60af865..18b9e342ac 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -1243,4 +1243,44 @@ CREATE TABLE /*$wgDBprefix*/updatelog ( PRIMARY KEY (ul_key) ) /*$wgDBTableOptions*/; +-- Allows admins to block user from editing certain namespaces or pages +CREATE TABLE /*$wgDBprefix*/user_restrictions ( + -- ID of the restriction + ur_id int NOT NULL auto_increment, + + -- Restriction type. Block from either editing namespace or page + ur_type varbinary(255) NOT NULL, + -- Namespace to restrict if ur_type = namespace + ur_namespace int default NULL, + -- Page to restrict if ur_type = page + ur_page_namespace int default NULL, + ur_page_title varchar(255) binary default '', + + -- User that is restricted + ur_user int unsigned NOT NULL, + ur_user_text tinyblob NOT NULL, + + -- User who has done this restriction + ur_by int unsigned NOT NULL, + ur_by_text varchar(255) binary NOT NULL default '', + -- Reason for this restriction + ur_reason tinyblob NOT NULL, + + -- Time when this restriction was made + ur_timestamp varbinary(14) NOT NULL default '', + -- Expiry or "infinity" + ur_expiry varbinary(14) NOT NULL default '', + + PRIMARY KEY ur_id (ur_id), + -- For looking up restrictions for user + INDEX ur_user (ur_user,ur_user_text(255)), + -- For Special:ListUserRestrictions + INDEX ur_type (ur_type(255),ur_timestamp), + INDEX ur_namespace (ur_namespace,ur_timestamp), + INDEX ur_page (ur_page_namespace,ur_page_title,ur_timestamp), + INDEX ur_timestamp (ur_timestamp), + -- For quick removal of expired restrictions + INDEX ur_expiry (ur_expiry) +) /*$wgDBTableOptions*/; + -- vim: sw=2 sts=2 et diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index 4980910e86..659fed562d 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -145,9 +145,10 @@ $wgMysqlUpdates = array( array( 'update_password_format' ), // 1.14 - array( 'add_field', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ), + array( 'add_field', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ), array( 'do_active_users_init' ), - array( 'add_field', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ) + array( 'add_field', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ), + array( 'add_table', 'user_restrictions', 'patch-user_restrictions.sql' ), ); -- 2.20.1