Index: trunk/extensions/UsernameBlacklist/UsernameBlacklist.i18n.php |
— | — | @@ -22,6 +22,7 @@ |
23 | 23 | # * Foo |
24 | 24 | # * [Bb]ar |
25 | 25 | </pre>', |
| 26 | +'usernameblacklist-invalid-lines' => 'The following lines in the username blacklist are invalid; please correct them before saving:', |
26 | 27 | |
27 | 28 | ), |
28 | 29 | |
Index: trunk/extensions/UsernameBlacklist/UsernameBlacklist.php |
— | — | @@ -29,7 +29,9 @@ |
30 | 30 | global $wgHooks, $wgVersion, $wgMessageCache; |
31 | 31 | require_once( dirname( __FILE__ ) . '/UsernameBlacklist.i18n.php' ); |
32 | 32 | $wgHooks['AbortNewAccount'][] = 'efUsernameBlacklist'; |
33 | | - $wgHooks['ArticleSave'][] = 'efUsernameBlacklistInvalidate'; |
| 33 | + $wgHooks['ArticleSaveComplete'][] = 'efUsernameBlacklistInvalidate'; |
| 34 | + $wgHooks['EditFilter'][] = 'efUsernameBlacklistValidate'; |
| 35 | + |
34 | 36 | if( version_compare( $wgVersion, '1.9alpha', '>=' ) ) { |
35 | 37 | foreach( efUsernameBlacklistMessages() as $lang => $messages ) |
36 | 38 | $wgMessageCache->addMessages( $messages, $lang ); |
— | — | @@ -72,6 +74,36 @@ |
73 | 75 | return true; |
74 | 76 | } |
75 | 77 | |
| 78 | + /** |
| 79 | + * If editing the username blacklist page, check for validity and whine at the user. |
| 80 | + */ |
| 81 | + function efUsernameBlacklistValidate( $editPage, $text, $section, &$hookError ) { |
| 82 | + if( $editPage->mTitle->getNamespace() == NS_MEDIAWIKI && |
| 83 | + $editPage->mTitle->getDbKey() == 'Usernameblacklist' ) { |
| 84 | + |
| 85 | + $blacklist = UsernameBlacklist::fetch(); |
| 86 | + $badLines = $blacklist->validate( $text ); |
| 87 | + |
| 88 | + if( $badLines ) { |
| 89 | + $badList = "*<tt>" . |
| 90 | + implode( "</tt>\n*<tt>", |
| 91 | + array_map( 'wfEscapeWikiText', $badLines ) ) . |
| 92 | + "</tt>\n"; |
| 93 | + $hookError = |
| 94 | + "<div class='errorbox'>" . |
| 95 | + wfMsgExt( 'usernameblacklist-invalid-lines', array( 'parsemag' ), count( $badLines ) ) . |
| 96 | + "\n" . |
| 97 | + $badList . |
| 98 | + "</div>\n" . |
| 99 | + "<br clear='all' />\n"; |
| 100 | + |
| 101 | + // This is kind of odd, but... :D |
| 102 | + return true; |
| 103 | + } |
| 104 | + } |
| 105 | + return true; |
| 106 | + } |
| 107 | + |
76 | 108 | class UsernameBlacklist { |
77 | 109 | |
78 | 110 | var $regex; |
— | — | @@ -98,12 +130,12 @@ |
99 | 131 | /** |
100 | 132 | * Attempt to fetch the blacklist from cache; build it if needs be |
101 | 133 | * |
102 | | - * @return string |
| 134 | + * @return array |
103 | 135 | */ |
104 | 136 | function fetchBlacklist() { |
105 | 137 | global $wgMemc, $wgDBname; |
106 | 138 | $list = $wgMemc->get( $this->key ); |
107 | | - if( $list ) { |
| 139 | + if( is_array( $list ) ) { |
108 | 140 | return $list; |
109 | 141 | } else { |
110 | 142 | $list = $this->buildBlacklist(); |
— | — | @@ -115,25 +147,87 @@ |
116 | 148 | /** |
117 | 149 | * Build the blacklist from scratch, using the message page |
118 | 150 | * |
119 | | - * @return string |
| 151 | + * @return array of regexes, potentially empty |
120 | 152 | */ |
121 | 153 | function buildBlacklist() { |
122 | 154 | $blacklist = wfMsgForContent( 'usernameblacklist' ); |
123 | | - $groups = array(); |
124 | 155 | if( $blacklist != '<usernameblacklist>' ) { |
125 | | - $lines = explode( "\n", $blacklist ); |
126 | | - foreach( $lines as $line ) { |
127 | | - $line = trim( $line ); |
128 | | - if( $this->isUsable( $line ) ) |
129 | | - $groups[] = $this->transform( $line ); |
| 156 | + return $this->safeBlacklist( $blacklist ); |
| 157 | + } else { |
| 158 | + return array(); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * Build one or more blacklist regular expressions from the input. |
| 164 | + * If a fragment causes an error, we'll return multiple items |
| 165 | + * so they can be run separately. |
| 166 | + * |
| 167 | + * @param string $input |
| 168 | + * @return array of regexes, potentially empty |
| 169 | + */ |
| 170 | + function safeBlacklist( $input ) { |
| 171 | + $groups = $this->fragmentsFromInput( $input ); |
| 172 | + if( count( $groups ) ) { |
| 173 | + $combinedRegex = '/(' . implode( '|', $groups ) . ')/u'; |
| 174 | + |
| 175 | + wfSuppressWarnings(); |
| 176 | + $ok = ( preg_match( $combinedRegex, '' ) !== false ); |
| 177 | + wfRestoreWarnings(); |
| 178 | + |
| 179 | + if( $ok ) { |
| 180 | + return array( $combinedRegex ); |
| 181 | + } else { |
| 182 | + $regexes = array(); |
| 183 | + foreach( $groups as $fragment ) { |
| 184 | + $regexes[] = '/' . $fragment . '/u'; |
| 185 | + } |
| 186 | + return $regexes; |
130 | 187 | } |
131 | | - return count( $groups ) ? '/(' . implode( '|', $groups ) . ')/u' : false; |
132 | 188 | } else { |
133 | | - return false; |
| 189 | + return array(); |
134 | 190 | } |
135 | 191 | } |
136 | 192 | |
137 | 193 | /** |
| 194 | + * Break input down by line, remove comments, and strip to regex fragments. |
| 195 | + * @input string |
| 196 | + * @return array |
| 197 | + */ |
| 198 | + function fragmentsFromInput( $input ) { |
| 199 | + $lines = explode( "\n", $input ); |
| 200 | + $groups = array(); |
| 201 | + foreach( $lines as $line ) { |
| 202 | + $line = trim( $line ); |
| 203 | + if( $this->isUsable( $line ) ) |
| 204 | + $groups[] = $this->transform( $line ); |
| 205 | + } |
| 206 | + return $groups; |
| 207 | + } |
| 208 | + |
| 209 | + /** |
| 210 | + * Go through a set of input and return a list of lines which |
| 211 | + * produce invalid regexes. |
| 212 | + * Empty set means good. :) |
| 213 | + * |
| 214 | + * @param string $input |
| 215 | + * @return array |
| 216 | + */ |
| 217 | + function validate( $input ) { |
| 218 | + $bad = array(); |
| 219 | + $fragments = $this->fragmentsFromInput( $input ); |
| 220 | + foreach( $fragments as $fragment ) { |
| 221 | + wfSuppressWarnings(); |
| 222 | + $ok = ( preg_match( "/$fragment/u", '' ) !== false ); |
| 223 | + wfRestoreWarnings(); |
| 224 | + if( !$ok ) { |
| 225 | + $bad[] = $fragment; |
| 226 | + } |
| 227 | + } |
| 228 | + return $bad; |
| 229 | + } |
| 230 | + |
| 231 | + /** |
138 | 232 | * Invalidate the blacklist cache |
139 | 233 | */ |
140 | 234 | function invalidateCache() { |
— | — | @@ -147,7 +241,18 @@ |
148 | 242 | * @return bool |
149 | 243 | */ |
150 | 244 | function match( $username ) { |
151 | | - return $this->regex ? preg_match( $this->regex, $username ) : false; |
| 245 | + foreach( $this->regexes as $regex ) { |
| 246 | + wfSuppressWarnings(); |
| 247 | + $match = preg_match( $regex, $username ); |
| 248 | + wfRestoreWarnings(); |
| 249 | + |
| 250 | + if( $match ) { |
| 251 | + return true; |
| 252 | + } elseif( $match === false ) { |
| 253 | + wfDebugLog( 'UsernameBlacklist', "Invalid username regex $regex" ); |
| 254 | + } |
| 255 | + } |
| 256 | + return false; |
152 | 257 | } |
153 | 258 | |
154 | 259 | /** |
— | — | @@ -157,7 +262,7 @@ |
158 | 263 | function UsernameBlacklist() { |
159 | 264 | global $wgDBname; |
160 | 265 | $this->key = "{$wgDBname}:username-blacklist"; |
161 | | - $this->regex = $this->fetchBlacklist(); |
| 266 | + $this->regexes = $this->fetchBlacklist(); |
162 | 267 | } |
163 | 268 | |
164 | 269 | /** |