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&amp;u=Frank-FlippityNet" target='_top'>http://reluk.ca:8080/v/w/Draft?p=Sys!p!sandbox&amp;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}