001package votorola.a.web.wic.authen; // Copyright 2012, 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 java.io.*;
004import java.net.*;
005import java.util.*;
006import javax.mail.internet.*;
007import javax.servlet.http.*;
008import org.apache.wicket.AttributeModifier;
009import org.apache.wicket.markup.html.IHeaderResponse;
010import org.apache.wicket.markup.html.basic.Label;
011import org.apache.wicket.markup.html.form.*;
012import org.apache.wicket.markup.html.link.*;
013import org.apache.wicket.model.PropertyModel;
014import org.apache.wicket.request.mapper.parameter.PageParameters;
015import org.apache.wicket.validation.*;
016import votorola.a.*;
017import votorola.a.voter.*;
018import votorola.a.web.wic.*;
019import votorola.g.*;
020import votorola.g.lang.*;
021import votorola.g.locale.*;
022import votorola.g.mail.*;
023import votorola.g.net.*;
024
025
026/** A login page based on the authentication facilities of the {@linkplain
027  * votorola.a.VoteServer#pollwiki() pollwiki}, enabling synchronized login between it and
028  * the Wicket interface.  {@linkplain VoteServer#testUseMode() Alias logins} are not
029  * synchronized however, nor, unlike the OpenID page, are they persisted.
030  *
031  *     @see <a href='../../../../../../../a/web/wic/authen/WP_WikiLogin.html'
032  *                                           target='_top'>WP_WikiLogin.html</a>
033  */
034public @ThreadRestricted("wicket") final class WP_WikiLogin extends LoginPage
035{
036
037
038    /** Constructs a WP_WikiLogin that redirects to a newly constructed, bookmarkable,
039      * return page if authentication succeeds.
040      *
041      *     @see #respondWithReturnPage(org.apache.wicket.request.cycle.RequestCycle)
042      */
043    public WP_WikiLogin( final PageParameters pP ) // must be public, per LoginPage
044    {
045        super( pP );
046        final VRequestCycle cycle = VRequestCycle.get();
047        final BundleFormatter bun = cycle.bunW();
048        add( new Label( "title", bun.l( "a.web.wic.authen.WP_Login" ) ));
049        final Form<Void> y = new LoginForm();
050        add( y );
051
052      // Email
053      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
054        y.add( new Label( "userEmailLabel", bun.l( "a.web.wic.authen.WP_EmailAuthen1.userEmail" )));
055        final VoteServer vS = VOWicket.get().vsRun().voteServer();
056        {
057            final TextField<String> emailField = new TextField<String>( "userEmail",
058              new PropertyModel<String>( WP_WikiLogin.this, "userEmailInput" ));
059            invalidStyled( inputLengthConstrained( emailField ));
060            emailField.add( new WicEmailAddressValidator()
061            {
062                // extracting conversions from a validator.  Improper, cf. WP_OpenIDLogin
063                public @Override void validate( final IValidatable<String> v )
064                {
065                    claimedUserIAddress = null; // till proven otherwise
066                    super.validate( v );
067                }
068                public @Override void onSuccess( final InternetAddress iAddress )
069                {
070                    claimedUserIAddress = iAddress;
071                    InternetAddressX.canonicalize( claimedUserIAddress );
072                }
073            });
074            y.add( emailField );
075        }
076
077      // Password
078      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
079        final PollwikiVS wiki = vS.pollwiki();
080        y.add( new Label( "passwordLabel", bun.l( "a.web.wic.authen.WP_WikiLogin.password" )));
081        {
082            final TextField<String> field = new PasswordTextField( "password",
083              new PropertyModel<String>( WP_WikiLogin.this, "passwordInput" ));
084            invalidStyled( inputLengthConstrained( field ));
085            field.setRequired( false );
086            y.add( field );
087        }
088        y.add( new Label( "userEmailDescription",
089          bun.l( "a.web.wic.authen.WP_WikiLogin.wikiDescription_XHT",
090            wiki.uri().toASCIIString() )).setEscapeModelStrings( false ));
091
092      // Submit
093      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
094        y.add( new Label( "submitPrequalifier",
095          bun.l( "a.web.wic.authen.WP_WikiLogin.submitPrequalifier_XHT",
096            wiki.appendPageSpecifier( "Special:Preferences#mw-prefsection-personal" ).toString() ))
097          .setEscapeModelStrings( false ));
098        {
099            persistent = cycle.vRequest().getCookie(COOKIE_PERSIST_BUTTON) != null;
100            final CheckBox button = new CheckBox( "persist",
101              new PropertyModel<Boolean>( WP_WikiLogin.this, "persistent" ));
102            y.add( button );
103
104            final Label label = new Label( "persistLabel",
105              bun.l( "a.web.wic.authen.WP_Login.persist" ));
106            label.add( AttributeModifier.replace( "title",
107              bun.l( "a.web.wic.authen.WP_Login.persistFull" )));
108            y.add( label );
109        }
110        {
111            final Button button = new Button( "submit" );
112            button.add( AttributeModifier.replace( "value",
113              bun.l( "a.web.wic.authen.WP_Login.submit" )));
114            y.add( button );
115        }
116
117      // Alias
118      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
119        if( vS.testUseMode() != VoteServer.TestUseMode.FULL ) y.add( newNullComponent( "test" ));
120        else y.add( new WC_Alias( "test", WP_WikiLogin.this, cycle ));
121
122      // Feedback
123      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
124        y.add( new WC_Feedback( "feedback" ));
125    }
126
127
128
129   // - I - H e a d e r - C o n t r i b u t o r ------------------------------------------
130
131
132    public @Override void renderHead( IHeaderResponse r )
133    {
134        super.renderHead( r );
135        if( !getSession().getFeedbackMessages().isEmpty() )
136        {
137            r.renderOnLoadJavaScript( "location.hash = 'feedback'" );
138        }
139    }
140
141
142
143//// P r i v a t e ///////////////////////////////////////////////////////////////////////
144
145
146    private String aliasInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
147
148
149
150    private transient InternetAddress claimedUserIAddress; // from field validator, in canonical form
151
152
153
154    private String passwordInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
155
156
157
158    private boolean persistent; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
159
160
161
162    private String userEmailInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
163
164
165
166   // ====================================================================================
167
168
169    /** A cookie handler that 'gets' cookies from the incoming request (for relaying to
170      * the pollwiki) and (from the pollwiki's response) 'puts' them to the outgoing
171      * response.  Effectively the client dialogues directly with the wiki as far as
172      * cookies are concerned.  This is only suitable for short dialogues in which a given
173      * "Set-Cookie" header will not be repeated, as otherwise it would be duplicated in
174      * the eventual relay back to client, perhaps out of order.
175      */
176    private static final class CookieRelayer extends CookieManager
177    {
178
179        CookieRelayer( final PollwikiVS wiki, final String wikiCookiePrefix, VRequestCycle _cycle )
180        {
181            super( /*store, default*/null, CookiePolicy.ACCEPT_ALL );
182            cycle = _cycle;
183
184            // Prime the store with wiki cookies copied from the client.  Let the manager
185            // (base class) maintain them and mimic what the client will do when it
186            // eventually receives the "Set-Cookie" headers relayed by 'put'.
187            final String domain = Net.widestCookieDomain( wiki.uri().getHost() );
188            if( domain != null )
189            {
190                final CookieStore store = getCookieStore();
191                final Cookie[] cookiesToRelay = ((HttpServletRequest)
192                  cycle.getRequest().getContainerRequest()).getCookies();
193                if( cookiesToRelay != null ) for( final Cookie cookieHS: cookiesToRelay )
194                {
195                    final String name = cookieHS.getName();
196                    if( !name.startsWith( wikiCookiePrefix )) return; // only these ones matter
197
198                    final HttpCookie relayedCookie = new HttpCookie( name, cookieHS.getValue() );
199                    relayedCookie.setDomain( domain ); /* set widest possible domain and
200                      leave URI null, max chance of getting to wiki */
201                    store.add( /*URI*/null, relayedCookie );
202                }
203            }
204            else assert false;
205        }
206
207
208        private final VRequestCycle cycle;
209
210
211        public @Override void put( final URI uri,
212          final Map<String,List<String>> responseHeadersFromWiki ) throws IOException
213        {
214            super.put( uri, responseHeadersFromWiki );
215            final List<String> values = responseHeadersFromWiki.get( "Set-Cookie" );
216            if( values == null ) return;
217
218            final HttpServletResponse resHS = (HttpServletResponse)
219              cycle.getResponse().getContainerResponse();
220            for( final String value: values ) resHS.addHeader( "Set-Cookie", value );
221              // ... to outgoing client response
222        }
223
224    }
225
226
227
228   // ====================================================================================
229
230
231    private final class LoginForm extends Form<Void>
232    {
233
234        private LoginForm()
235        {
236            super( "form" );
237            add( new IDFieldValidator()
238            {
239                List<TextField<?>> idFields()
240                {
241                    final ArrayList<TextField<?>> fields = new ArrayList<>( /*initial capacity*/2 );
242                    fields.add( (TextField<?>)get( "userEmail" ));
243                    final TextField<?> field = (TextField<?>)
244                   // get( "test.alias" );
245                   /// always gives null for some reason, so:
246                      ((WC_Alias)get( "test" )).get( "alias" );
247                    if( field != null ) fields.add( field );
248                    return fields;
249                }
250            });
251            add( new org.apache.wicket.markup.html.form.validation.AbstractFormValidator()
252            {
253                public FormComponent<?>[] getDependentFormComponents()
254                {
255                    return new FormComponent<?>[]{ (TextField<?>)get("userEmail"),
256                      (TextField<?>)get("password") };
257                }
258                public void validate( Form<?> _form )
259                {
260                    TextField<?> field = (TextField<?>)get( "userEmail" );
261                    if( field.getConvertedInput() == null ) return; // no password expected
262
263                    field = (TextField<?>)get( "password" );
264                    if( field.getConvertedInput() != null ) return; // valid
265
266                    field.error( (IValidationError)new ValidationError().setMessage(
267                      VRequestCycle.get().bunW().l(
268                        "a.web.wic.authen.WP_WikiLogin.password.missing" )));
269                }
270            });
271        }
272
273
274        protected @Override void onSubmit()
275        {
276            super.onSubmit();
277            final VRequestCycle cycle = VRequestCycle.get();
278            WP_OpenIDLogin.persistPersistButton( persistent, cycle.vResponse() );
279            if( userEmailInput != null ) onSubmitWiki( cycle ); // not claimedUserIAddress, which is not properly nulled by an empty field
280            else onSubmitAlias( cycle );
281        }
282
283
284        private void onSubmitAlias( final VRequestCycle cycle )
285        {
286            if( aliasInput == null ) throw new IllegalStateException();
287
288            setUserInSession( IDPair.fromEmail(WC_Alias.toEmail(aliasInput)), "alias",
289              /*persistent*/false, /*toReplaceSession*/false, cycle );
290            respondWithReturnPage( cycle );
291        }
292
293
294        private void onSubmitWiki( final VRequestCycle cycle )
295        {
296            if( claimedUserIAddress == null || passwordInput == null ) throw new IllegalStateException();
297
298            final VOWicket app = VOWicket.get();
299            final PollwikiVS wiki = app.vsRun().voteServer().pollwiki();
300            final URI scriptURI = wiki.scriptURI();
301            final URI api;
302            try{ api = new URI( scriptURI.toASCIIString() + "/api.php" ); }
303            catch( URISyntaxException x ) { throw new RuntimeException( x ); }
304
305            final WikiAuthenticator authenticator = (WikiAuthenticator)app.authenticator();
306            final CookieHandler cookieHandler = persistent?
307              new CookieRelayer( wiki, authenticator.getCookiePrefix(), cycle ):
308              new CookieManager( /*store, default*/null, CookiePolicy.ACCEPT_ALL );
309            final IDPair claimedID = IDPair.fromEmail( claimedUserIAddress.getAddress() );
310            try
311            {
312                final String errorMessage = MediaWiki.login( api, cookieHandler,
313                  claimedID.username(), passwordInput );
314                if( errorMessage != null )
315                {
316                    VSession.get().warn( errorMessage );
317                    return;
318                }
319
320                final WikiAuthenticator.FailureMessage fM = authenticator.authenticateEmail(
321                  claimedID, cookieHandler );
322                if( fM != null )
323                {
324                    final String fMC = fM.content();
325                    final BundleFormatter bun = cycle.bunW();
326                    final String title = bun.l( "a.web.wic.authen.WP_WikiLogin.mailFail.title" );
327                    final WP_Message messagePage;
328                    if( fM.isLocalized() ) messagePage = new WP_Message( title, fMC );
329                    else
330                    {
331                        messagePage = new WP_Message( title, bun.l(fMC) );
332                        if( "a.web.wic.authen.WP_WikiLogin.mailFailMessage.none".equals(fMC)
333                         || "a.web.wic.authen.WP_WikiLogin.mailFailMessage.wrong".equals(fMC) )
334                        {
335                            messagePage.addLinks( new ExternalLink( "link", wiki.appendPageSpecifier(
336                              "Special:Preferences#mw-prefsection-personal" ).toString(),
337                             bun.l( "a.web.wic.authen.WP_WikiLogin.mailFail.link" )));
338                        }
339                    }
340                    cycle.setResponsePage( messagePage );
341                    return;
342                }
343            }
344            catch( IOException|VotorolaException x ) { throw new RuntimeException( x ); }
345
346            setUserInSession( claimedID, "pollwiki", persistent, /*toReplaceSession*/false, cycle );
347              // replacement can mess up history, as for WP_OpenIDLogin (see setUserInSession)
348            respondWithReturnPage( cycle );
349        }
350
351    }
352
353
354}