r40898 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r40897‎ | r40898 | r40899 >
Date:04:09, 16 September 2008
Author:tstarling
Status:old
Tags:
Comment:
In protect.js:
* Use fragments of English when naming things, instead of a jumble of words vaguely related to the topic
* Use verbs in function names that are related to what the functions do
* Move all functions into an object, to avoid prefixes that look like part of a nonsensical sentence
* Fixed a bug whereby the "move" controls would not be unlocked if the expiry times differed. Refactored the relevant code.
* When the user types something into the "other time" box, automatically select "other time" from the drop-down list.

In ProtectionForm:
* Use the new protect.js interface
* Use accessor instead of Title member variable mRestrictionsExpiry
* Added an "existing expiry" option to the expiry drop-down list, instead of putting the ISO 8601 time in the "other" box
* Formatting
* Fixed convoluted expiry time data flow, documented
* s/List/Selection, for variables containing a value that the user has selected from a list
Modified paths:
  • /trunk/phase3/includes/ProtectionForm.php (modified) (history)
  • /trunk/phase3/includes/Title.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/skins/common/protect.js (modified) (history)

Diff [purge]

Index: trunk/phase3/skins/common/protect.js
@@ -1,327 +1,352 @@
2 -/**
3 - * Set up the protection chaining interface (i.e. "unlock move permissions" checkbox)
4 - * on the protection form
5 - *
6 - * @param String tableId Identifier of the table containing UI bits
7 - * @param String labelText Text to use for the checkbox label
8 - */
9 -function protectInitialize( tableId, labelText, types ) {
10 - if( !( document.createTextNode && document.getElementById && document.getElementsByTagName ) )
11 - return false;
12 -
13 - var box = document.getElementById( tableId );
14 - if( !box )
15 - return false;
16 -
17 - var tbody = box.getElementsByTagName( 'tbody' )[0];
18 - var row = document.createElement( 'tr' );
19 - tbody.appendChild( row );
20 -
21 - row.appendChild( document.createElement( 'td' ) );
22 - var col = document.createElement( 'td' );
23 - row.appendChild( col );
24 - // If there is only one protection type, there is nothing to chain
25 - if( types > 1 ) {
26 - var check = document.createElement( 'input' );
27 - check.id = 'mwProtectUnchained';
28 - check.type = 'checkbox';
29 - col.appendChild( check );
30 - addClickHandler( check, protectChainUpdate );
312
32 - col.appendChild( document.createTextNode( ' ' ) );
33 - var label = document.createElement( 'label' );
34 - label.htmlFor = 'mwProtectUnchained';
35 - label.appendChild( document.createTextNode( labelText ) );
36 - col.appendChild( label );
 3+var ProtectionForm = {
 4+ 'existingMatch': false,
375
38 - check.checked = !protectAllMatch();
39 - protectEnable( check.checked );
40 - }
41 -
42 - setCascadeCheckbox();
43 -
44 - return true;
45 -}
 6+ /**
 7+ * Set up the protection chaining interface (i.e. "unlock move permissions" checkbox)
 8+ * on the protection form
 9+ *
 10+ * @param Object opts : parameters with members:
 11+ * tableId Identifier of the table containing UI bits
 12+ * labelText Text to use for the checkbox label
 13+ * numTypes The number of protection types
 14+ * existingMatch True if all the existing expiry times match
 15+ */
 16+ 'init': function( opts ) {
 17+ if( !( document.createTextNode && document.getElementById && document.getElementsByTagName ) )
 18+ return false;
4619
47 -/**
48 -* Determine if, given the cascadeable protection levels
49 -* and what is currently selected, if the cascade box
50 -* can be checked
51 -*
52 -* @return boolean
53 -*
54 -*/
55 -function setCascadeCheckbox() {
56 - // For non-existent titles, there is no cascade option
57 - if( !document.getElementById( 'mwProtect-cascade' ) ) {
58 - return false;
59 - }
60 - var lists = protectSelectors();
61 - for( var i = 0; i < lists.length; i++ ) {
62 - if( lists[i].selectedIndex > -1 ) {
63 - var items = lists[i].getElementsByTagName( 'option' );
64 - var selected = items[ lists[i].selectedIndex ].value;
65 - if( !isCascadeableLevel(selected) ) {
66 - document.getElementById( 'mwProtect-cascade' ).checked = false;
67 - document.getElementById( 'mwProtect-cascade' ).disabled = true;
68 - return false;
69 - }
70 - }
71 - }
72 - document.getElementById( 'mwProtect-cascade' ).disabled = false;
73 - return true;
74 -}
 20+ var box = document.getElementById( opts.tableId );
 21+ if( !box )
 22+ return false;
7523
76 -/**
77 -* Is this protection level cascadeable?
78 -* @param String level
79 -*
80 -* @return boolean
81 -*
82 -*/
83 -function isCascadeableLevel( level ) {
84 - for (var k = 0; k < wgCascadeableLevels.length; k++) {
85 - if ( wgCascadeableLevels[k] == level ) {
86 - return true;
87 - }
88 - }
89 - return false;
90 -}
 24+ var tbody = box.getElementsByTagName( 'tbody' )[0];
 25+ var row = document.createElement( 'tr' );
 26+ tbody.appendChild( row );
9127
92 -/**
93 - * When protection levels are locked together, update the rest
94 - * when one action's level changes
95 - *
96 - * @param Element source Level selector that changed
97 - */
98 -function protectLevelsUpdate(source) {
99 - if( !protectUnchained() )
100 - protectUpdateAll( source.selectedIndex );
101 - setCascadeCheckbox();
102 -}
 28+ this.existingMatch = opts.existingMatch;
10329
104 -/**
105 - * When protection levels are locked together, update the
106 - * expiries when one changes
107 - *
108 - * @param Element source expiry input that changed
109 - */
 30+ row.appendChild( document.createElement( 'td' ) );
 31+ var cell = document.createElement( 'td' );
 32+ row.appendChild( cell );
 33+ // If there is only one protection type, there is nothing to chain
 34+ if( opts.numTypes > 1 ) {
 35+ var check = document.createElement( 'input' );
 36+ check.id = 'mwProtectUnchained';
 37+ check.type = 'checkbox';
 38+ cell.appendChild( check );
 39+ addClickHandler( check, function() { ProtectionForm.onChainClick(); } );
11040
111 -function protectExpiryUpdate(source) {
112 - if( !protectUnchained() ) {
113 - var expiry = source.value;
114 - expiryForInputs(function(set) {
115 - set.value = expiry;
116 - });
117 - }
118 -}
 41+ cell.appendChild( document.createTextNode( ' ' ) );
 42+ var label = document.createElement( 'label' );
 43+ label.htmlFor = 'mwProtectUnchained';
 44+ label.appendChild( document.createTextNode( opts.labelText ) );
 45+ cell.appendChild( label );
11946
120 -/**
121 - * When protection levels are locked together, update the
122 - * expiry lists when one changes and clear the custom inputs
123 - *
124 - * @param Element source expiry selector that changed
125 - */
126 -function protectExpiryListUpdate(source) {
127 - if( !protectUnchained() ) {
128 - var expiry = source.value;
129 - expiryListForInputs(function(set) {
130 - set.value = expiry;
131 - });
132 - expiryForInputs(function(set) {
133 - set.value = '';
134 - });
135 - }
136 -}
 47+ check.checked = !this.areAllTypesMatching();
 48+ this.enableUnchainedInputs( check.checked );
 49+ }
13750
138 -/**
139 - * Update chain status and enable/disable various bits of the UI
140 - * when the user changes the "unlock move permissions" checkbox
141 - */
142 -function protectChainUpdate() {
143 - if( protectUnchained() ) {
144 - protectEnable( true );
145 - } else {
146 - protectChain();
147 - protectEnable( false );
148 - }
149 - setCascadeCheckbox();
150 -}
 51+ this.updateCascadeCheckbox();
15152
152 -/**
153 - * Are all actions protected at the same level?
154 - *
155 - * @return boolean
156 - */
157 -function protectAllMatch() {
158 - var values = new Array();
159 - protectForSelectors(function(set) {
160 - values[values.length] = set.selectedIndex;
161 - });
162 - for (var i = 1; i < values.length; i++) {
163 - if (values[i] != values[0]) {
164 - return false;
 53+ return true;
 54+ },
 55+
 56+ /**
 57+ * Sets the disabled attribute on the cascade checkbox depending on the current selected levels
 58+ */
 59+ 'updateCascadeCheckbox': function() {
 60+ // For non-existent titles, there is no cascade option
 61+ if( !document.getElementById( 'mwProtect-cascade' ) ) {
 62+ return;
16563 }
166 - }
167 - return true;
168 -}
 64+ var lists = this.getLevelSelectors();
 65+ for( var i = 0; i < lists.length; i++ ) {
 66+ if( lists[i].selectedIndex > -1 ) {
 67+ var items = lists[i].getElementsByTagName( 'option' );
 68+ var selected = items[ lists[i].selectedIndex ].value;
 69+ if( !this.isCascadeableLevel(selected) ) {
 70+ document.getElementById( 'mwProtect-cascade' ).checked = false;
 71+ document.getElementById( 'mwProtect-cascade' ).disabled = true;
 72+ return;
 73+ }
 74+ }
 75+ }
 76+ document.getElementById( 'mwProtect-cascade' ).disabled = false;
 77+ },
16978
170 -/**
171 - * Is protection chaining on or off?
172 - *
173 - * @return bool
174 - */
175 -function protectUnchained() {
176 - var unchain = document.getElementById( 'mwProtectUnchained' );
177 - return unchain
178 - ? unchain.checked
179 - : true; // No control, so we need to let the user set both levels
180 -}
 79+ /**
 80+ * Is this protection level cascadeable?
 81+ * @param String level
 82+ *
 83+ * @return boolean
 84+ *
 85+ */
 86+ 'isCascadeableLevel': function( level ) {
 87+ for (var k = 0; k < wgCascadeableLevels.length; k++) {
 88+ if ( wgCascadeableLevels[k] == level ) {
 89+ return true;
 90+ }
 91+ }
 92+ return false;
 93+ },
18194
182 -/**
183 - * Find the highest-protected action and set all others to that level
184 - */
185 -function protectChain() {
186 - var maxIndex = -1;
187 - protectForSelectors(function(set) {
188 - if (set.selectedIndex > maxIndex) {
189 - maxIndex = set.selectedIndex;
 95+ /**
 96+ * When protection levels are locked together, update the rest
 97+ * when one action's level changes
 98+ *
 99+ * @param Element source Level selector that changed
 100+ */
 101+ 'updateLevels': function(source) {
 102+ if( !this.isUnchained() )
 103+ this.setAllSelectors( source.selectedIndex );
 104+ this.updateCascadeCheckbox();
 105+ },
 106+
 107+ /**
 108+ * When protection levels are locked together, update the
 109+ * expiries when one changes
 110+ *
 111+ * @param Element source expiry input that changed
 112+ */
 113+
 114+ 'updateExpiry': function(source) {
 115+ if( !this.isUnchained() ) {
 116+ var expiry = source.value;
 117+ this.forEachExpiryInput(function(element) {
 118+ element.value = expiry;
 119+ });
190120 }
191 - });
192 - protectUpdateAll(maxIndex);
193 -}
 121+ var listId = source.id.replace( /^mwProtect-(\w+)-expires$/, 'mwProtectExpirySelection-$1' );
 122+ var list = document.getElementById( listId );
 123+ if (list && list.value != 'othertime' ) {
 124+ if ( this.isUnchained() ) {
 125+ list.value = 'othertime';
 126+ } else {
 127+ this.forEachExpirySelector(function(element) {
 128+ element.value = 'othertime';
 129+ });
 130+ }
 131+ }
 132+ },
194133
195 -/**
196 - * Protect all actions at the specified level
197 - *
198 - * @param int index Protection level
199 - */
200 -function protectUpdateAll(index) {
201 - protectForSelectors(function(set) {
202 - if (set.selectedIndex != index) {
203 - set.selectedIndex = index;
 134+ /**
 135+ * When protection levels are locked together, update the
 136+ * expiry lists when one changes and clear the custom inputs
 137+ *
 138+ * @param Element source expiry selector that changed
 139+ */
 140+ 'updateExpiryList': function(source) {
 141+ if( !this.isUnchained() ) {
 142+ var expiry = source.value;
 143+ this.forEachExpirySelector(function(element) {
 144+ element.value = expiry;
 145+ });
 146+ this.forEachExpiryInput(function(element) {
 147+ element.value = '';
 148+ });
204149 }
205 - });
206 -}
 150+ },
207151
208 -/**
209 - * Apply a callback to each protection selector
210 - *
211 - * @param callable func Callback function
212 - */
213 -function protectForSelectors(func) {
214 - var selectors = protectSelectors();
215 - for (var i = 0; i < selectors.length; i++) {
216 - func(selectors[i]);
217 - }
218 -}
 152+ /**
 153+ * Update chain status and enable/disable various bits of the UI
 154+ * when the user changes the "unlock move permissions" checkbox
 155+ */
 156+ 'onChainClick': function() {
 157+ if( this.isUnchained() ) {
 158+ this.enableUnchainedInputs( true );
 159+ } else {
 160+ this.setAllSelectors( this.getMaxLevel() );
 161+ this.enableUnchainedInputs( false );
 162+ }
 163+ this.updateCascadeCheckbox();
 164+ },
219165
220 -/**
221 - * Get a list of all protection selectors on the page
222 - *
223 - * @return Array
224 - */
225 -function protectSelectors() {
226 - var all = document.getElementsByTagName("select");
227 - var ours = new Array();
228 - for (var i = 0; i < all.length; i++) {
229 - var set = all[i];
230 - if (set.id.match(/^mwProtect-level-/)) {
231 - ours[ours.length] = set;
 166+ /**
 167+ * Returns true if the named attribute in all objects in the given array are matching
 168+ */
 169+ 'matchAttribute' : function( objects, attrName ) {
 170+ var value = null;
 171+
 172+ // Check levels
 173+ for ( var i = 0; i < objects.length; i++ ) {
 174+ var element = objects[i];
 175+ if ( value == null ) {
 176+ value = element[attrName];
 177+ } else {
 178+ if ( value != element[attrName] ) {
 179+ return false;
 180+ }
 181+ }
232182 }
233 - }
234 - return ours;
235 -}
 183+ return true;
 184+ },
236185
237 -/**
238 - * Apply a callback to each expiry input
239 - *
240 - * @param callable func Callback function
241 - */
242 -function expiryForInputs(func) {
243 - var inputs = expiryInputs();
244 - for (var i = 0; i < inputs.length; i++) {
245 - func(inputs[i]);
246 - }
247 -}
 186+ /**
 187+ * Are all actions protected at the same level, with the same expiry time?
 188+ *
 189+ * @return boolean
 190+ */
 191+ 'areAllTypesMatching': function() {
 192+ return this.existingMatch
 193+ && this.matchAttribute( this.getLevelSelectors(), 'selectedIndex' )
 194+ && this.matchAttribute( this.getExpirySelectors(), 'selectedIndex' )
 195+ && this.matchAttribute( this.getExpiryInputs(), 'value' );
 196+ },
248197
249 -/**
250 - * Get a list of all expiry inputs on the page
251 - *
252 - * @return Array
253 - */
254 -function expiryInputs() {
255 - var all = document.getElementsByTagName("input");
256 - var ours = new Array();
257 - for (var i = 0; i < all.length; i++) {
258 - var set = all[i];
259 - if (set.name.match(/^mwProtect-expiry-/)) {
260 - ours[ours.length] = set;
 198+ /**
 199+ * Is protection chaining off?
 200+ *
 201+ * @return bool
 202+ */
 203+ 'isUnchained': function() {
 204+ var element = document.getElementById( 'mwProtectUnchained' );
 205+ return element
 206+ ? element.checked
 207+ : true; // No control, so we need to let the user set both levels
 208+ },
 209+
 210+ /**
 211+ * Find the highest protection level in any selector
 212+ */
 213+ 'getMaxLevel': function() {
 214+ var maxIndex = -1;
 215+ this.forEachLevelSelector(function(element) {
 216+ if (element.selectedIndex > maxIndex) {
 217+ maxIndex = element.selectedIndex;
 218+ }
 219+ });
 220+ return maxIndex;
 221+ },
 222+
 223+ /**
 224+ * Protect all actions at the specified level
 225+ *
 226+ * @param int index Protection level
 227+ */
 228+ 'setAllSelectors': function(index) {
 229+ this.forEachLevelSelector(function(element) {
 230+ if (element.selectedIndex != index) {
 231+ element.selectedIndex = index;
 232+ }
 233+ });
 234+ },
 235+
 236+ /**
 237+ * Apply a callback to each protection selector
 238+ *
 239+ * @param callable func Callback function
 240+ */
 241+ 'forEachLevelSelector': function(func) {
 242+ var selectors = this.getLevelSelectors();
 243+ for (var i = 0; i < selectors.length; i++) {
 244+ func(selectors[i]);
261245 }
262 - }
263 - return ours;
264 -}
 246+ },
265247
266 -/**
267 - * Apply a callback to each expiry selector list
268 - * @param callable func Callback function
269 - */
270 -function expiryListForInputs(func) {
271 - var inputs = expiryListInputs();
272 - for (var i = 0; i < inputs.length; i++) {
273 - func(inputs[i]);
274 - }
275 -}
 248+ /**
 249+ * Get a list of all protection selectors on the page
 250+ *
 251+ * @return Array
 252+ */
 253+ 'getLevelSelectors': function() {
 254+ var all = document.getElementsByTagName("select");
 255+ var ours = new Array();
 256+ for (var i = 0; i < all.length; i++) {
 257+ var element = all[i];
 258+ if (element.id.match(/^mwProtect-level-/)) {
 259+ ours[ours.length] = element;
 260+ }
 261+ }
 262+ return ours;
 263+ },
276264
277 -/**
278 - * Get a list of all expiry selector lists on the page
279 - *
280 - * @return Array
281 - */
282 -function expiryListInputs() {
283 - var all = document.getElementsByTagName("select");
284 - var ours = new Array();
285 - for (var i = 0; i < all.length; i++) {
286 - var set = all[i];
287 - if (set.id.match(/^mwProtectExpiryList-/)) {
288 - ours[ours.length] = set;
 265+ /**
 266+ * Apply a callback to each expiry input
 267+ *
 268+ * @param callable func Callback function
 269+ */
 270+ 'forEachExpiryInput': function(func) {
 271+ var inputs = this.getExpiryInputs();
 272+ for (var i = 0; i < inputs.length; i++) {
 273+ func(inputs[i]);
289274 }
290 - }
291 - return ours;
292 -}
 275+ },
293276
294 -/**
295 - * Enable/disable protection selectors and expiry inputs
296 - *
297 - * @param boolean val Enable?
298 - */
299 -function protectEnable(val) {
300 - // fixme
301 - var first = true;
302 - protectForSelectors(function(set) {
303 - if (first) {
304 - first = false;
305 - } else {
306 - set.disabled = !val;
307 - set.style.visible = val ? "visible" : "hidden";
 277+ /**
 278+ * Get a list of all expiry inputs on the page
 279+ *
 280+ * @return Array
 281+ */
 282+ 'getExpiryInputs': function() {
 283+ var all = document.getElementsByTagName("input");
 284+ var ours = new Array();
 285+ for (var i = 0; i < all.length; i++) {
 286+ var element = all[i];
 287+ if (element.name.match(/^mwProtect-expiry-/)) {
 288+ ours[ours.length] = element;
 289+ }
308290 }
309 - });
310 - first = true;
311 - expiryForInputs(function(set) {
312 - if (first) {
313 - first = false;
314 - } else {
315 - set.disabled = !val;
316 - set.style.visible = val ? "visible" : "hidden";
 291+ return ours;
 292+ },
 293+
 294+ /**
 295+ * Apply a callback to each expiry selector list
 296+ * @param callable func Callback function
 297+ */
 298+ 'forEachExpirySelector': function(func) {
 299+ var inputs = this.getExpirySelectors();
 300+ for (var i = 0; i < inputs.length; i++) {
 301+ func(inputs[i]);
317302 }
318 - });
319 - first = true;
320 - expiryListForInputs(function(set) {
321 - if (first) {
322 - first = false;
323 - } else {
324 - set.disabled = !val;
325 - set.style.visible = val ? "visible" : "hidden";
 303+ },
 304+
 305+ /**
 306+ * Get a list of all expiry selector lists on the page
 307+ *
 308+ * @return Array
 309+ */
 310+ 'getExpirySelectors': function() {
 311+ var all = document.getElementsByTagName("select");
 312+ var ours = new Array();
 313+ for (var i = 0; i < all.length; i++) {
 314+ var element = all[i];
 315+ if (element.id.match(/^mwProtectExpirySelection-/)) {
 316+ ours[ours.length] = element;
 317+ }
326318 }
327 - });
 319+ return ours;
 320+ },
 321+
 322+ /**
 323+ * Enable/disable protection selectors and expiry inputs
 324+ *
 325+ * @param boolean val Enable?
 326+ */
 327+ 'enableUnchainedInputs': function(val) {
 328+ var first = true;
 329+ this.forEachLevelSelector(function(element) {
 330+ if (first) {
 331+ first = false;
 332+ } else {
 333+ element.disabled = !val;
 334+ }
 335+ });
 336+ first = true;
 337+ this.forEachExpiryInput(function(element) {
 338+ if (first) {
 339+ first = false;
 340+ } else {
 341+ element.disabled = !val;
 342+ }
 343+ });
 344+ first = true;
 345+ this.forEachExpirySelector(function(element) {
 346+ if (first) {
 347+ first = false;
 348+ } else {
 349+ element.disabled = !val;
 350+ }
 351+ });
 352+ }
328353 }
Index: trunk/phase3/includes/ProtectionForm.php
@@ -23,15 +23,36 @@
2424 * @todo document, briefly.
2525 */
2626 class ProtectionForm {
 27+ /** A map of action to restriction level, from request or default */
2728 var $mRestrictions = array();
 29+
 30+ /** The custom/additional protection reason */
2831 var $mReason = '';
29 - var $mReasonList = '';
 32+
 33+ /** The reason selected from the list, blank for other/additional */
 34+ var $mReasonSelection = '';
 35+
 36+ /** True if the restrictions are cascading, from request or existing protection */
3037 var $mCascade = false;
31 - var $mExpiry =array();
32 - var $mExpiryList = array();
 38+
 39+ /** Map of action to "other" expiry time. Used in preference to mExpirySelection. */
 40+ var $mExpiry = array();
 41+
 42+ /**
 43+ * Map of action to value selected in expiry drop-down list.
 44+ * Will be set to 'othertime' whenever mExpiry is set.
 45+ */
 46+ var $mExpirySelection = array();
 47+
 48+ /** Permissions errors for the protect action */
3349 var $mPermErrors = array();
 50+
 51+ /** Types (i.e. actions) for which levels can be selected */
3452 var $mApplicableTypes = array();
3553
 54+ /** Map of action to the expiry time of the existing protection */
 55+ var $mExistingExpiry = array();
 56+
3657 function __construct( Article $article ) {
3758 global $wgRequest, $wgUser;
3859 global $wgRestrictionTypes, $wgRestrictionLevels;
@@ -39,44 +60,57 @@
4061 $this->mTitle = $article->mTitle;
4162 $this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create');
4263
43 - if( $this->mTitle ) {
44 - $this->mTitle->loadRestrictions();
 64+ $this->mCascade = $this->mTitle->areRestrictionsCascading();
4565
46 - foreach( $this->mApplicableTypes as $action ) {
47 - // Fixme: this form currently requires individual selections,
48 - // but the db allows multiples separated by commas.
49 - $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
50 -
51 - if ( $this->mTitle->mRestrictionsExpiry[$action] == 'infinity' ) {
52 - $this->mExpiry[$action] = 'infinite';
53 - } else if ( strlen($this->mTitle->mRestrictionsExpiry[$action]) == 0 ) {
54 - $this->mExpiry[$action] = '';
55 - } else {
56 - // FIXME: this format is not user friendly
57 - $this->mExpiry[$action] = wfTimestamp( TS_ISO_8601, $this->mTitle->mRestrictionsExpiry[$action] );
58 - }
59 - }
60 - $this->mCascade = $this->mTitle->areRestrictionsCascading();
61 - }
62 -
6366 // The form will be available in read-only to show levels.
64 - $this->disabled = wfReadOnly() || ($this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser)) != array();
 67+ $this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser);
 68+ $this->disabled = wfReadOnly() || $this->mPermErrors != array();
6569 $this->disabledAttrib = $this->disabled
6670 ? array( 'disabled' => 'disabled' )
6771 : array();
6872
6973 $this->mReason = $wgRequest->getText( 'mwProtect-reason' );
70 - $this->mReasonList = $wgRequest->getText( 'wpProtectReasonList' );
 74+ $this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' );
7175 $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade', $this->mCascade );
72 -
 76+
7377 foreach( $this->mApplicableTypes as $action ) {
74 - // Let dropdown have 'infinite' for unprotected pages
75 - if( !($expiry[$action] = $wgRequest->getText( "mwProtect-expiry-$action" )) && $this->mExpiry[$action] != 'infinite' ) {
76 - $expiry[$action] = $this->mExpiry[$action];
 78+ // Fixme: this form currently requires individual selections,
 79+ // but the db allows multiples separated by commas.
 80+ $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
 81+
 82+ if ( !$this->mRestrictions[$action] ) {
 83+ // No existing expiry
 84+ $existingExpiry = '';
 85+ } else {
 86+ $existingExpiry = $this->mTitle->getRestrictionExpiry( $action );
7787 }
78 - $this->mExpiry[$action] = $expiry[$action];
79 - $this->mExpiryList[$action] = $wgRequest->getText( "wpProtectExpiryList-$action", $this->mExpiry[$action] ? '' : 'infinite' );
 88+ $this->mExistingExpiry[$action] = $existingExpiry;
8089
 90+ $requestExpiry = $wgRequest->getText( "mwProtect-expiry-$action" );
 91+ $requestExpirySelection = $wgRequest->getVal( "wpProtectExpirySelection-$action" );
 92+
 93+ if ( $requestExpiry ) {
 94+ // Custom expiry takes precedence
 95+ $this->mExpiry[$action] = $requestExpiry;
 96+ $this->mExpirySelection[$action] = 'othertime';
 97+ } elseif ( $requestExpirySelection ) {
 98+ // Expiry selected from list
 99+ $this->mExpiry[$action] = '';
 100+ $this->mExpirySelection[$action] = $requestExpirySelection;
 101+ } elseif ( $existingExpiry == 'infinite' ) {
 102+ // Existing expiry is infinite, use "infinite" in drop-down
 103+ $this->mExpiry[$action] = '';
 104+ $this->mExpirySelection[$action] = 'infinite';
 105+ } elseif ( $existingExpiry ) {
 106+ // Use existing expiry in its own list item
 107+ $this->mExpiry[$action] = '';
 108+ $this->mExpirySelection[$action] = $existingExpiry;
 109+ } else {
 110+ // Final default: infinite
 111+ $this->mExpiry[$action] = '';
 112+ $this->mExpirySelection[$action] = 'infinite';
 113+ }
 114+
81115 $val = $wgRequest->getVal( "mwProtect-level-$action" );
82116 if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
83117 // Prevent users from setting levels that they cannot later unset
@@ -93,6 +127,34 @@
94128 }
95129 }
96130
 131+ /**
 132+ * Get the expiry time for a given action, by combining the relevant inputs.
 133+ * Returns a 14-char timestamp or "infinity", or false if the input was invalid
 134+ */
 135+ function getExpiry( $action ) {
 136+ if ( $this->mExpirySelection[$action] == 'existing' ) {
 137+ return $this->mExistingExpiry[$action];
 138+ } elseif ( $this->mExpirySelection[$action] == 'othertime' ) {
 139+ $value = $this->mExpiry[$action];
 140+ } else {
 141+ $value = $this->mExpirySelection[$action];
 142+ }
 143+ if ( $value == 'infinite' || $value == 'indefinite' || $value == 'infinity' ) {
 144+ $time = Block::infinity();
 145+ } else {
 146+ $unix = strtotime( $value );
 147+
 148+ if ( !$unix || $unix === -1 ) {
 149+ return false;
 150+ }
 151+
 152+ // Fixme: non-qualified absolute times are not in users specified timezone
 153+ // and there isn't notice about it in the ui
 154+ $time = wfTimestamp( TS_MW, $unix );
 155+ }
 156+ return $time;
 157+ }
 158+
97159 function execute() {
98160 global $wgRequest, $wgOut;
99161 if( $wgRequest->wasPosted() ) {
@@ -170,7 +232,7 @@
171233 }
172234
173235 # Create reason string. Use list and/or custom string.
174 - $reasonstr = $this->mReasonList;
 236+ $reasonstr = $this->mReasonSelection;
175237 if ( $reasonstr != 'other' && $this->mReason != '' ) {
176238 // Entry from drop down menu + additional comment
177239 $reasonstr .= ': ' . $this->mReason;
@@ -179,33 +241,17 @@
180242 }
181243 $expiry = array();
182244 foreach( $this->mApplicableTypes as $action ) {
183 - # Custom expiry takes precedence
184 - if ( strlen( $wgRequest->getText( "mwProtect-expiry-$action" ) ) == 0 ) {
185 - $this->mExpiry[$action] = strlen($wgRequest->getText( "wpProtectExpiryList-$action")) ? $wgRequest->getText( "wpProtectExpiryList-$action") : 'infinite';
186 - } else {
187 - $this->mExpiry[$action] = $wgRequest->getText( "mwProtect-expiry-$action" );
 245+ $expiry[$action] = $this->getExpiry( $action );
 246+ if ( !$expiry[$action] ) {
 247+ $this->show( wfMsg( 'protect_expiry_invalid' ) );
 248+ return false;
188249 }
189 - if ( $this->mExpiry[$action] == 'infinite' || $this->mExpiry[$action] == 'indefinite' ) {
190 - $expiry[$action] = Block::infinity();
191 - } else {
192 - # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1
193 - $expiry[$action] = strtotime( $this->mExpiry[$action] );
194 -
195 - if ( $expiry[$action] < 0 || $expiry[$action] === false ) {
196 - $this->show( wfMsg( 'protect_expiry_invalid' ) );
197 - return false;
198 - }
199 -
200 - // Fixme: non-qualified absolute times are not in users specified timezone
201 - // and there isn't notice about it in the ui
202 - $expiry[$action] = wfTimestamp( TS_MW, $expiry[$action] );
203 -
204 - if ( $expiry[$action] < wfTimestampNow() ) {
205 - $this->show( wfMsg( 'protect_expiry_old' ) );
206 - return false;
207 - }
 250+ if ( $expiry[$action] < wfTimestampNow() ) {
 251+ $this->show( wfMsg( 'protect_expiry_old' ) );
 252+ return false;
208253 }
209254 }
 255+
210256 # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied
211257 # to a semi-protected page.
212258 global $wgGroupPermissions;
@@ -231,7 +277,6 @@
232278 } elseif( $this->mTitle->userIsWatching() ) {
233279 $this->mArticle->doUnwatch();
234280 }
235 -
236281 return $ok;
237282 }
238283
@@ -241,19 +286,17 @@
242287 * @return $out string HTML form
243288 */
244289 function buildForm() {
245 - global $wgUser;
 290+ global $wgUser, $wgLang;
246291
247 - $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonList' );
 292+ $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonSelection' );
248293 $mProtectreason = Xml::label( wfMsg( 'protect-otherreason' ), 'mwProtect-reason' );
249294
250295 $out = '';
251296 if( !$this->disabled ) {
252297 $out .= $this->buildScript();
253 - // The submission needs to reenable the move permission selector
254 - // if it's in locked mode, or some browsers won't submit the data.
255298 $out .= Xml::openElement( 'form', array( 'method' => 'post',
256299 'action' => $this->mTitle->getLocalUrl( 'action=protect' ),
257 - 'id' => 'mw-Protect-Form', 'onsubmit' => 'protectEnable(true)' ) );
 300+ 'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) );
258301 $out .= Xml::hidden( 'wpEditToken',$wgUser->editToken() );
259302 }
260303
@@ -273,22 +316,38 @@
274317 "<tr><th>$label</th><th></th></tr>" .
275318 "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td><td>";
276319
277 - $reasonDropDown = Xml::listDropDown( 'wpProtectReasonList',
 320+ $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection',
278321 wfMsgForContent( 'protect-dropdown' ),
279 - wfMsgForContent( 'protect-otherreason-op' ), '', 'mwProtect-reason', 4 );
 322+ wfMsgForContent( 'protect-otherreason-op' ),
 323+ $this->mReasonSelection,
 324+ 'mwProtect-reason', 4 );
280325 $scExpiryOptions = wfMsgForContent( 'ipboptions' ); // FIXME: use its own message
281326
282327 $showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled);
283328
284 - $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpiryList-$action" );
 329+ $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpirySelection-$action" );
285330 $mProtectother = Xml::label( wfMsg( 'protect-othertime' ), "mwProtect-$action-expires" );
286 - $expiryFormOptions = Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" );
 331+
 332+ $expiryFormOptions = '';
 333+ if ( $this->mExistingExpiry[$action] && $this->mExistingExpiry[$action] != 'infinity' ) {
 334+ $expiryFormOptions .=
 335+ Xml::option(
 336+ wfMsg( 'protect-existing-expiry', $wgLang->timeanddate( $this->mExistingExpiry[$action] ) ),
 337+ 'existing',
 338+ $this->mExpirySelection[$action] == 'existing'
 339+ ) . "\n";
 340+ }
 341+
 342+ $expiryFormOptions .= Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" ) . "\n";
287343 foreach( explode(',', $scExpiryOptions) as $option ) {
288 - if ( strpos($option, ":") === false ) $option = "$option:$option";
289 - list($show, $value) = explode(":", $option);
 344+ if ( strpos($option, ":") === false ) {
 345+ $show = $value = $option;
 346+ } else {
 347+ list($show, $value) = explode(":", $option);
 348+ }
290349 $show = htmlspecialchars($show);
291350 $value = htmlspecialchars($value);
292 - $expiryFormOptions .= Xml::option( $show, $value, $this->mExpiryList[$action] === $value ? true : false ) . "\n";
 351+ $expiryFormOptions .= Xml::option( $show, $value, $this->mExpirySelection[$action] === $value ) . "\n";
293352 }
294353 # Add expiry dropdown
295354 if( $showProtectOptions && !$this->disabled ) {
@@ -300,16 +359,16 @@
301360 <td class='mw-input'>" .
302361 Xml::tags( 'select',
303362 array(
304 - 'id' => "mwProtectExpiryList-$action",
305 - 'name' => "wpProtectExpiryList-$action",
306 - 'onchange' => "protectExpiryListUpdate(this)",
 363+ 'id' => "mwProtectExpirySelection-$action",
 364+ 'name' => "wpProtectExpirySelection-$action",
 365+ 'onchange' => "ProtectionForm.updateExpiryList(this)",
307366 'tabindex' => '2' ) + $this->disabledAttrib,
308367 $expiryFormOptions ) .
309368 "</td>
310369 </tr></table>";
311370 }
312371 # Add custom expiry field
313 - $attribs = array( 'id' => "mwProtect-$action-expires", 'onkeyup' => 'protectExpiryUpdate(this)' ) + $this->disabledAttrib;
 372+ $attribs = array( 'id' => "mwProtect-$action-expires", 'onkeyup' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib;
314373 $out .= "<table><tr>
315374 <td class='mw-label'>" .
316375 $mProtectother .
@@ -407,7 +466,7 @@
408467 'id' => $id,
409468 'name' => $id,
410469 'size' => count( $levels ),
411 - 'onchange' => 'protectLevelsUpdate(this)',
 470+ 'onchange' => 'ProtectionForm.updateLevels(this)',
412471 ) + $this->disabledAttrib;
413472
414473 $out = Xml::openElement( 'select', $attribs );
@@ -440,7 +499,7 @@
441500 global $wgStylePath, $wgStyleVersion;
442501 return Xml::tags( 'script', array(
443502 'type' => 'text/javascript',
444 - 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion" ), '' );
 503+ 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion.1" ), '' );
445504 }
446505
447506 function buildCleanupScript() {
@@ -453,8 +512,15 @@
454513 }
455514 }
456515 $script .= "[" . implode(',',$CascadeableLevels) . "];\n";
457 - $script .= 'protectInitialize("mw-protect-table2","' . Xml::escapeJsString( wfMsg( 'protect-unchain' ) ) .
458 - '","' . count($this->mApplicableTypes) . '")';
 516+ $options = (object)array(
 517+ 'tableId' => 'mw-protect-table2',
 518+ 'labelText' => wfMsg( 'protect-unchain' ),
 519+ 'numTypes' => count($this->mApplicableTypes),
 520+ 'existingMatch' => 1 == count( array_unique( $this->mExistingExpiry ) ),
 521+ );
 522+ $encOptions = Xml::encodeJsVar( $options );
 523+
 524+ $script .= "ProtectionForm.init($encOptions)";
459525 return Xml::tags( 'script', array( 'type' => 'text/javascript' ), $script );
460526 }
461527
Index: trunk/phase3/includes/Title.php
@@ -1895,6 +1895,18 @@
18961896 }
18971897
18981898 /**
 1899+ * Get the expiry time for the restriction against a given action
 1900+ * @return 14-char timestamp, or 'infinity' if the page is protected forever
 1901+ * or not protected at all, or false if the action is not recognised.
 1902+ */
 1903+ public function getRestrictionExpiry( $action ) {
 1904+ if( !$this->mRestrictionsLoaded ) {
 1905+ $this->loadRestrictions();
 1906+ }
 1907+ return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
 1908+ }
 1909+
 1910+ /**
18991911 * Is there a version of this page in the deletion archive?
19001912 * @return \type{\int} the number of archived revisions
19011913 */
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -2329,6 +2329,8 @@
23302330 'protect-cantedit' => 'You cannot change the protection levels of this page, because you do not have permission to edit it.',
23312331 'protect-othertime' => 'Other time:',
23322332 'protect-othertime-op' => 'other time',
 2333+'protect-existing-expiry' => 'Existing expiry time: $1',
 2334+
23332335 'protect-otherreason' => 'Other/additional reason:',
23342336 'protect-otherreason-op' => 'other/additional reason',
23352337 'protect-dropdown' => '*Common protection reasons