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 ); |
31 | 2 | |
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, |
37 | 5 | |
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; |
46 | 19 | |
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; |
75 | 23 | |
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 ); |
91 | 27 | |
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; |
103 | 29 | |
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(); } ); |
110 | 40 | |
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 ); |
119 | 46 | |
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 | + } |
137 | 50 | |
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(); |
151 | 52 | |
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; |
165 | 63 | } |
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 | + }, |
169 | 78 | |
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 | + }, |
181 | 94 | |
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 | + }); |
190 | 120 | } |
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 | + }, |
194 | 133 | |
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 | + }); |
204 | 149 | } |
205 | | - }); |
206 | | -} |
| 150 | + }, |
207 | 151 | |
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 | + }, |
219 | 165 | |
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 | + } |
232 | 182 | } |
233 | | - } |
234 | | - return ours; |
235 | | -} |
| 183 | + return true; |
| 184 | + }, |
236 | 185 | |
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 | + }, |
248 | 197 | |
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]); |
261 | 245 | } |
262 | | - } |
263 | | - return ours; |
264 | | -} |
| 246 | + }, |
265 | 247 | |
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 | + }, |
276 | 264 | |
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]); |
289 | 274 | } |
290 | | - } |
291 | | - return ours; |
292 | | -} |
| 275 | + }, |
293 | 276 | |
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 | + } |
308 | 290 | } |
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]); |
317 | 302 | } |
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 | + } |
326 | 318 | } |
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 | + } |
328 | 353 | } |
Index: trunk/phase3/includes/ProtectionForm.php |
— | — | @@ -23,15 +23,36 @@ |
24 | 24 | * @todo document, briefly. |
25 | 25 | */ |
26 | 26 | class ProtectionForm { |
| 27 | + /** A map of action to restriction level, from request or default */ |
27 | 28 | var $mRestrictions = array(); |
| 29 | + |
| 30 | + /** The custom/additional protection reason */ |
28 | 31 | 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 */ |
30 | 37 | 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 */ |
33 | 49 | var $mPermErrors = array(); |
| 50 | + |
| 51 | + /** Types (i.e. actions) for which levels can be selected */ |
34 | 52 | var $mApplicableTypes = array(); |
35 | 53 | |
| 54 | + /** Map of action to the expiry time of the existing protection */ |
| 55 | + var $mExistingExpiry = array(); |
| 56 | + |
36 | 57 | function __construct( Article $article ) { |
37 | 58 | global $wgRequest, $wgUser; |
38 | 59 | global $wgRestrictionTypes, $wgRestrictionLevels; |
— | — | @@ -39,44 +60,57 @@ |
40 | 61 | $this->mTitle = $article->mTitle; |
41 | 62 | $this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create'); |
42 | 63 | |
43 | | - if( $this->mTitle ) { |
44 | | - $this->mTitle->loadRestrictions(); |
| 64 | + $this->mCascade = $this->mTitle->areRestrictionsCascading(); |
45 | 65 | |
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 | | - |
63 | 66 | // 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(); |
65 | 69 | $this->disabledAttrib = $this->disabled |
66 | 70 | ? array( 'disabled' => 'disabled' ) |
67 | 71 | : array(); |
68 | 72 | |
69 | 73 | $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); |
70 | | - $this->mReasonList = $wgRequest->getText( 'wpProtectReasonList' ); |
| 74 | + $this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' ); |
71 | 75 | $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade', $this->mCascade ); |
72 | | - |
| 76 | + |
73 | 77 | 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 ); |
77 | 87 | } |
78 | | - $this->mExpiry[$action] = $expiry[$action]; |
79 | | - $this->mExpiryList[$action] = $wgRequest->getText( "wpProtectExpiryList-$action", $this->mExpiry[$action] ? '' : 'infinite' ); |
| 88 | + $this->mExistingExpiry[$action] = $existingExpiry; |
80 | 89 | |
| 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 | + |
81 | 115 | $val = $wgRequest->getVal( "mwProtect-level-$action" ); |
82 | 116 | if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { |
83 | 117 | // Prevent users from setting levels that they cannot later unset |
— | — | @@ -93,6 +127,34 @@ |
94 | 128 | } |
95 | 129 | } |
96 | 130 | |
| 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 | + |
97 | 159 | function execute() { |
98 | 160 | global $wgRequest, $wgOut; |
99 | 161 | if( $wgRequest->wasPosted() ) { |
— | — | @@ -170,7 +232,7 @@ |
171 | 233 | } |
172 | 234 | |
173 | 235 | # Create reason string. Use list and/or custom string. |
174 | | - $reasonstr = $this->mReasonList; |
| 236 | + $reasonstr = $this->mReasonSelection; |
175 | 237 | if ( $reasonstr != 'other' && $this->mReason != '' ) { |
176 | 238 | // Entry from drop down menu + additional comment |
177 | 239 | $reasonstr .= ': ' . $this->mReason; |
— | — | @@ -179,33 +241,17 @@ |
180 | 242 | } |
181 | 243 | $expiry = array(); |
182 | 244 | 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; |
188 | 249 | } |
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; |
208 | 253 | } |
209 | 254 | } |
| 255 | + |
210 | 256 | # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied |
211 | 257 | # to a semi-protected page. |
212 | 258 | global $wgGroupPermissions; |
— | — | @@ -231,7 +277,6 @@ |
232 | 278 | } elseif( $this->mTitle->userIsWatching() ) { |
233 | 279 | $this->mArticle->doUnwatch(); |
234 | 280 | } |
235 | | - |
236 | 281 | return $ok; |
237 | 282 | } |
238 | 283 | |
— | — | @@ -241,19 +286,17 @@ |
242 | 287 | * @return $out string HTML form |
243 | 288 | */ |
244 | 289 | function buildForm() { |
245 | | - global $wgUser; |
| 290 | + global $wgUser, $wgLang; |
246 | 291 | |
247 | | - $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonList' ); |
| 292 | + $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonSelection' ); |
248 | 293 | $mProtectreason = Xml::label( wfMsg( 'protect-otherreason' ), 'mwProtect-reason' ); |
249 | 294 | |
250 | 295 | $out = ''; |
251 | 296 | if( !$this->disabled ) { |
252 | 297 | $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. |
255 | 298 | $out .= Xml::openElement( 'form', array( 'method' => 'post', |
256 | 299 | 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), |
257 | | - 'id' => 'mw-Protect-Form', 'onsubmit' => 'protectEnable(true)' ) ); |
| 300 | + 'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) ); |
258 | 301 | $out .= Xml::hidden( 'wpEditToken',$wgUser->editToken() ); |
259 | 302 | } |
260 | 303 | |
— | — | @@ -273,22 +316,38 @@ |
274 | 317 | "<tr><th>$label</th><th></th></tr>" . |
275 | 318 | "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td><td>"; |
276 | 319 | |
277 | | - $reasonDropDown = Xml::listDropDown( 'wpProtectReasonList', |
| 320 | + $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection', |
278 | 321 | wfMsgForContent( 'protect-dropdown' ), |
279 | | - wfMsgForContent( 'protect-otherreason-op' ), '', 'mwProtect-reason', 4 ); |
| 322 | + wfMsgForContent( 'protect-otherreason-op' ), |
| 323 | + $this->mReasonSelection, |
| 324 | + 'mwProtect-reason', 4 ); |
280 | 325 | $scExpiryOptions = wfMsgForContent( 'ipboptions' ); // FIXME: use its own message |
281 | 326 | |
282 | 327 | $showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled); |
283 | 328 | |
284 | | - $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpiryList-$action" ); |
| 329 | + $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpirySelection-$action" ); |
285 | 330 | $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"; |
287 | 343 | 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 | + } |
290 | 349 | $show = htmlspecialchars($show); |
291 | 350 | $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"; |
293 | 352 | } |
294 | 353 | # Add expiry dropdown |
295 | 354 | if( $showProtectOptions && !$this->disabled ) { |
— | — | @@ -300,16 +359,16 @@ |
301 | 360 | <td class='mw-input'>" . |
302 | 361 | Xml::tags( 'select', |
303 | 362 | 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)", |
307 | 366 | 'tabindex' => '2' ) + $this->disabledAttrib, |
308 | 367 | $expiryFormOptions ) . |
309 | 368 | "</td> |
310 | 369 | </tr></table>"; |
311 | 370 | } |
312 | 371 | # 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; |
314 | 373 | $out .= "<table><tr> |
315 | 374 | <td class='mw-label'>" . |
316 | 375 | $mProtectother . |
— | — | @@ -407,7 +466,7 @@ |
408 | 467 | 'id' => $id, |
409 | 468 | 'name' => $id, |
410 | 469 | 'size' => count( $levels ), |
411 | | - 'onchange' => 'protectLevelsUpdate(this)', |
| 470 | + 'onchange' => 'ProtectionForm.updateLevels(this)', |
412 | 471 | ) + $this->disabledAttrib; |
413 | 472 | |
414 | 473 | $out = Xml::openElement( 'select', $attribs ); |
— | — | @@ -440,7 +499,7 @@ |
441 | 500 | global $wgStylePath, $wgStyleVersion; |
442 | 501 | return Xml::tags( 'script', array( |
443 | 502 | 'type' => 'text/javascript', |
444 | | - 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion" ), '' ); |
| 503 | + 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion.1" ), '' ); |
445 | 504 | } |
446 | 505 | |
447 | 506 | function buildCleanupScript() { |
— | — | @@ -453,8 +512,15 @@ |
454 | 513 | } |
455 | 514 | } |
456 | 515 | $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)"; |
459 | 525 | return Xml::tags( 'script', array( 'type' => 'text/javascript' ), $script ); |
460 | 526 | } |
461 | 527 | |
Index: trunk/phase3/includes/Title.php |
— | — | @@ -1895,6 +1895,18 @@ |
1896 | 1896 | } |
1897 | 1897 | |
1898 | 1898 | /** |
| 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 | + /** |
1899 | 1911 | * Is there a version of this page in the deletion archive? |
1900 | 1912 | * @return \type{\int} the number of archived revisions |
1901 | 1913 | */ |
Index: trunk/phase3/languages/messages/MessagesEn.php |
— | — | @@ -2329,6 +2329,8 @@ |
2330 | 2330 | 'protect-cantedit' => 'You cannot change the protection levels of this page, because you do not have permission to edit it.', |
2331 | 2331 | 'protect-othertime' => 'Other time:', |
2332 | 2332 | 'protect-othertime-op' => 'other time', |
| 2333 | +'protect-existing-expiry' => 'Existing expiry time: $1', |
| 2334 | + |
2333 | 2335 | 'protect-otherreason' => 'Other/additional reason:', |
2334 | 2336 | 'protect-otherreason-op' => 'other/additional reason', |
2335 | 2337 | 'protect-dropdown' => '*Common protection reasons |