From 18d21c9ba0793dbb6c9daea1ffbdd4cb4a8519ad Mon Sep 17 00:00:00 2001 From: Reedy Date: Sat, 18 Nov 2017 21:59:47 +0000 Subject: [PATCH] Add Special:PasswordPolicies Bug: T174812 Change-Id: Ifb4876f7309a667154c7469c29e703b6c33d54af --- autoload.php | 1 + includes/specialpage/SpecialPageFactory.php | 1 + includes/specials/SpecialPasswordPolicies.php | 163 ++++++++++++++++++ languages/i18n/en.json | 14 +- languages/i18n/qqq.json | 15 +- languages/messages/MessagesEn.php | 1 + resources/src/mediawiki.special/special.css | 5 + .../password/PasswordPolicyChecksTest.php | 17 ++ 8 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 includes/specials/SpecialPasswordPolicies.php diff --git a/autoload.php b/autoload.php index c55b931235..c57f408363 100644 --- a/autoload.php +++ b/autoload.php @@ -1446,6 +1446,7 @@ $wgAutoloadLocalClasses = [ 'SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php', 'SpecialPageLanguage' => __DIR__ . '/includes/specials/SpecialPageLanguage.php', 'SpecialPagesWithProp' => __DIR__ . '/includes/specials/SpecialPagesWithProp.php', + 'SpecialPasswordPolicies' => __DIR__ . '/includes/specials/SpecialPasswordPolicies.php', 'SpecialPasswordReset' => __DIR__ . '/includes/specials/SpecialPasswordReset.php', 'SpecialPermanentLink' => __DIR__ . '/includes/specials/SpecialPermanentLink.php', 'SpecialPreferences' => __DIR__ . '/includes/specials/SpecialPreferences.php', diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index fdf4d52d78..b3cb806942 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -112,6 +112,7 @@ class SpecialPageFactory { 'Listbots' => SpecialListBots::class, 'Userrights' => UserrightsPage::class, 'EditWatchlist' => SpecialEditWatchlist::class, + 'PasswordPolicies' => SpecialPasswordPolicies::class, // Recent changes and logs 'Newimages' => SpecialNewFiles::class, diff --git a/includes/specials/SpecialPasswordPolicies.php b/includes/specials/SpecialPasswordPolicies.php new file mode 100644 index 0000000000..415f973525 --- /dev/null +++ b/includes/specials/SpecialPasswordPolicies.php @@ -0,0 +1,163 @@ +setHeaders(); + $this->outputHeader(); + + $out = $this->getOutput(); + $out->addModuleStyles( 'mediawiki.special' ); + + $this->addHelpLink( 'Help:Password policies' ); + + $out->addHTML( + Xml::openElement( 'table', [ 'class' => 'wikitable mw-passwordpolicies-table' ] ) . + '' . + Xml::element( 'th', null, $this->msg( 'passwordpolicies-group' )->text() ) . + Xml::element( 'th', null, $this->msg( 'passwordpolicies-policies' )->text() ) . + '' + ); + + $config = $this->getConfig(); + $policies = $config->get( 'PasswordPolicy' ); + + $groupPermissions = $config->get( 'GroupPermissions' ); + $revokePermissions = $config->get( 'RevokePermissions' ); + $addGroups = $config->get( 'AddGroups' ); + $removeGroups = $config->get( 'RemoveGroups' ); + $groupsAddToSelf = $config->get( 'GroupsAddToSelf' ); + $groupsRemoveFromSelf = $config->get( 'GroupsRemoveFromSelf' ); + $allGroups = array_unique( array_merge( + array_keys( $groupPermissions ), + array_keys( $revokePermissions ), + array_keys( $addGroups ), + array_keys( $removeGroups ), + array_keys( $groupsAddToSelf ), + array_keys( $groupsRemoveFromSelf ) + ) ); + asort( $allGroups ); + + $linkRenderer = $this->getLinkRenderer(); + + foreach ( $allGroups as $group ) { + if ( $group == '*' ) { + continue; + } + + $groupnameLocalized = UserGroupMembership::getGroupName( $group ); + + $grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $group ) + ?: Title::newFromText( MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $group ); + + $grouppage = $linkRenderer->makeLink( + $grouppageLocalizedTitle, + $groupnameLocalized + ); + + if ( $group === 'user' ) { + // Link to Special:listusers for implicit group 'user' + $grouplink = '
' . $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Listusers' ), + $this->msg( 'listgrouprights-members' )->text() + ); + } elseif ( !in_array( $group, $config->get( 'ImplicitGroups' ) ) ) { + $grouplink = '
' . $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Listusers' ), + $this->msg( 'listgrouprights-members' )->text(), + [], + [ 'group' => $group ] + ); + } else { + // No link to Special:listusers for other implicit groups as they are unlistable + $grouplink = ''; + } + + $out->addHTML( Html::rawElement( 'tr', [ 'id' => Sanitizer::escapeIdForAttribute( $group ) ], " + $grouppage$grouplink + " . $this->formatPolicies( $policies, $group ) . ' + ' + ) ); + + } + + $out->addHTML( Xml::closeElement( 'table' ) ); + } + + /** + * Create a HTML list of password policies for $group + * + * @param array $policies Original $wgPasswordPolicy array + * @param array $group Group to format password policies for + * + * @return string HTML list of all applied password policies + */ + private function formatPolicies( $policies, $group ) { + $groupPolicies = UserPasswordPolicy::getPoliciesForGroups( + $policies['policies'], + [ $group ], + $policies['policies']['default'] + ); + + $ret = []; + foreach ( $groupPolicies as $gp => $val ) { + if ( $val === false ) { + // Policy isn't enabled, so no need to dislpay it + continue; + } elseif ( $val === true ) { + $msg = $this->msg( 'passwordpolicies-policy-' . strtolower( $gp ) ); + } else { + $msg = $this->msg( 'passwordpolicies-policy-' . strtolower( $gp ) )->numParams( $val ); + } + $ret[] = $this->msg( + 'passwordpolicies-policy-display', + $msg, + '' . $gp . '' + )->parse(); + } + if ( !count( $ret ) ) { + return ''; + } else { + return ''; + } + } + + protected function getGroupName() { + return 'users'; + } +} diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 236d6e598d..1d98ad1df1 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4468,5 +4468,17 @@ "pagedata-text": "This page provides a data interface to pages. Please provide the page title in the URL, using subpage syntax.\n* Content negotiation applies based on your client's Accept header. This means that the page data will be provided in the format preferred by your client.", "pagedata-not-acceptable": "No matching format found. Supported MIME types: $1", "pagedata-bad-title": "Invalid title: $1.", - "unregistered-user-config": "For security reasons JavaScript, CSS and JSON user subpages cannot be loaded for unregistered users." + "unregistered-user-config": "For security reasons JavaScript, CSS and JSON user subpages cannot be loaded for unregistered users.", + "passwordpolicies": "Password policies", + "passwordpolicies-summary": "This is a list of the effective password policies for the user groups defined in this wiki.", + "passwordpolicies-helppage": "Manual:$wgPasswordPolicy", + "passwordpolicies-group": "Group", + "passwordpolicies-policies": "Policies", + "passwordpolicies-policy-display": "$1 ($2)", + "passwordpolicies-policy-minimalpasswordlength": "Password must be at least $1 {{PLURAL:$1|character|characters}} long", + "passwordpolicies-policy-minimumpasswordlengthtologin": "Password must be at least $1 {{PLURAL:$1|character|characters}} long to be able to login", + "passwordpolicies-policy-passwordcannotmatchusername": "Password cannot be the same as username", + "passwordpolicies-policy-passwordcannotmatchblacklist": "Password cannot match specifically blacklisted passwords", + "passwordpolicies-policy-maximalpasswordlength": "Password must be less than $1 {{PLURAL:$1|character|characters}} long", + "passwordpolicies-policy-passwordcannotbepopular": "Password cannot be {{PLURAL:$1|the popular password|in the list of $1 popular passwords}}" } diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 0947db2173..01ead78c0e 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4666,5 +4666,18 @@ "pagedata-text": "Error shown when none of the formats acceptable to the client is supported (HTTP error 406). Parameters:\n* $1 - the list of supported MIME types", "pagedata-not-acceptable": "No matching format found. Supported MIME types: $1", "pagedata-bad-title": "Error shown when the requested title is invalid. Parameters:\n* $1: the malformed ID", - "unregistered-user-config": "Shown when viewing a user JS, CSS or JSON subpage with ?action=raw&ctype= where there is no such user. It is shown as a paragraph after a header saying 'Forbidden'." + "unregistered-user-config": "Shown when viewing a user JS, CSS or JSON subpage with ?action=raw&ctype= where there is no such user. It is shown as a paragraph after a header saying 'Forbidden'.", + "passwordpolicies": "The name of the special page [[Special:PasswordPolicies]].", + "passwordpolicies-summary": "The description used on [[Special:ListGroupRights]].\n\nRefers to {{msg-mw|Passwordpolicies-helppage}}.", + "passwordpolicies-helppage": "The link used on [[Special:PasswordPolicies]].", + "passwordpolicies-group": "The title of the column in the table, about user groups (like you are in the ''translator'' group).\n\n{{Identical|Group}}\n{{Related|Passwordpolicies}}", + "passwordpolicies-policies": "The title of the column in the table, about password policies.\n{{Related|Passwordpolicies}}", + "passwordpolicies-policy-display": "{{optional}}\nParameters:\n* $1 - the text from the \"passwordpolicies-policy-...\" messages, i.e. {{msg-mw|passwordpolicies-policy-minimalpasswordlength}}\n* $2 - the name of this password policy", + "passwordpolicies-policy-minimalpasswordlength": "Password policy that enforces a minimum number of characters a password must be. $1 - minimum number of characters that a password can be", + "passwordpolicies-policy-minimumpasswordlengthtologin": "Password policy that enforces a minimum number of characters a password must be to be able to login to the wiki. $1 - minimum number of characters that a password can be to be able to login", + "passwordpolicies-policy-passwordcannotmatchusername": "Password policy that enforces that the password of the account cannot be the same as the username", + "passwordpolicies-policy-passwordcannotmatchblacklist": "Password policy that enforces that passwords are not on a list of blacklisted passwords (often previously used during MediaWiki automated testing)", + "passwordpolicies-policy-maximalpasswordlength": "Password policy that enforces a maximum number of characters a password must be. $1 - maximum number of characters that a password can be", + "passwordpolicies-policy-passwordcannotbepopular": "Password policy that enforces that a password is not in a list of $1 number of \"popular\" passwords. $1 - number of popular passwords the password will be checked against" + } diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 16a12de148..7a7370f81e 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -468,6 +468,7 @@ $specialPageAliases = [ 'PagesWithProp' => [ 'PagesWithProp', 'Pageswithprop', 'PagesByProp', 'Pagesbyprop' ], 'PageData' => [ 'PageData' ], 'PageLanguage' => [ 'PageLanguage' ], + 'PasswordPolicies' => [ 'PasswordPolicies' ], 'PasswordReset' => [ 'PasswordReset' ], 'PermanentLink' => [ 'PermanentLink', 'PermaLink' ], 'Preferences' => [ 'Preferences' ], diff --git a/resources/src/mediawiki.special/special.css b/resources/src/mediawiki.special/special.css index 0676bfc32d..0404c455f9 100644 --- a/resources/src/mediawiki.special/special.css +++ b/resources/src/mediawiki.special/special.css @@ -134,3 +134,8 @@ color: #72777d; font-size: 90%; } + +/* Special:PasswordPolicies */ +.mw-passwordpolicies-table tr { + vertical-align: top; +} diff --git a/tests/phpunit/includes/password/PasswordPolicyChecksTest.php b/tests/phpunit/includes/password/PasswordPolicyChecksTest.php index 7dfb3cf5f1..5ddbe271eb 100644 --- a/tests/phpunit/includes/password/PasswordPolicyChecksTest.php +++ b/tests/phpunit/includes/password/PasswordPolicyChecksTest.php @@ -156,4 +156,21 @@ class PasswordPolicyChecksTest extends MediaWikiTestCase { $status = PasswordPolicyChecks::checkPopularPasswordBlacklist( PHP_INT_MAX, $user, $password ); $this->assertSame( $expected, $status->isGood() ); } + + /** + * Verify that all password policy description messages actually exist. + * Messages used on Special:PasswordPolicies + */ + public function testPasswordPolicyDescriptionsExist() { + global $wgPasswordPolicy; + $lang = Language::factory( 'en' ); + + foreach ( array_keys( $wgPasswordPolicy['checks'] ) as $check ) { + $msgKey = 'passwordpolicies-policy-' . strtolower( $check ); + $this->assertTrue( + wfMessage( $msgKey )->useDatabase( false )->inLanguage( $lang )->exists(), + "Message '$msgKey' required by '$check' must exist" + ); + } + } } -- 2.20.1