package 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. import com.google.gwt.regexp.shared.MatchResult; import com.sun.jersey.api.uri.*; import java.io.*; import java.net.*; import java.sql.*; import java.util.logging.*; import votorola.g.logging.*; import javax.servlet.http.*; import javax.xml.stream.*; import org.apache.wicket.RestartResponseException; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.request.http.WebRequest; import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.request.mapper.parameter.PageParameters; import votorola.a.*; import votorola.a.count.*; import votorola.a.position.*; import votorola.a.web.wic.*; import votorola.g.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.web.*; import votorola.g.web.gwt.GWTX; import votorola.g.web.wic.*; import votorola.s.gwt.stage.*; import votorola.s.wap.store.*; import votorola.s.wic.count.*; import static votorola.s.wic.WP_Draft.DomainRelation.OTHER; import static votorola.s.wic.WP_Draft.DomainRelation.SAME; /** A page that redirects to a specified position draft. It supports * GWT dev mode and the relaying of state between * Crossforum Theatre pages. An example request is: * *
http://reluk.ca:8080/v/w/Draft?p=Sys!p!sandbox&u=Frank-FlippityNet
* *

Query parameters

* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
KeyValueDefault
cPThe name of the person from whose viewpoint the draft is sought.Null, optional item.
pThe {@linkplain votorola.a.count.Poll#name() poll name}. Any slash * characters (/) may be encoded as exclamation marks (!).Null, resulting in a 303 (see other) redirect that fills in the name of * the {@linkplain votorola.a.count.Poll#TEST_POLL_NAME test poll}.
uThe person's {@linkplain votorola.a.voter.IDPair#username() * username}.None, a value is required.
* * @see WP_Draft.html */ @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent public final class WP_Draft extends WebPage { /** Constructs a WP_Draft. */ public WP_Draft( final PageParameters pP ) throws IOException // bookmarkable page iff constructor public & (default|PageParameter) { super( pP ); final VRequestCycle cycle = VRequestCycle.get(); final String pollName = Poll.U.toName( WP_Poll.maybeRedirect_P( WP_Draft.class, pP, cycle )); final String personName = PageParametersX.getStringRequired( pP, "u" ); final String contextPersonName = PageParametersX.getStringNonEmpty( pP, "cP" ); // Either redirect to the existing position draft ... // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final String coreLoc = maybeRedirectToDraft( personName, pollName, contextPersonName, VOWicket.get(), cycle ); // ... or to the core page, whether it exists or not. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - throw new RedirectException( coreLoc, 303 ); } // ------------------------------------------------------------------------------------ /** Redirects to the position draft if it exists, otherwise returns the URL of the * position core in the local pollwiki, which may or may not exist. * GWT dev mode * is supported during redirection. If the referrer * URL includes a gwt.codesvr query parameter, then that parameter is * replicated in the redirection. (Whether this works for dragged links depends on * the browser; Firefox 9 does not set the "referer" (sic) header for dragged links, * so it fails there.) {@linkplain ReferrerRelayer Relaying of stage * state} between Crossforum Theatre pages is also supported during * redirection. * * @param contextPersonName the name of the person from whose viewpoint the draft * is sought (context person), or null if there is no context person. */ static String maybeRedirectToDraft( final String personName, final String pollName, final String contextPersonName, final VOWicket app, final VRequestCycle cycle ) throws IOException { final VoteServer.Run vsRun = app.vsRun(); final PollwikiVS wiki = vsRun.voteServer().pollwiki(); final String coreName = wiki.positionPageName( personName, pollName ); // Query pollwiki for core position page, if any. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final StringBuilder qB = new StringBuilder(); qB.append( wiki.scriptURI() ); qB.append( "/api.php?format=xml&action=query&titles=" ); qB.append( UriComponent.encode( coreName, UriComponent.Type.QUERY_PARAM )); qB.append( '&' ).append( CoreRevision.U.read_QUERY ); final URL queryURL = new URL( qB.toString() ); logger.fine( "querying pollwiki for local draft or pointer: " + queryURL ); final Spool spool = new Spool1(); CoreRevision core; try { final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); core = CoreRevision.U.read( xml, wiki, ComponentPipeRevisionL.class, contextPersonName, /*countSource*/contextPersonName == null? null: new CountSource1(vsRun) ); } catch( final MediaWiki.NoSuchPage x ) { core = null; } catch( final IOException x ) { if( !(x instanceof UserInformative )) throw x; VSession.get().error( ThrowableX.toStringExpanded( x )); throw new RestartResponseException( new WP_Message() ); } catch( final XMLStreamException x ) { throw new IOException( x ); } finally{ spool.unwind(); } // Resolve draft location (destination) and return if no actual draft exists there. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final WebRequest reqW = cycle.vRequest(); final HttpServletRequest reqHS = (HttpServletRequest)reqW.getContainerRequest(); final String referrerOrNull = reqHS.getHeader( "referer" ); // sic final String codesvrLoc; if( referrerOrNull == null ) codesvrLoc = null; else { final MatchResult m = GWTX.DEV_MODE_LOCATION_PATTERN.exec( referrerOrNull ); codesvrLoc = m == null? null: m.getGroup(2); } final String destination = DraftRevision.U.resolveLocation( wiki, coreName, core, codesvrLoc ); if( core == null ) return destination; // Store state for relay. cf. s.gwt.stage.ReferrerRelayer // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final WebResponse resW = cycle.vResponse(); final String apparentHostName; { // apparentHostName = vS.serverName(); //// not always fully qualified // apparentHostName = reqHS.getLocalName(); //// Yields IP address if request is targeting root domain (http://reluk.ca/PATH, //// Tomcat 6). And anyway, for cookie work we want the name seen by the client: apparentHostName = reqHS.getServerName(); } final String cookieDomain = ReferrerRelayer.cookieDomain( apparentHostName ); final DomainRelation referrerRelation = domainRelation( cookieDomain, referrerOrNull ); final DomainRelation destinationRelation = domainRelation( cookieDomain, destination ); store: if( destinationRelation == SAME ) // then use cookie { CookieX cookie = new CookieX( ReferrerRelayer.KEY_HREF, destination, /*toEncode*/true ); configureReferrerRelay( cookie, cookieDomain ); WebResponseX.addCookie( resW, cookie ); if( referrerRelation != SAME ) // translate state from WAP to cookie store { final StoreTable table = app.scopeDraft().storeTable; final String client = StoreWAP.clientSignature( reqHS ); final StringBuilder locB = HTTPServletRequestX.getRequestURLFull( reqHS ); try { if( !table.delete( client, ReferrerRelayer.KEY_HREF, locB.toString() )) { // state apparently not stored by referrer to *this* page break store; } final String pageJSON = table.get( client, ReferrerRelayer.KEY ); if( pageJSON == null ) break store; // malformed storage, nothing to translate cookie = new CookieX( ReferrerRelayer.KEY, pageJSON, /*toEncode*/true ); configureReferrerRelay( cookie, cookieDomain ); WebResponseX.addCookie( resW, cookie ); // table.delete( client, ReferrerRelayer.KEY ); //// No gain for the pain; next overwrite/upsert actually faster if row exists. // And deletion of the HREF has effectively neutralized it meantime. } catch( SQLException x ) { throw new RuntimeException( x ); } } } else // destination is OTHER domain or UNKNOWN relation, so use WAP { final StoreTable table = app.scopeDraft().storeTable; final String client = StoreWAP.clientSignature( reqHS ); try { table.put( client, ReferrerRelayer.KEY_HREF, destination ); if( referrerRelation != OTHER ) // translate state from cookie to WAP store { Cookie cookie = reqW.getCookie( ReferrerRelayer.KEY_HREF ); if( cookie == null ) break store; final String originalDestination = CookieX.decodedValue( cookie.getValue() ); final StringBuilder locB = HTTPServletRequestX.getRequestURLFull( reqHS ); if( !originalDestination.contentEquals( locB )) break store; // state apparently not stored by referrer to *this* page configureReferrerRelay( cookie, cookieDomain ); WebResponseX.clearCookie( resW, cookie ); cookie = reqW.getCookie( ReferrerRelayer.KEY ); if( cookie == null ) break store; // malformed storage, nothing to translate table.put( client, ReferrerRelayer.KEY, CookieX.decodedValue(cookie.getValue()) ); configureReferrerRelay( cookie, cookieDomain ); WebResponseX.clearCookie( resW, cookie ); // lighten future requests } } catch( SQLException x ) { throw new RuntimeException( x ); } } // Redirect to draft location. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - throw new RedirectException( destination, 303 ); } // ==================================================================================== /** Application scope for instances of WP_Draft. * * @see VOWicket#scopeDraft() */ public static @ThreadSafe final class ApplicationScope { /** Constructs an ApplicationScope. */ public ApplicationScope( final VOWicket app ) { try { storeTable = new StoreTable( app ); } catch( SQLException x ) { throw new RuntimeException( x ); } } private final StoreTable storeTable; } // ==================================================================================== /** A relation between two domains. */ static enum DomainRelation { OTHER, SAME, UNKNOWN } // non-private only because 'import static' fails otherwise //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static void configureReferrerRelay( final Cookie cookie, final String domain ) { cookie.setDomain( domain ); cookie.setPath( ReferrerRelayer.COOKIE_PATH ); cookie.setSecure( ReferrerRelayer.COOKIE_SECURE ); } /** Determines the domain relation between this page and another. * * @param cookieDomain the cookie domain for this page. * @param otherLoc the URL of the other page, which may be null if unknown. */ private static DomainRelation domainRelation( final String cookieDomain, final String otherLoc ) { final DomainRelation relation; if( cookieDomain == null ) relation = DomainRelation.UNKNOWN; else { try { if( otherLoc == null ) relation = DomainRelation.UNKNOWN; else { final String cookieDomainOther = ReferrerRelayer.cookieDomain( new URI(otherLoc).getHost() ); if( cookieDomain.equals( cookieDomainOther )) relation = SAME; else relation = OTHER; } } catch( URISyntaxException x ) { throw new RuntimeException( x ); } } return relation; } private static final Logger logger = LoggerX.i( WP_Draft.class ); }