001package votorola.s.wic; // Copyright 2011-2013, Michael Allan. 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 com.google.gwt.regexp.shared.MatchResult; 004import com.sun.jersey.api.uri.*; 005import java.io.*; 006import java.net.*; 007import java.sql.*; 008import java.util.logging.*; import votorola.g.logging.*; 009import javax.servlet.http.*; 010import javax.xml.stream.*; 011import org.apache.wicket.RestartResponseException; 012import org.apache.wicket.markup.html.WebPage; 013import org.apache.wicket.request.http.WebRequest; 014import org.apache.wicket.request.http.WebResponse; 015import org.apache.wicket.request.mapper.parameter.PageParameters; 016import votorola.a.*; 017import votorola.a.count.*; 018import votorola.a.position.*; 019import votorola.a.web.wic.*; 020import votorola.g.*; 021import votorola.g.hold.*; 022import votorola.g.lang.*; 023import votorola.g.web.*; 024import votorola.g.web.gwt.GWTX; 025import votorola.g.web.wic.*; 026import votorola.s.gwt.stage.*; 027import votorola.s.wap.store.*; 028import votorola.s.wic.count.*; 029 030import static votorola.s.wic.WP_Draft.DomainRelation.OTHER; 031import static votorola.s.wic.WP_Draft.DomainRelation.SAME; 032 033 034/** A page that redirects to a specified position draft. It supports 035 * <a href='#devMode'>GWT dev mode</a> and the <a href='#xf'>relaying of state</a> between 036 * Crossforum Theatre pages. An example request is: 037 * 038 * <blockquote><code><a href="http://reluk.ca:8080/v/w/Draft?p=Sys!p!sandbox&u=Frank-FlippityNet" target='_top'>http://reluk.ca:8080/v/w/Draft?p=Sys!p!sandbox&u=Frank-FlippityNet</a></code></blockquote> 039 * 040 * <h3>Query parameters</h3> 041 * 042 * <table class='definition' style='margin-left:1em'> 043 * <tr> 044 * <th class='key'>Key</th> 045 * <th>Value</th> 046 * <th>Default</th> 047 * </tr> 048 * <tr><td class='key'>cP</td> 049 * 050 * <td>The name of the person from whose viewpoint the draft is sought.</td> 051 * 052 * <td>Null, optional item.</td> 053 * 054 * </tr> 055 * <tr><td class='key'>p</td> 056 * 057 * <td>The {@linkplain votorola.a.count.Poll#name() poll name}. Any slash 058 * characters (/) may be encoded as exclamation marks (!).</td> 059 * 060 * <td>Null, resulting in a 303 (see other) redirect that fills in the name of 061 * the {@linkplain votorola.a.count.Poll#TEST_POLL_NAME test poll}.</td> 062 * 063 * </tr> 064 * <tr><td class='key'>u</td> 065 * 066 * <td>The person's {@linkplain votorola.a.voter.IDPair#username() 067 * username}.</td> 068 * 069 * <td>None, a value is required.</td> 070 * 071 * </tr> 072 * </table> 073 * 074 * @see <a href='../../../../../a/position/WP_Draft.html' target='_top'>WP_Draft.html</a> 075 */ 076 @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent 077public final class WP_Draft extends WebPage 078{ 079 080 081 /** Constructs a WP_Draft. 082 */ 083 public WP_Draft( final PageParameters pP ) throws IOException // bookmarkable page iff constructor public & (default|PageParameter) 084 { 085 super( pP ); 086 final VRequestCycle cycle = VRequestCycle.get(); 087 final String pollName = Poll.U.toName( WP_Poll.maybeRedirect_P( WP_Draft.class, pP, 088 cycle )); 089 final String personName = PageParametersX.getStringRequired( pP, "u" ); 090 final String contextPersonName = PageParametersX.getStringNonEmpty( pP, "cP" ); 091 092 // Either redirect to the existing position draft ... 093 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 094 final String coreLoc = maybeRedirectToDraft( personName, pollName, contextPersonName, 095 VOWicket.get(), cycle ); 096 097 // ... or to the core page, whether it exists or not. 098 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 099 throw new RedirectException( coreLoc, 303 ); 100 } 101 102 103 104 // ------------------------------------------------------------------------------------ 105 106 107 /** Redirects to the position draft if it exists, otherwise returns the URL of the 108 * position core in the local pollwiki, which may or may not exist. 109 * <a id='devMode' href='https://developers.google.com/web-toolkit/doc/latest/DevGuideCompilingAndDebugging#DevGuideDevMode' target='_top'>GWT dev mode</a> 110 * is supported during redirection. If the referrer 111 * URL includes a <code>gwt.codesvr</code> query parameter, then that parameter is 112 * replicated in the redirection. (Whether this works for dragged links depends on 113 * the browser; Firefox 9 does not set the "referer" (sic) header for dragged links, 114 * so it fails there.) <span id='xf'>{@linkplain ReferrerRelayer Relaying of stage 115 * state}</span> between Crossforum Theatre pages is also supported during 116 * redirection. 117 * 118 * @param contextPersonName the name of the person from whose viewpoint the draft 119 * is sought (context person), or null if there is no context person. 120 */ 121 static String maybeRedirectToDraft( final String personName, final String pollName, 122 final String contextPersonName, final VOWicket app, final VRequestCycle cycle ) 123 throws IOException 124 { 125 final VoteServer.Run vsRun = app.vsRun(); 126 final PollwikiVS wiki = vsRun.voteServer().pollwiki(); 127 final String coreName = wiki.positionPageName( personName, pollName ); 128 129 // Query pollwiki for core position page, if any. 130 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 131 final StringBuilder qB = new StringBuilder(); 132 qB.append( wiki.scriptURI() ); 133 qB.append( "/api.php?format=xml&action=query&titles=" ); 134 qB.append( UriComponent.encode( coreName, UriComponent.Type.QUERY_PARAM )); 135 qB.append( '&' ).append( CoreRevision.U.read_QUERY ); 136 final URL queryURL = new URL( qB.toString() ); 137 logger.fine( "querying pollwiki for local draft or pointer: " + queryURL ); 138 final Spool spool = new Spool1(); 139 CoreRevision core; 140 try 141 { 142 final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); 143 core = CoreRevision.U.read( xml, wiki, ComponentPipeRevisionL.class, contextPersonName, 144 /*countSource*/contextPersonName == null? null: new CountSource1(vsRun) ); 145 } 146 catch( final MediaWiki.NoSuchPage x ) { core = null; } 147 catch( final IOException x ) 148 { 149 if( !(x instanceof UserInformative )) throw x; 150 151 VSession.get().error( ThrowableX.toStringExpanded( x )); 152 throw new RestartResponseException( new WP_Message() ); 153 } 154 catch( final XMLStreamException x ) { throw new IOException( x ); } 155 finally{ spool.unwind(); } 156 157 // Resolve draft location (destination) and return if no actual draft exists there. 158 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 159 final WebRequest reqW = cycle.vRequest(); 160 final HttpServletRequest reqHS = (HttpServletRequest)reqW.getContainerRequest(); 161 final String referrerOrNull = reqHS.getHeader( "referer" ); // sic 162 final String codesvrLoc; 163 if( referrerOrNull == null ) codesvrLoc = null; 164 else 165 { 166 final MatchResult m = GWTX.DEV_MODE_LOCATION_PATTERN.exec( referrerOrNull ); 167 codesvrLoc = m == null? null: m.getGroup(2); 168 } 169 final String destination = DraftRevision.U.resolveLocation( wiki, coreName, core, 170 codesvrLoc ); 171 if( core == null ) return destination; 172 173 // Store state for relay. cf. s.gwt.stage.ReferrerRelayer 174 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 175 final WebResponse resW = cycle.vResponse(); 176 final String apparentHostName; 177 { 178 // apparentHostName = vS.serverName(); 179 //// not always fully qualified 180 // apparentHostName = reqHS.getLocalName(); 181 //// Yields IP address if request is targeting root domain (http://reluk.ca/PATH, 182 //// Tomcat 6). And anyway, for cookie work we want the name seen by the client: 183 apparentHostName = reqHS.getServerName(); 184 } 185 final String cookieDomain = ReferrerRelayer.cookieDomain( apparentHostName ); 186 final DomainRelation referrerRelation = domainRelation( cookieDomain, referrerOrNull ); 187 final DomainRelation destinationRelation = domainRelation( cookieDomain, destination ); 188 store: if( destinationRelation == SAME ) // then use cookie 189 { 190 CookieX cookie = new CookieX( ReferrerRelayer.KEY_HREF, destination, /*toEncode*/true ); 191 configureReferrerRelay( cookie, cookieDomain ); 192 WebResponseX.addCookie( resW, cookie ); 193 if( referrerRelation != SAME ) // translate state from WAP to cookie store 194 { 195 final StoreTable table = app.scopeDraft().storeTable; 196 final String client = StoreWAP.clientSignature( reqHS ); 197 final StringBuilder locB = HTTPServletRequestX.getRequestURLFull( reqHS ); 198 try 199 { 200 if( !table.delete( client, ReferrerRelayer.KEY_HREF, locB.toString() )) 201 { 202 // state apparently not stored by referrer to *this* page 203 break store; 204 } 205 206 final String pageJSON = table.get( client, ReferrerRelayer.KEY ); 207 if( pageJSON == null ) break store; // malformed storage, nothing to translate 208 209 cookie = new CookieX( ReferrerRelayer.KEY, pageJSON, /*toEncode*/true ); 210 configureReferrerRelay( cookie, cookieDomain ); 211 WebResponseX.addCookie( resW, cookie ); 212 // table.delete( client, ReferrerRelayer.KEY ); 213 //// No gain for the pain; next overwrite/upsert actually faster if row exists. 214 // And deletion of the HREF has effectively neutralized it meantime. 215 } 216 catch( SQLException x ) { throw new RuntimeException( x ); } 217 } 218 } 219 else // destination is OTHER domain or UNKNOWN relation, so use WAP 220 { 221 final StoreTable table = app.scopeDraft().storeTable; 222 final String client = StoreWAP.clientSignature( reqHS ); 223 try 224 { 225 table.put( client, ReferrerRelayer.KEY_HREF, destination ); 226 if( referrerRelation != OTHER ) // translate state from cookie to WAP store 227 { 228 Cookie cookie = reqW.getCookie( ReferrerRelayer.KEY_HREF ); 229 if( cookie == null ) break store; 230 231 final String originalDestination = CookieX.decodedValue( cookie.getValue() ); 232 final StringBuilder locB = HTTPServletRequestX.getRequestURLFull( reqHS ); 233 if( !originalDestination.contentEquals( locB )) break store; 234 // state apparently not stored by referrer to *this* page 235 236 configureReferrerRelay( cookie, cookieDomain ); 237 WebResponseX.clearCookie( resW, cookie ); 238 cookie = reqW.getCookie( ReferrerRelayer.KEY ); 239 if( cookie == null ) break store; // malformed storage, nothing to translate 240 241 table.put( client, ReferrerRelayer.KEY, 242 CookieX.decodedValue(cookie.getValue()) ); 243 configureReferrerRelay( cookie, cookieDomain ); 244 WebResponseX.clearCookie( resW, cookie ); // lighten future requests 245 } 246 } 247 catch( SQLException x ) { throw new RuntimeException( x ); } 248 } 249 250 // Redirect to draft location. 251 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 252 throw new RedirectException( destination, 303 ); 253 } 254 255 256 257 // ==================================================================================== 258 259 260 /** Application scope for instances of WP_Draft. 261 * 262 * @see VOWicket#scopeDraft() 263 */ 264 public static @ThreadSafe final class ApplicationScope 265 { 266 267 /** Constructs an ApplicationScope. 268 */ 269 public ApplicationScope( final VOWicket app ) 270 { 271 try { storeTable = new StoreTable( app ); } 272 catch( SQLException x ) { throw new RuntimeException( x ); } 273 } 274 275 276 private final StoreTable storeTable; 277 278 } 279 280 281 282 // ==================================================================================== 283 284 285 /** A relation between two domains. 286 */ 287 static enum DomainRelation { OTHER, SAME, UNKNOWN } 288 // non-private only because 'import static' fails otherwise 289 290 291 292//// P r i v a t e /////////////////////////////////////////////////////////////////////// 293 294 295 private static void configureReferrerRelay( final Cookie cookie, final String domain ) 296 { 297 cookie.setDomain( domain ); 298 cookie.setPath( ReferrerRelayer.COOKIE_PATH ); 299 cookie.setSecure( ReferrerRelayer.COOKIE_SECURE ); 300 } 301 302 303 304 /** Determines the domain relation between this page and another. 305 * 306 * @param cookieDomain the cookie domain for this page. 307 * @param otherLoc the URL of the other page, which may be null if unknown. 308 */ 309 private static DomainRelation domainRelation( final String cookieDomain, final String otherLoc ) 310 { 311 final DomainRelation relation; 312 if( cookieDomain == null ) relation = DomainRelation.UNKNOWN; 313 else 314 { 315 try 316 { 317 if( otherLoc == null ) relation = DomainRelation.UNKNOWN; 318 else 319 { 320 final String cookieDomainOther = ReferrerRelayer.cookieDomain( 321 new URI(otherLoc).getHost() ); 322 if( cookieDomain.equals( cookieDomainOther )) relation = SAME; 323 else relation = OTHER; 324 } 325 } 326 catch( URISyntaxException x ) { throw new RuntimeException( x ); } 327 } 328 return relation; 329 } 330 331 332 333 private static final Logger logger = LoggerX.i( WP_Draft.class ); 334 335 336}