001package votorola.s.wic.diff; // Copyright 2010-2013, Michael Allan, Christian Weilbach. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. 002 003import java.io.*; 004import java.net.*; 005import java.nio.charset.*; 006import java.sql.SQLException; 007import java.util.*; 008import java.util.logging.*; import votorola.g.logging.*; 009import java.util.regex.*; 010import javax.script.ScriptException; 011import javax.xml.stream.*; 012import org.apache.wicket.Component; 013import org.apache.wicket.AttributeModifier; 014import org.apache.wicket.RestartResponseException; 015import org.apache.wicket.behavior.AttributeAppender; 016import org.apache.wicket.markup.html.IHeaderResponse; 017import org.apache.wicket.markup.html.WebMarkupContainer; 018import org.apache.wicket.markup.html.basic.Label; 019import org.apache.wicket.markup.html.form.Button; 020import org.apache.wicket.markup.html.form.CheckBox; 021import org.apache.wicket.markup.html.form.StatelessForm; 022import org.apache.wicket.markup.html.link.ExternalLink; 023import org.apache.wicket.markup.html.panel.Fragment; 024import org.apache.wicket.markup.repeater.RepeatingView; 025import org.apache.wicket.model.Model; 026import org.apache.wicket.request.mapper.parameter.PageParameters; 027import votorola.a.*; 028import votorola.a.count.*; 029import votorola.a.diff.*; 030import votorola.a.position.*; 031import votorola.a.web.wic.*; 032import votorola.a.web.wic.authen.*; 033import votorola.g.*; 034import votorola.g.hold.*; 035import votorola.g.io.*; 036import votorola.g.lang.*; 037import votorola.g.locale.*; 038import votorola.g.net.*; 039import votorola.g.web.wic.*; 040import votorola.s.wap.*; 041 042import static votorola.g.MediaWiki.API_POST_CHARSET; 043import static votorola.s.gwt.stage.vote.LightableDifference.REL_CANDIDATE; 044import static votorola.s.gwt.stage.vote.LightableDifference.REL_CO; 045import static votorola.s.gwt.stage.vote.LightableDifference.REL_TIGHT_CYCLE; 046import static votorola.s.gwt.stage.vote.LightableDifference.REL_UNKNOWN; 047import static votorola.s.gwt.stage.vote.LightableDifference.REL_VOTER; 048 049 050/** The Wicket interface of the difference bridge, which includes a view of the difference 051 * between a pair of draft revisions. Two major views (stage and difference) divide the 052 * overall layout and interconnect via specialized {@linkplain votorola.s.gwt.wic.DIn 053 * overlay graphics} (not shown):<pre> 054 * 055 * +--------------------------------------+ 056 * | stage | 057 * +--------------------------------------+ 058 * | | 059 * | | 060 * | | 061 * | | 062 * | difference | 063 * | | 064 * | | 065 * | | 066 * | | 067 * | | 068 * +--------------------------------------+</pre> 069 * 070 * <p>At the top is a Crossforum Theatre {@linkplain votorola.s.gwt.stage.StageV stage 071 * view} implented in GWT. The bulk of the page is occupied by a static HTML view of a 072 * difference between two drafts, together with controls for selectively patching between 073 * them. Only drafts published as MediaWiki pages are supported. The pair is specified 074 * by one or more query parameters which together comprise a "pair specifier". Several 075 * types of pair specifier are supported:</p> 076 * 077 * <h3>Difference key</h3> 078 * 079 * <p>Here is an example of a request using a difference key:</p> 080 * 081 * <blockquote><code><a href='http://reluk.ca:8080/v/w/D?k=3812.3556-3242.3004!1' target='_top' 082 * >http://reluk.ca:8080/v/w/D?k=3812.3556-3242.3004!1</a></code></blockquote> 083 * 084 * <table class='definition' style='margin-left:1em'> 085 * <tr> 086 * <th class='key'>Key</th> 087 * <th>Value</th> 088 * </tr> 089 * <tr><td class='key'>k</td> 090 * 091 * <td>The {@linkplain DiffKey difference key}.</td> 092 * 093 * </tr> 094 * </table> 095 * 096 * <h3>Convenience redirects</h3> 097 * 098 * <p>Choose one of the following. Each redirects to a difference view in normal 099 * voter-candidate order where applicable, or lexical order otherwise.</p> 100 * 101 * <table class='definition' style='margin-left:1em'> 102 * <tr> 103 * <th class='key'>Key</th> 104 * <th>Value</th> 105 * </tr> 106 * <tr><td class='key'>alterAuthor<br>poll</td> 107 * 108 * <td>The name of an author and a poll in the local wiki. The requester is 109 * redirected to the difference between the latest revisions of that author's 110 * draft and the authenticated user's draft. Parameter 's' is interpreted as 111 * though the user's revision will come first (a) and the other second (b), 112 * though the actual placement may be the opposite.</td> 113 * 114 * </tr> 115 * <tr><td class='key'>aAuthor<br>bAuthor<br>poll</td> 116 * 117 * <td>The names of two authors and a poll. The requester is redirected to the 118 * difference between the latest revisions of their position drafts.</td> 119 * 120 * </tr> 121 * </table> 122 * 123 * <h3>Legacy pair specifier</h3> 124 * 125 * <p>This obsolete, parsed form of a difference key specifier (k) is provided for the 126 * service of old links. It comprises up to 4 query parameters: 'a', 'aR', 'b', and 127 * 'bR'. For example: (fails because it cannot accommodate the revision series !1)</p> 128 * 129 * <blockquote><code><a href='http://reluk.ca:8080/v/w/D?a=3812&b=3242&aR=3556&bR=3004' target='_top' 130 * >http://reluk.ca:8080/v/w/D?a=3812&b=3242&aR=3556&bR=3004</a></code> (fails)</blockquote> 131 * 132 * <table class='definition' style='margin-left:1em'> 133 * <tr> 134 * <th class='key'>Key</th> 135 * <th>Value</th> 136 * </tr> 137 * <tr><td class='key'>a</td> 138 * 139 * <td>The first component in the revision path of the a-draft.</td> 140 * 141 * </tr> 142 * <tr><td class='key'>aR</td> 143 * 144 * <td>The second component in the revision path of the a-draft, if any.</td> 145 * 146 * </tr> 147 * <tr><td class='key'>b</td> 148 * 149 * <td>The first component in the revision path of the b-draft.</td> 150 * 151 * </tr> 152 * <tr><td class='key'>bR</td> 153 * 154 * <td>The second component in the revision path of the b-draft, if any.</td> 155 * 156 * </tr> 157 * </table> 158 * 159 * <h3>Other query parameters</h3> 160 * 161 * <table class='definition' style='margin-left:1em'> 162 * <tr> 163 * <th class='key'>Key</th> 164 * <th>Value</th> 165 * <th>Default</th> 166 * </tr> 167 * <tr><td class='key'>s</td> 168 * 169 * <td>The selectand specified as diff ordinal 'a' or 'b'. Use 's' or 's=b' to 170 * select the second revison of the pair. The choice affects links and other 171 * controls associated with the difference view and the stage.</td> 172 * 173 * <td>'a', or the first revision.</td> 174 * 175 * </tr> 176 * </table> 177 * 178 * @see <a href='http://reluk.ca/w/Category:Draft' target='_top'>Category:Draft</a> 179 * @see <a href='http://reluk.ca/w/Category:Draft_pointer' target='_top'>Category:Draft pointer</a> 180 * @see <a href='../../../../../../s/wic/diff/WP_D.html' target='_top'>WP_D.html</a> 181 */ 182 @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent 183public final class WP_D extends VPageHTML 184{ 185 186 // Planned URL mapping strategy: 187 // 188 // D -> WP_D, lt=1 (these are defaults) 189 // D?f=a1 -> WP_D, lt=1 190 // D?f=a2 -> WP_D, lt=2 191 // D?f=b2 -> WP_DiffBridgeB, lt=2 192 // 193 // Where form parameter (f) is unpacked into page layout and line transformer version. 194 // The URL could be squeezed further at any time by a path mounting, like D/a1, etc. 195 196 197 /** Constructs a WP_D. 198 */ 199 public WP_D( final PageParameters pP ) throws IOException, SQLException, XMLStreamException 200 { // bookmarkable page iff constructor public & (default|PageParameter) 201 super( pP ); 202 // VOWicket.get().vsRun().voteServer().diffCache().LINE_TRANSFORMER.test(); // TEST 203 final boolean bToSelect; 204 { 205 final String s = PageParametersX.getString( pP, "s" ); 206 if( s == null || "a".equals(s) ) bToSelect = false; 207 else if( "".equals(s) || "b".equals(s) ) bToSelect = true; 208 else 209 { 210 VSession.get().error( "bad value for query parameter 's': " + s ); 211 throw new RestartResponseException( new WP_Message() ); 212 } 213 } 214 215 // Redirect if requested 216 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 217 final VRequestCycle cycle = VRequestCycle.get(); 218 final VOWicket app = VOWicket.get(); 219 final VSession.User userOrNull = VSession.get().user(); 220 final VoteServer.Run vsRun = app.vsRun(); 221 final VoteServer vS = vsRun.voteServer(); 222 final PollwikiVS wiki = vS.pollwiki(); 223 try 224 { 225 // alterAuthor 226 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 227 final String alterAuthor = stringNonEmpty( pP, "alterAuthor" ); 228 if( alterAuthor != null ) 229 { 230 pP.remove( "alterAuthor" ); 231 final String pollName = stringRequired( pP, "poll" ); 232 pP.remove( "poll" ); 233 if( userOrNull == null ) 234 { 235 pP.set( "returnClass", getClass().getName() ); 236 throw new RestartResponseException( app.authenticator().newLoginPage( pP )); 237 } 238 239 final String username = userOrNull.username(); 240 if( username.equals( alterAuthor )) 241 { 242 VSession.get().info( "attempt to diff latest of same author: " + alterAuthor ); 243 throw new RestartResponseException( new WP_Message() ); 244 } 245 246 final CountSource1 countSource = new CountSource1( vsRun ); 247 DraftPair pair = DraftPair.newDraftPair( wiki.positionPageName(alterAuthor,pollName), 248 wiki.positionPageName(username,pollName), vsRun, countSource ); 249 final Count count = countSource.count( pollName ); 250 if( count != null ) 251 { 252 final CountNodeW bNode = count.countTablePV().get( userOrNull.email() ); 253 if( bNode != null ) 254 { 255 if( pair.aCore().person().email().equals( bNode.getCandidateEmail() )) 256 { 257 pair = pair.newReversePair(); 258 } 259 } 260 } 261 if( pair.bCore().person().equals( userOrNull )) // then reverse sense of selectand s 262 { 263 if( bToSelect ) pP.remove( "s" ); 264 else pP.set( "s", "" ); 265 } 266 pP.set( "k", pair.diffKeyParse().key() ); 267 throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 ); 268 } 269 270 // aAuthor, bAuthor, poll 271 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 272 final String aAuthor = stringNonEmpty( pP, "aAuthor" ); 273 if( aAuthor != null ) 274 { 275 pP.remove( "aAuthor" ); 276 final String bAuthor = stringRequired( pP, "bAuthor" ); 277 pP.remove( "bAuthor" ); 278 if( aAuthor.equals( bAuthor )) 279 { 280 VSession.get().info( "attempt to diff latest of same author: " + aAuthor ); 281 throw new RestartResponseException( new WP_Message() ); 282 } 283 284 final String pollName = stringRequired( pP, "poll" ); 285 pP.remove( "poll" ); 286 DraftPair pair = DraftPair.newDraftPair( wiki.positionPageName(aAuthor,pollName), 287 wiki.positionPageName(bAuthor,pollName), vsRun, /*countSource*/null ); 288 if( !pair.aCore().person().username().equals( aAuthor )) 289 { 290 pair = pair.newReversePair(); 291 } 292 pP.set( "k", pair.diffKeyParse().key() ); 293 throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 ); 294 } 295 296 // ` ` ` 297 // voterDraft - removed per note [1] 298 } 299 catch( final IOException x ) 300 { 301 if( !(x instanceof UserInformative )) throw x; 302 303 VSession.get().error( ThrowableX.toStringExpanded( x )); 304 throw new RestartResponseException( new WP_Message() ); 305 } 306 307 // Query wiki for drafts 308 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 309 { 310 final String k = stringNonEmpty( pP, "k" ); 311 final DiffKeyParse kP; 312 try 313 { 314 if( k != null ) kP = new DiffKeyParse( k ); 315 else 316 { 317 kP = new DiffKeyParse( legacyRev(pP,"a"), legacyRevOptional(pP,"aR"), 318 legacyRev(pP,"b"), legacyRevOptional(pP,"bR") ); 319 } 320 } 321 catch( final DiffKeyParse.MalformedKey x ) 322 { 323 VSession.get().error( x.toString() ); 324 throw new RestartResponseException( new WP_Message() ); 325 } 326 327 try{ pair = DraftPair.newDraftPair( kP, wiki ); } 328 catch( final IOException x ) 329 { 330 if( !( x instanceof UserInformative )) throw x; 331 332 VSession.get().error( ThrowableX.toStringExpanded( x )); 333 throw new RestartResponseException( new WP_Message() ); 334 } 335 } 336 337 // Redirect if pair misordered, per DiffKey normal ordering, R/C/U rules 338 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 339 final CoreRevision aCore = pair.aCore(); 340 final CoreRevision bCore = pair.bCore(); 341 final String aUsername = aCore.person().username(); 342 final String bUsername = bCore.person().username(); 343 char rel = REL_UNKNOWN; // style symbol for cast relation of a to b 344 { 345 final DiffKeyParse kP = pair.diffKeyParse(); 346 boolean isOrdered = true; // safer assumption to prevent redirect cycle 347 order: if( aUsername.equals( bUsername )) // same author, order by revision path R1 348 { 349 final List<Integer> aPath = kP.aPath(); 350 final List<Integer> bPath = kP.bPath(); 351 int r = 0; 352 for( ;; ) 353 { 354 final int comparison = aPath.get(r).compareTo( bPath.get(r) ); 355 if( comparison > 0 ) break; // R1 356 else if( comparison < 0 ) 357 { 358 isOrdered = false; // R1 359 break; 360 } 361 362 ++r; 363 if( r == aPath.size() ) 364 { 365 if( r == bPath.size() ) 366 { 367 VSession.get().info( "attempt to diff same draft: " + aCore ); 368 throw new RestartResponseException( new WP_Message() ); 369 } 370 371 isOrdered = false; // R2 372 break; 373 } 374 375 if( r == bPath.size() ) break; // R2 376 } 377 } 378 else // two authors 379 { 380 final String pollName = aCore.pollName(); 381 if( pollName.equals( bCore.pollName() )) // same poll 382 { 383 final Count count; 384 try{ count = vsRun.scopePoll().ensurePoll(pollName).countToReportT(); } 385 catch( ScriptException x ) { throw new RuntimeException( x ); } 386 387 if( count != null ) 388 { 389 final CountTable.PollView countTablePV = count.countTablePV(); 390 final CountNodeW aNode = countTablePV.get( aCore.person().email() ); 391 if( aNode != null ) 392 { 393 final CountNodeW bNode = countTablePV.get( bCore.person().email() ); 394 if( bNode != null ) 395 { 396 final String aCanEmail = aNode.getCandidateEmail(); 397 final String bCanEmail = bNode.getCandidateEmail(); 398 if( bNode.email().equals( aCanEmail )) 399 { 400 // if( aNode.holdVolume() == 0 ) 401 /// (a) with impersonal nodes, now invalid cast-relation test, so: 402 if( !aNode.isBaseCandidate() ) 403 { 404 rel = bToSelect? REL_VOTER: REL_CANDIDATE; // C2 405 break order; 406 } 407 408 // assert bNode.holdVolume() != 0; // but (a), so: 409 assert bNode.isBaseCandidate(); // must be co-base 410 if( aNode.email().equals( bCanEmail )) 411 { 412 rel = REL_TIGHT_CYCLE; // C3, U1 or U2 413 isOrdered = DiffKey.isDartOrdered( aNode, aUsername, bNode, 414 bUsername ); 415 } 416 else rel = REL_CO; // C1 417 break order; 418 } 419 420 if( aNode.email().equals( bCanEmail )) 421 { 422 // if( bNode.holdVolume() == 0 ) // but (a), so: 423 if( !bNode.isBaseCandidate() ) 424 { 425 rel = bToSelect? REL_CANDIDATE: REL_VOTER; // C2 426 } 427 else rel = REL_CO; // C1 co-base, not tight, caught earlier 428 isOrdered = false; 429 break order; 430 } 431 432 if( aCanEmail != null && aCanEmail.equals(bCanEmail) // co-voter 433 // || aNode.holdVolume() > 0 && bNode.holdVolume() > 0 ) 434 /// but (a), so: 435 || aNode.isBaseCandidate() && bNode.isBaseCandidate() ) // co-base 436 { 437 rel = REL_CO; // C3, U1 or U2 438 isOrdered = DiffKey.isDartOrdered( aNode, aUsername, bNode, 439 bUsername ); 440 break order; 441 } 442 } 443 } 444 } 445 } 446 isOrdered = DiffKey.isLexicallyOrdered( aUsername, bUsername ); // U1 or U2 447 } 448 if( !isOrdered ) 449 { 450 pP.set( "k", DiffKey.newReverseKey(kP.key()) ); 451 if( bToSelect ) pP.remove( "s" ); 452 else pP.set( "s", "" ); 453 throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 ); 454 } 455 } 456 457 // Crossforum Theatre stage 458 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 459 { 460 final StringBuilder b = WC_Stage.appendLeader( userOrNull, vS, cycle ); 461 final String selectand; 462 final CoreRevision anchorCore; 463 final CoreRevision otherCore; 464 if( bToSelect ) 465 { 466 selectand = "b"; 467 anchorCore = bCore; 468 otherCore = aCore; 469 } 470 else 471 { 472 selectand = "a"; 473 anchorCore = aCore; 474 otherCore = bCore; 475 } 476 477 // s_gwt_stage 478 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 479 b.append( 480 "_temp = function()" 481 + "{" 482 + "var fThis = arguments.callee;" 483 + "if( fThis.fWrapped ) fThis.fWrapped();" // call admin's own config, if any 484 + "s_gwt_stage_link_NominalDifferenceTargeter_setEnabled();" 485 + "s_gwt_stage_vote_CountNodeV_setDeselectionGuard( 'Lax' );" 486 // Allow click to deselect any node in vote track. With default actor 487 // set, 'Default' would be same as 'Lax' but slightly slower. 488 + "s_gwt_stage_vote_DifferenceLight_setScene( '" ); 489 b.append( otherCore.person().username() ).append( "', '" ); 490 b.append( rel ).append( "' );" 491 + "};" 492 + "_temp.fWrapped = voGWTConfig.s_gwt_stage;" 493 + "voGWTConfig.s_gwt_stage = _temp;" ); 494 495 // s_gwt_stage_Stage_init, per WC_Stage below 496 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 497 b.append( 498 "voGWTConfig.s_gwt_stage_Stage_init = function()" 499 + "{" 500 + "s_gwt_stage_Stage_setDefaultActorName( '" ); 501 b.append( anchorCore.person().username() ).append( "' );" 502 + "s_gwt_stage_Stage_setDefaultDifference(" 503 + "{" 504 + "key:'" ).append( pair.diffKeyParse().key() ).append( "'," 505 + "selectand:'" ).append( selectand ).append( "'" 506 + "});" 507 + "s_gwt_stage_Stage_setDefaultPollName( '" ); 508 b.append( anchorCore.pollName() ).append( "' );" 509 + "};" ); 510 511 // ` ` ` 512 add( new WC_Stage( "stage", "votorola.s.gwt.wic.DIn", b, cycle )); 513 } 514 515 // RENDER VIEW 516 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 517 final BundleFormatter bunW = cycle.bunW(); 518 { 519 CoreRevision d = aCore; 520 if( d.person().equals( userOrNull )) userCoreOrNull = d; 521 else 522 { 523 d = bCore; 524 if( d.person().equals( userOrNull )) userCoreOrNull = d; 525 } 526 527 if( bCore.person().equals( aCore.person() )) 528 { 529 patchBar = bunW.l( "s.wic.diff.WP_D.patchBar.sameUser" ); 530 } 531 else if( userOrNull == null ) patchBar = PATCH_BAR_LOGGED_OUT; 532 else if( userCoreOrNull == null ) patchBar = PATCH_BAR_NON_AUTHOR; 533 else if( userCoreOrNull instanceof ComponentPipeRevision ) 534 { 535 patchBar = bunW.l( "s.wic.diff.WP_D.patchBar.componentPipe" ); 536 } 537 } 538 setPageIcon( cycle.staticContextLocation() + "/diff/diff.png" ); 539 add( new Label( "title", bunW.l( "s.wic.diff.WP_D.title", 540 pair.aUserMnemonic(), pair.bUserMnemonic() ))); 541 add( new WC_NavigationHead( "navHead", WP_D.this, cycle )); 542 543 // Diff 544 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 545 final DiffForm form; 546 final String patchButtonValue = bunW.l( "s.wic.diff.WP_D.patchButtonValue" ); 547 final String patchButtonTitle = bunW.l( "s.wic.diff.WP_D.patchButtonTitle" ); 548 549 diffFile = vS.diffCache().diffFile( pair ); 550 final LineNumberReader in = new LineNumberReader( new InputStreamReader( 551 new FileInputStream(diffFile), Charset.defaultCharset() )); 552 try 553 { 554 555 // Diff header 556 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 557 String line; 558 line = in.readLine(); 559 if( line == null || line.length() == 0 ) throw new BadDiffException( "no diff output", diffFile ); 560 561 if( !line.startsWith( "---" )) 562 { 563 final Matcher m = DiffCache.NO_DIFF_PATTERN.matcher( line ); 564 if( !m.matches() ) throw new BadDiffException( "diff says '" + line + "'", diffFile ); 565 566 VSession.get().info( "no difference between draft revisions " + aCore 567 + " and " + bCore + " (FIX to handle this more gracefully)" ); 568 throw new RestartResponseException( new WP_Message() ); 569 } 570 571 { 572 final PageParameters linkP = new PageParameters( pP ); 573 final BookmarkablePageLinkX aLink; 574 { 575 add( new Label( "a-aMnemonic", pair.aUserMnemonic() )); 576 aLink = new BookmarkablePageLinkX( "a-a", WP_D.class, linkP ); 577 aLink.setBody( aUsername ); 578 add( aLink ); 579 } 580 line = in.readLine(); 581 if( !line.startsWith( "+++" )) throw new BadDiffException( "missing '+++' line in diff header", diffFile ); 582 583 final BookmarkablePageLinkX bLink; 584 { 585 add( new Label( "b-aMnemonic", pair.bUserMnemonic() )); 586 bLink = new BookmarkablePageLinkX( "b-a", WP_D.class, linkP ); 587 bLink.setBody( bUsername ); 588 add( bLink ); 589 } 590 final BookmarkablePageLinkX sLink; 591 final BookmarkablePageLinkX tLink; // other one 592 if( bToSelect ) 593 { 594 sLink = bLink; 595 tLink = aLink; 596 linkP.remove( "s" ); 597 } 598 else 599 { 600 sLink = aLink; 601 tLink = bLink; 602 linkP.set( "s", "" ); 603 } 604 appendStyleClass( sLink, "selected" ); 605 sLink.setEnabled( false ); 606 sLink.add( AttributeModifier.replace( "id", "sLink" )); // for AnchorLine 607 tLink.add( AttributeModifier.replace( "id", "tLink" )); // for AlterLine 608 } 609 610 // Patch controls (top) 611 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 612 form = new DiffForm(); add( form ); 613 if( patchBar == null ) appendStyleClass( form, "patchNoBar" ); 614 615 addPatchButton( form, "patchControlTop", patchButtonValue, patchButtonTitle, bunW ); 616 617 // Hunks 618 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 619 final RepeatingView tbodyRepeating = new RepeatingView( "tbody" ); 620 form.add( tbodyRepeating ); 621 line = in.readLine(); 622 int h = 1; 623 while( line != null ) line = addHunkTo( tbodyRepeating, line, in, diffFile, h++, cycle ); 624 } 625 finally{ in.close(); } 626 appendStyleClass( form, "rel-" + rel ); 627 628 // Patch controls (bottom) 629 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 630 addPatchButton( form, "patchControlBottom", patchButtonValue, patchButtonTitle, bunW ); 631 632 // Feedback messages 633 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 634 add( new WC_Feedback( "feedback" )); 635 636 // = = = 637 setCacheable( true ); 638 setCacheDuration( CACHE_DURATION_YEAR ); 639 } 640 641 642 643 // - I - H e a d e r - C o n t r i b u t o r ------------------------------------------ 644 645 646 public @Override void renderHead( IHeaderResponse r ) 647 { 648 super.renderHead( r ); 649 if( !getSession().getFeedbackMessages().isEmpty() ) 650 { 651 r.renderOnLoadJavaScript( "location.hash = 'feedback'" ); 652 } 653 } 654 655 656 657//// P r i v a t e /////////////////////////////////////////////////////////////////////// 658 659 660 /** @param line the header of the hunk. 661 * @param h the hunk number. 662 * 663 * @return the header of the next hunk, or null if there is none. 664 */ 665 private String addHunkTo( final RepeatingView tbodyRepeating, String line, 666 final LineNumberReader in, final File diffFile, final int h, final VRequestCycle cycle ) 667 throws IOException 668 { 669 if( !line.startsWith( "@@" )) throw new BadDiffException( "bad hunk header '" + line + "'", diffFile ); 670 671 final BundleFormatter bunW = cycle.bunW(); 672 final Hunk hunk = new Hunk(); 673 hunkList.add( hunk ); 674 675 final String hunkID = "_" + h; 676 final WebMarkupContainer tbody = new WebMarkupContainer( tbodyRepeating.newChildId() ); 677 tbody.add( AttributeModifier.replace( "id", hunkID )); // here rather than in td 678 // else when targeted, browser may scroll horizontally. FIX, it still scrolls a little. 679 tbodyRepeating.add( tbody ); 680 681 // Checkbox clicker 682 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 683 final String checkID = "c" + h; 684 final AttributeModifier checkTitler = AttributeModifier.replace( "title", 685 bunW.l( "s.wic.diff.WP_D.checkTitle" )); 686 { 687 final CheckBox check = new CheckBox( "inputCheck", hunk ); 688 tbody.add( check ); 689 690 check.add( AttributeModifier.replace( "id", checkID )); 691 if( patchBar == null ) 692 { 693 check.add( checkTitler ); 694 check.add( new AttributeAppender( "onclick", 695 new Model<String>( "_a_diff_WP_D.onPatchCheckboxClick( " + h + " )" ), "; " )); 696 } 697 else check.setEnabled( false ); 698 } 699 { 700 final WebMarkupContainer td = new WebMarkupContainer( "tdCheck" ); 701 tbody.add( td ); 702 if( patchBar == null ) 703 { 704 // td.add( checkTitler ); 705 //// annoying 706 td.add( new AttributeAppender( "onclick", 707 new Model<String>( "document.getElementById( '" + checkID + "' ).click()" ), "; " )); 708 } 709 } 710 711 // Lines 712 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 713 final RepeatingView trRepeating = new RepeatingView( "tr" ); 714 tbody.add( trRepeating ); 715 716 final Model<String> aMnemonic = new Model<String>( pair.aUserMnemonic() ); 717 final Model<String> bMnemonic = new Model<String>( pair.bUserMnemonic() ); 718 int segment = 0; // in-hunk difference, separated by one or more context lines 719 boolean isSegmentIDSet = false; 720 char prefixCharLast = 0; // force refresh 721 String lineABClass = null; 722 String lineUserClass = null; 723 WebMarkupContainer trHunkTailContext = null; // top line of trailing context, if any 724 for( ;; ) 725 { 726 line = in.readLine(); 727 if( line == null ) 728 { 729 hunk.boundaryLine = in.getLineNumber() + 1; 730 break; 731 } 732 733 if( line.startsWith( "@@" )) 734 { 735 hunk.boundaryLine = in.getLineNumber(); 736 break; 737 } 738 739 final WebMarkupContainer tr = new WebMarkupContainer( trRepeating.newChildId() ); 740 trRepeating.add( tr ); 741 742 // th 743 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 744 final char prefixChar = line.charAt( 0 ); 745 final Component th; 746 if( prefixChar != prefixCharLast ) 747 { 748 if( prefixChar == ' ' ) // context line 749 { 750 final Fragment y = new Fragment( "th", "segmentLinkFrag", WP_D.this ); 751 appendStyleClass( y, "segment" ); 752 ++segment; // transit to context marks start of segment, or tail of hunk 753 trHunkTailContext = tr; // till proven otherwise 754 755 lineABClass = null; 756 lineUserClass = null; 757 final String segmentID = setSegmentID( hunkID, segment, tr ); 758 isSegmentIDSet = true; 759 y.add( new ExternalLink( "link", /*href*/"#" + segmentID, 760 /*label*/segmentID.substring(1) )); 761 th = y; 762 } 763 else // difference line 764 { 765 th = new Label( "th" ); 766 appendStyleClass( th, "mnemonic" ); 767 trHunkTailContext = null; // we cannot be in the tail, yet 768 if( !isSegmentIDSet ) // hunk lacks leading context, ID not set above 769 { 770 ++segment; // all the same, we must be in a new segment 771 setSegmentID( hunkID, segment, tr ); // set it here on a diff line 772 isSegmentIDSet = true; 773 } 774 775 final Model<String> mnemonic; 776 final CoreRevision mnemonicCore; 777 if( prefixChar == '-' ) 778 { 779 mnemonic = aMnemonic; 780 mnemonicCore = pair.aCore(); 781 lineABClass = "a"; 782 } 783 else 784 { 785 mnemonic = bMnemonic; 786 mnemonicCore = pair.bCore(); 787 lineABClass = "b"; 788 } 789 lineUserClass = userCoreOrNull == mnemonicCore? "u": "o"; 790 th.setDefaultModel( mnemonic ); 791 } 792 prefixCharLast = prefixChar; 793 } 794 else th = new WebMarkupContainer( "th" ); // empty 795 tr.add( th ); 796 797 if( lineABClass != null ) appendStyleClass( tr, lineABClass ); 798 if( lineUserClass != null ) appendStyleClass( tr, lineUserClass ); 799 800 // td 801 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 802 String lineText = line.substring( 1 ); 803 final boolean isEmpty = "".equals( lineText ); 804 if( isEmpty ) lineText = " "; // ensure row is rendered, give it some guts 805 806 final Label td = new Label( "td", lineText ); 807 td.setEscapeModelStrings( !isEmpty ); 808 tr.add( td ); 809 } 810 811 if( trHunkTailContext != null ) appendStyleClass( trHunkTailContext, "hunkTail" ); 812 813 return line; 814 } 815 816 817 818 private void addPatchButton( final WebMarkupContainer y, final String id, 819 final String patchButtonValue, final String patchButtonTitle, final BundleFormatter bunW ) 820 { 821 final Fragment yy = new Fragment( id, "patchControlFrag", WP_D.this ); 822 y.add( yy ); 823 824 final Button button = new Button( "button" ); 825 button.add( AttributeModifier.replace( "value", patchButtonValue )); 826 button.add( AttributeModifier.replace( "title", patchButtonTitle )); 827 yy.add( button ); 828 829 final Component loginLink; 830 if( patchBar == null ) loginLink = newNullComponent( "loginLink" ); 831 else 832 { 833 button.setEnabled( false ); 834 if( patchBar.equals( PATCH_BAR_LOGGED_OUT )) 835 { 836 loginLink = new WC_LoginLink( "loginLink", WP_D.this, 837 bunW.l( "s.wic.diff.WP_D.login" )); 838 } 839 else if( patchBar.equals( PATCH_BAR_NON_AUTHOR )) // typical case, make no noise 840 { 841 loginLink = newNullComponent( "loginLink" ); 842 } 843 else // rare edge case, just render it as a disabled link for now 844 { 845 loginLink = new ExternalLink( "loginLink", ".", patchBar ); 846 loginLink.setEnabled( false ); 847 } 848 } 849 yy.add( loginLink ); 850 } 851 852 853 854 private static void appendIfFile( final File fileOrNull, final Charset charset, 855 final Writer out ) throws IOException 856 { 857 if( fileOrNull != null ) FileX.appendTo( out, fileOrNull, charset ); 858 } 859 860 861 862 private static void appendIfFile( final File fileOrNull, final Charset charset, 863 final Writer out, final LineTransformer1 transformer ) throws IOException 864 { 865 if( fileOrNull == null ) return; 866 867 final BufferedReader in = new BufferedReader( new InputStreamReader( 868 new FileInputStream(fileOrNull), charset )); 869 try 870 { 871 for( ;; ) 872 { 873 final String l = in.readLine(); 874 if( l == null ) break; 875 876 transformer.appendToWiki( l, out ); 877 } 878 } 879 finally{ in.close(); } 880 } 881 882 883 884 private final File diffFile; 885 886 887 888 private static final String FAILURE_PAGE_TITLE = "Unable to patch"; 889 // failure messages not currently localized 890 891 892 893 // /** The pattern of a hunk header. 894 // */ 895 // private static final Pattern HUNK_HEADER_PATTERN = Pattern.compile( 896 // "^@@ -\\d+,\\d+ \\+\\d+,\\d+ @@$" ); 897 // // @@ -11,6 +12,7 @@ 898 899 900 901 private ArrayList<Hunk> hunkList = new ArrayList<Hunk>(); 902 903 904 905 private static int legacyRev( final PageParameters pP, final String key ) 906 { 907 final int rev = legacyRevOptional( pP, key ); 908 if( rev < 0 ) 909 { 910 VSession.get().error( "missing query parameter 'k'" ); 911 throw new RestartResponseException( new WP_Message() ); 912 } 913 914 return rev; 915 } 916 917 918 919 private static int legacyRevOptional( final PageParameters pP, final String keyR ) 920 { 921 final String revString = stringNonEmpty( pP, keyR ); 922 if( revString == null ) return -1; 923 924 final int rev = Integer.parseInt( revString ); 925 if( rev < 0 ) throw new IllegalArgumentException(); 926 927 return rev; 928 } 929 930 931 932 private static final Logger logger = LoggerX.i( WP_D.class ); 933 934 935 936 private DraftPair pair; 937 938 939 940 /** If non-null, patching is disallowed. 941 */ 942 private String patchBar; 943 944 945 946 private static final String PATCH_BAR_LOGGED_OUT = "Login required"; 947 948 private static final String PATCH_BAR_NON_AUTHOR = "Patching others' drafts not allowed"; 949 950 951 952 private static String setSegmentID( final String hunkID, final int segment, 953 final WebMarkupContainer tr ) 954 { 955 final String segmentID = hunkID + "." + segment; 956 tr.add( AttributeModifier.replace( "id", segmentID )); 957 return segmentID; 958 } 959 960 961 962 /** Either pair.aCore or pair.bCore, whichever is the user's. 963 */ 964 private CoreRevision userCoreOrNull; 965 966 967 968 // ==================================================================================== 969 970 971 private static final class BadDiffException extends RuntimeException 972 { 973 BadDiffException( final String message, final File diffFile ) 974 { 975 super( message + ": " + diffFile ); 976 } 977 } 978 979 980 981 // ==================================================================================== 982 983 984 private final class DiffForm extends StatelessForm<Void> 985 { 986 987 private DiffForm() { super( "form" ); } 988 989 990 protected @Override void onSubmit() 991 { 992 super.onSubmit(); 993 if( patchBar != null ) throw new IllegalStateException(); // probably impossible 994 995 final VRequestCycle cycle = VRequestCycle.get(); 996 997 // Ensure at least one hunk is included 998 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 999 int hunkCount = 0; 1000 for( final Hunk hunk: hunkList ) if( hunk.getObject() ) ++hunkCount; 1001 if( hunkCount == 0 ) 1002 { 1003 VSession.get().getFeedbackMessages().warn( /*reporter*/DiffForm.this, 1004 cycle.bunW().l( "s.wic.diff.WP_D.patchFail.noHunk" )); 1005 return; 1006 } 1007 1008 final DraftRevision userDraft = userCoreOrNull.draft(); 1009 final URI targetScriptLocation = userDraft.wikiScriptURI(); 1010 try 1011 { 1012 final Spool tmpFileSpool = new Spool1(); 1013 1014 // Construct the patch file from the selected hunks 1015 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1016 final File patchFile = File.createTempFile( "WP_D_patch", "." + userDraft.pageID() ); 1017 tmpFileSpool.add( new FileHold( patchFile )); 1018 // Create patch as file rather than feeding it in via stdin. I want to 1019 // pull the output via stdout and don't want to mess with threads. For 1020 // an example of that alternative, see thread 'feeder': 1021 // http://reluk.ca/var/db/repo/votorola/file/fd139156408c/votorola/a/diff/WP_Diff.java 1022 final Charset nativeCharset = Charset.defaultCharset(); 1023 { 1024 final LineNumberReader in = new LineNumberReader( new InputStreamReader( 1025 new FileInputStream(diffFile), nativeCharset )); 1026 // reading from native charset, only for sake of line counting 1027 try 1028 { 1029 final BufferedWriter out = new BufferedWriter( new OutputStreamWriter( 1030 new FileOutputStream(patchFile), nativeCharset )); // back to native 1031 try 1032 { 1033 boolean toInclude = true; // i.e. include all lines (header) prior to first hunk 1034 for( int h = -1, boundaryLine = 3;; ) // the first hunk starts at line 3 1035 { 1036 final String l = in.readLine(); 1037 if( l == null ) break; 1038 1039 if( in.getLineNumber() == boundaryLine ) // on a new hunk 1040 { 1041 ++h; 1042 final Hunk hunk = hunkList.get( h ); 1043 toInclude = hunk.getObject(); 1044 boundaryLine = hunk.boundaryLine; 1045 } 1046 1047 if( !toInclude ) continue; 1048 1049 out.append( l ); 1050 out.newLine(); 1051 } 1052 } 1053 finally{ out.close(); } 1054 } 1055 finally{ in.close(); } 1056 } 1057 1058 // Fetch the user's draft text as the target file 1059 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1060 final File[] targetFileSplit = DiffCache.LINE_TRANSFORMER.fetchPageAsFile( 1061 targetScriptLocation, "curid", userDraft.pageID(), "WP_D_target", tmpFileSpool ); 1062 final File targetFile = targetFileSplit[1]; 1063 1064 // Apply the patch to the target file 1065 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1066 final boolean isReversePatch = userCoreOrNull.equals( pair.bCore() ); 1067 final CoreRevision otherCore = // as opposed to userCoreOrNull 1068 isReversePatch? pair.aCore(): pair.bCore(); 1069 final StringBuilder outB = new StringBuilder(); // not currently localized 1070 { 1071 final ProcessBuilder pB = new ProcessBuilder( "/bin/bash", "-c", 1072 "patch --force --no-backup-if-mismatch --unified " 1073 + (isReversePatch? "--reverse ": "") 1074 + targetFile.getName() + " " + patchFile.getName() + " 2>&1" ); 1075 pB.directory( patchFile.getParentFile() ); 1076 logger.fine( "calling out to OS: " + pB.command() ); 1077 pB.directory( patchFile.getParentFile() ); 1078 final Process p = pB.start(); 1079 outB.append( "PATCHING" ).append( '\n' ); 1080 outB.append( "--------" ).append( '\n' ); 1081 ProcessX.appendTo( outB, p, nativeCharset ); 1082 final int exitValue = ProcessX.waitForWithoutInterrupt( p ); 1083 if( exitValue == 1 ) // some hunks won't apply or merge hit conflicts 1084 { 1085 outB.append( '\n' ); 1086 outB.append( "patch attempt failed" ).append( '\n' ); 1087 cycle.setResponsePage( new WP_Message( FAILURE_PAGE_TITLE, 1088 outB.toString() ).pre()); 1089 return; 1090 } 1091 else if( exitValue != 0 ) // == 2 which means severe error 1092 { 1093 throw new IOException( "exit value of " + exitValue + " from process: " 1094 + pB.command() + " : " + outB.toString() ); 1095 } 1096 } 1097 1098 // Login if necessary (currently always) 1099 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1100 final URI api; 1101 try{ api = new URI( userDraft.wikiScriptURI().toASCIIString() + "/api.php" ); } 1102 catch( URISyntaxException x ) { throw new RuntimeException( x ); } 1103 1104 String editToken = null; 1105 token: if( editToken == null ) 1106 { 1107 outB.append( '\n' ); 1108 outB.append( "LOGGING IN" ).append( '\n' ); 1109 outB.append( "----------" ).append( '\n' ); 1110 { 1111 final String entry = "requesting login to " + api; 1112 logger.fine( entry ); 1113 outB.append( entry ).append( '\n' ); 1114 } 1115 final VOWicket app = VOWicket.get(); 1116 final CookieHandler cookieHandler = app.cookieManager(); 1117 final String errorMessage = MediaWiki.login( // FIX by detecting existing login in prior query or cookies 1118 api, cookieHandler, "Vobot", app.vsRun().voteServer().pollwiki().password() ); 1119 if( errorMessage != null ) 1120 { 1121 outB.append( '\n' ); 1122 outB.append( errorMessage ); 1123 outB.append( '\n' ); 1124 cycle.setResponsePage( new WP_Message( FAILURE_PAGE_TITLE, 1125 outB.toString() ).pre()); 1126 return; 1127 } 1128 1129 // request edit token 1130 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1131 final URI queryURI; 1132 try 1133 { 1134 queryURI = new URI( api.toString() 1135 + "?format=xml&action=query&prop=info&intoken=edit&pageids=" 1136 + userDraft.pageID() ); 1137 logger.fine( "requesting edit token for user's draft: " + queryURI ); 1138 } 1139 catch( URISyntaxException x ) { throw new RuntimeException( x ); } 1140 1141 final URLConnection http = queryURI.toURL().openConnection(); 1142 URLConnectionX.setRequestCookies( queryURI, http, cookieHandler ); // after other req headers 1143 final Spool spool = new Spool1(); 1144 try 1145 { 1146 final XMLStreamReader xml = MediaWiki.requestXML( http, spool ); 1147 cookieHandler.put( queryURI, http.getHeaderFields() ); 1148 while( xml.hasNext() ) 1149 { 1150 xml.next(); 1151 if( !xml.isStartElement() ) continue; 1152 1153 if( "page".equals( xml.getLocalName() )) 1154 { 1155 editToken = xml.getAttributeValue( /*ns*/null, "edittoken" ); 1156 break token; 1157 } 1158 1159 MediaWiki.test_badrevids( xml ); 1160 MediaWiki.test_error( xml ); 1161 } 1162 } 1163 finally{ spool.unwind(); } 1164 throw new IllegalStateException(); 1165 } 1166 1167 // Post patched file to wiki 1168 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1169 outB.append( '\n' ); 1170 outB.append( "POSTING" ).append( '\n' ); 1171 outB.append( "-------" ).append( '\n' ); 1172 { 1173 // We cannot redirect the browser to submit the changes via the wiki 1174 // interactively, i.e. showing a preview or diff screen in advance of 1175 // saving the changes. Changes must be posted and clients cannot be 1176 // forced to post in response to a redirect. 1177 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3 1178 { 1179 final String entry = "posting changes to " + api; 1180 logger.fine( entry ); 1181 outB.append( entry ).append( '\n' ); 1182 } 1183 1184 final HttpURLConnection http = (HttpURLConnection)api.toURL().openConnection(); 1185 http.setDoOutput( true ); // automatically does setRequestMethod( "POST" ) 1186 // http.setChunkedStreamingMode( /*chunk length, default*/0 ); 1187 /// fails, giving the API help page (1.16.1) 1188 http.setRequestProperty( "Content-Type", 1189 "application/x-www-form-urlencoded;charset=" + API_POST_CHARSET ); 1190 final CookieManager cookieManager = VOWicket.get().cookieManager(); 1191 URLConnectionX.setRequestCookies( api, http, cookieManager ); // after other req headers 1192 final Spool spool = new Spool1(); 1193 try 1194 { 1195 URLConnectionX.connect( http ); 1196 spool.add( new Hold() { public void release() { http.disconnect(); }} ); 1197 1198 // write 1199 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1200 { 1201 Writer out = new BufferedWriter( new OutputStreamWriter( 1202 http.getOutputStream(), API_POST_CHARSET )); 1203 try 1204 { 1205 out.append( "format=xml&action=edit&title=" ); 1206 out.append( URLEncoder.encode( userDraft.pageName(), 1207 API_POST_CHARSET )); 1208 // must be specified by title, pageids and revids both 1209 // give error "title parameter must be set" 1210 out.append( "&summary=" ); 1211 out.append( URLEncoder.encode( "Patch from " + otherCore.pageName(), 1212 API_POST_CHARSET )); 1213 out.append( "&token=" ); 1214 out.append( URLEncoder.encode( editToken, API_POST_CHARSET )); 1215 out.append( "&text=" ); // remainder of output is encoded: 1216 out = new URLEncodedWriter( API_POST_CHARSET, out ); 1217 1218 appendIfFile( /*voHiBrac*/targetFileSplit[0], nativeCharset, out, 1219 DiffCache.LINE_TRANSFORMER ); 1220 appendIfFile( targetFile/*targetFileSplit[1]*/, nativeCharset, out, 1221 DiffCache.LINE_TRANSFORMER ); 1222 appendIfFile( /*voLoBrac*/targetFileSplit[2], nativeCharset, out ); 1223 } 1224 finally{ out.close(); } 1225 } 1226 1227 // read 1228 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1229 cookieManager.put( api, http.getHeaderFields() ); 1230 final InputStream in = http.getInputStream(); 1231 spool.add( new Hold() 1232 { 1233 public void release() 1234 { 1235 try{ in.close(); } 1236 catch( IOException x ) { throw new RuntimeException( x ); } 1237 } 1238 }); 1239 1240 final XMLStreamReader xml = MediaWiki.newXMLStreamReader( in, spool ); 1241 while( xml.hasNext() ) 1242 { 1243 xml.next(); 1244 if( !xml.isStartElement() ) continue; 1245 1246 if( "edit".equals( xml.getLocalName() )) 1247 { 1248 final String result = xml.getAttributeValue( /*ns*/null, "result" ); 1249 if( !result.equals( "Success" )) 1250 { 1251 outB.append( '\n' ); 1252 outB.append( "post attempt failed with result: " ); 1253 outB.append( result ).append ( '\n' ); 1254 cycle.setResponsePage( new WP_Message( FAILURE_PAGE_TITLE, 1255 outB.toString() ).pre()); 1256 return; 1257 } 1258 } 1259 1260 MediaWiki.test_error( xml ); 1261 } 1262 } 1263 finally{ spool.unwind(); } 1264 } 1265 1266 tmpFileSpool.unwind(); // if no exceptions, otherwise keep files for admin to diagnose 1267 } 1268 catch( IOException|XMLStreamException x ) { throw new RuntimeException( x ); } 1269 1270 // Redirect to wiki history 1271 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1272 try 1273 { 1274 final URI uri = new URI( targetScriptLocation + "/index.php?action=history&curid=" 1275 + userDraft.pageID() ); 1276 logger.finest( "redirecting client to " + uri ); 1277 throw new RedirectException( uri.toASCIIString(), 303 ); // user takes it from there 1278 } 1279 catch( URISyntaxException x ) { throw new RuntimeException( x ); } 1280 } 1281 1282 } 1283 1284 1285 1286 // ==================================================================================== 1287 1288 1289 /** A hunk implemented as an overloaded Wicket model. The members of the model proper 1290 * answer whether to include the hunk in any patch request, while the other members 1291 * record other properties of the hunk. 1292 */ 1293 private static final class Hunk extends Model<Boolean> 1294 { 1295 1296 /** The base-one line number of the succeeding hunk header within the 1297 * <code>diff</code> output. For the final hunk, this is one plus the final line 1298 * number. 1299 */ 1300 int boundaryLine; // final after external init 1301 1302 } 1303 1304 1305} 1306 1307 1308// NOTES 1309// 1310// [1] Convenience redirect 'voterDraft' is yanked as dead code. If it ever needs 1311// salvaging, here it is: 1312// 1313// The name of a position page in the local wiki. The requester is redirected to 1314// the difference between the latest revisions of that position's draft and the 1315// corresponding candidate draft. 1316// 1317// // voterDraft 1318// // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 1319// final String voterDraft = stringNonEmpty( pP, "voterDraft" ); 1320// if( voterDraft != null ) 1321// { 1322// pP.remove( "voterDraft" ); 1323// final MatchResult m = Position.ensurePageName( voterDraft ); 1324// final String pollName = m.group( 3 ); 1325// final String voterName = m.group( 2 ); 1326// final IDPair voter; 1327// try{ voter = IDPair.fromUsername( voterName ); } 1328// catch( AddressException x ) 1329// { 1330// throw new MediaWiki.IDException( "Not a positional page: '" 1331// + voterName + "' is not a mailish username:" + x ); 1332// } 1333// 1334// final Vote vote; 1335// try 1336// { 1337// vote = new Vote( voter, 1338// vsRun.scopePoll().ensurePoll(pollName).voterInputTable() ); 1339// } 1340// catch( ScriptException x ) { throw new RuntimeException( x ); } 1341// 1342// final IDPair candidate = vote.getCandidate(); 1343// if( candidate.equals( IDPair.NOBODY )) 1344// { 1345// VSession.get().info( "no candidate, " + voterName + " has not voted in '" 1346// + pollName + "'" ); 1347// throw new RestartResponseException( new WP_Message() ); 1348// } 1349// 1350// if( voter.equals( candidate )) 1351// { 1352// VSession.get().info( "unable to diff vs. candidate, " + voterName + 1353// " is voting for self" ); 1354// throw new RestartResponseException( new WP_Message() ); 1355// } 1356// 1357// DraftPair pair = DraftPair.newDraftPair( voterDraft, 1358// wiki.positionPageName(candidate.username(),pollName), vS ); 1359// if( !pair.aCore().person().equals( voter )) pair = pair.newReversePair(); 1360// revPut( pP, "a", pair.a() ); 1361// revPut( pP, "aR", pair.aR() ); 1362// revPut( pP, "b", pair.b() ); 1363// revPut( pP, "bR", pair.bR() ); 1364// throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 ); 1365// }