package 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. import java.io.*; import java.net.*; import java.util.*; import javax.mail.internet.*; import javax.servlet.http.*; import org.apache.wicket.AttributeModifier; import org.apache.wicket.markup.html.IHeaderResponse; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.*; import org.apache.wicket.markup.html.link.*; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.validation.*; import votorola.a.*; import votorola.a.voter.*; import votorola.a.web.wic.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.mail.*; import votorola.g.net.*; /** A login page based on the authentication facilities of the {@linkplain * votorola.a.VoteServer#pollwiki() pollwiki}, enabling synchronized login between it and * the Wicket interface. {@linkplain VoteServer#testUseMode() Alias logins} are not * synchronized however, nor, unlike the OpenID page, are they persisted. * * @see WP_WikiLogin.html */ public @ThreadRestricted("wicket") final class WP_WikiLogin extends LoginPage { /** Constructs a WP_WikiLogin that redirects to a newly constructed, bookmarkable, * return page if authentication succeeds. * * @see #respondWithReturnPage(org.apache.wicket.request.cycle.RequestCycle) */ public WP_WikiLogin( final PageParameters pP ) // must be public, per LoginPage { super( pP ); final VRequestCycle cycle = VRequestCycle.get(); final BundleFormatter bun = cycle.bunW(); add( new Label( "title", bun.l( "a.web.wic.authen.WP_Login" ) )); final Form y = new LoginForm(); add( y ); // Email // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - y.add( new Label( "userEmailLabel", bun.l( "a.web.wic.authen.WP_EmailAuthen1.userEmail" ))); final VoteServer vS = VOWicket.get().vsRun().voteServer(); { final TextField emailField = new TextField( "userEmail", new PropertyModel( WP_WikiLogin.this, "userEmailInput" )); invalidStyled( inputLengthConstrained( emailField )); emailField.add( new WicEmailAddressValidator() { // extracting conversions from a validator. Improper, cf. WP_OpenIDLogin public @Override void validate( final IValidatable v ) { claimedUserIAddress = null; // till proven otherwise super.validate( v ); } public @Override void onSuccess( final InternetAddress iAddress ) { claimedUserIAddress = iAddress; InternetAddressX.canonicalize( claimedUserIAddress ); } }); y.add( emailField ); } // Password // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final PollwikiVS wiki = vS.pollwiki(); y.add( new Label( "passwordLabel", bun.l( "a.web.wic.authen.WP_WikiLogin.password" ))); { final TextField field = new PasswordTextField( "password", new PropertyModel( WP_WikiLogin.this, "passwordInput" )); invalidStyled( inputLengthConstrained( field )); field.setRequired( false ); y.add( field ); } y.add( new Label( "userEmailDescription", bun.l( "a.web.wic.authen.WP_WikiLogin.wikiDescription_XHT", wiki.uri().toASCIIString() )).setEscapeModelStrings( false )); // Submit // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - y.add( new Label( "submitPrequalifier", bun.l( "a.web.wic.authen.WP_WikiLogin.submitPrequalifier_XHT", wiki.appendPageSpecifier( "Special:Preferences#mw-prefsection-personal" ).toString() )) .setEscapeModelStrings( false )); { persistent = cycle.vRequest().getCookie(COOKIE_PERSIST_BUTTON) != null; final CheckBox button = new CheckBox( "persist", new PropertyModel( WP_WikiLogin.this, "persistent" )); y.add( button ); final Label label = new Label( "persistLabel", bun.l( "a.web.wic.authen.WP_Login.persist" )); label.add( AttributeModifier.replace( "title", bun.l( "a.web.wic.authen.WP_Login.persistFull" ))); y.add( label ); } { final Button button = new Button( "submit" ); button.add( AttributeModifier.replace( "value", bun.l( "a.web.wic.authen.WP_Login.submit" ))); y.add( button ); } // Alias // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( vS.testUseMode() != VoteServer.TestUseMode.FULL ) y.add( newNullComponent( "test" )); else y.add( new WC_Alias( "test", WP_WikiLogin.this, cycle )); // Feedback // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - y.add( new WC_Feedback( "feedback" )); } // - I - H e a d e r - C o n t r i b u t o r ------------------------------------------ public @Override void renderHead( IHeaderResponse r ) { super.renderHead( r ); if( !getSession().getFeedbackMessages().isEmpty() ) { r.renderOnLoadJavaScript( "location.hash = 'feedback'" ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private String aliasInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() private transient InternetAddress claimedUserIAddress; // from field validator, in canonical form private String passwordInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() private boolean persistent; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() private String userEmailInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() // ==================================================================================== /** A cookie handler that 'gets' cookies from the incoming request (for relaying to * the pollwiki) and (from the pollwiki's response) 'puts' them to the outgoing * response. Effectively the client dialogues directly with the wiki as far as * cookies are concerned. This is only suitable for short dialogues in which a given * "Set-Cookie" header will not be repeated, as otherwise it would be duplicated in * the eventual relay back to client, perhaps out of order. */ private static final class CookieRelayer extends CookieManager { CookieRelayer( final PollwikiVS wiki, final String wikiCookiePrefix, VRequestCycle _cycle ) { super( /*store, default*/null, CookiePolicy.ACCEPT_ALL ); cycle = _cycle; // Prime the store with wiki cookies copied from the client. Let the manager // (base class) maintain them and mimic what the client will do when it // eventually receives the "Set-Cookie" headers relayed by 'put'. final String domain = Net.widestCookieDomain( wiki.uri().getHost() ); if( domain != null ) { final CookieStore store = getCookieStore(); final Cookie[] cookiesToRelay = ((HttpServletRequest) cycle.getRequest().getContainerRequest()).getCookies(); if( cookiesToRelay != null ) for( final Cookie cookieHS: cookiesToRelay ) { final String name = cookieHS.getName(); if( !name.startsWith( wikiCookiePrefix )) return; // only these ones matter final HttpCookie relayedCookie = new HttpCookie( name, cookieHS.getValue() ); relayedCookie.setDomain( domain ); /* set widest possible domain and leave URI null, max chance of getting to wiki */ store.add( /*URI*/null, relayedCookie ); } } else assert false; } private final VRequestCycle cycle; public @Override void put( final URI uri, final Map> responseHeadersFromWiki ) throws IOException { super.put( uri, responseHeadersFromWiki ); final List values = responseHeadersFromWiki.get( "Set-Cookie" ); if( values == null ) return; final HttpServletResponse resHS = (HttpServletResponse) cycle.getResponse().getContainerResponse(); for( final String value: values ) resHS.addHeader( "Set-Cookie", value ); // ... to outgoing client response } } // ==================================================================================== private final class LoginForm extends Form { private LoginForm() { super( "form" ); add( new IDFieldValidator() { List> idFields() { final ArrayList> fields = new ArrayList<>( /*initial capacity*/2 ); fields.add( (TextField)get( "userEmail" )); final TextField field = (TextField) // get( "test.alias" ); /// always gives null for some reason, so: ((WC_Alias)get( "test" )).get( "alias" ); if( field != null ) fields.add( field ); return fields; } }); add( new org.apache.wicket.markup.html.form.validation.AbstractFormValidator() { public FormComponent[] getDependentFormComponents() { return new FormComponent[]{ (TextField)get("userEmail"), (TextField)get("password") }; } public void validate( Form _form ) { TextField field = (TextField)get( "userEmail" ); if( field.getConvertedInput() == null ) return; // no password expected field = (TextField)get( "password" ); if( field.getConvertedInput() != null ) return; // valid field.error( (IValidationError)new ValidationError().setMessage( VRequestCycle.get().bunW().l( "a.web.wic.authen.WP_WikiLogin.password.missing" ))); } }); } protected @Override void onSubmit() { super.onSubmit(); final VRequestCycle cycle = VRequestCycle.get(); WP_OpenIDLogin.persistPersistButton( persistent, cycle.vResponse() ); if( userEmailInput != null ) onSubmitWiki( cycle ); // not claimedUserIAddress, which is not properly nulled by an empty field else onSubmitAlias( cycle ); } private void onSubmitAlias( final VRequestCycle cycle ) { if( aliasInput == null ) throw new IllegalStateException(); setUserInSession( IDPair.fromEmail(WC_Alias.toEmail(aliasInput)), "alias", /*persistent*/false, /*toReplaceSession*/false, cycle ); respondWithReturnPage( cycle ); } private void onSubmitWiki( final VRequestCycle cycle ) { if( claimedUserIAddress == null || passwordInput == null ) throw new IllegalStateException(); final VOWicket app = VOWicket.get(); final PollwikiVS wiki = app.vsRun().voteServer().pollwiki(); final URI scriptURI = wiki.scriptURI(); final URI api; try{ api = new URI( scriptURI.toASCIIString() + "/api.php" ); } catch( URISyntaxException x ) { throw new RuntimeException( x ); } final WikiAuthenticator authenticator = (WikiAuthenticator)app.authenticator(); final CookieHandler cookieHandler = persistent? new CookieRelayer( wiki, authenticator.getCookiePrefix(), cycle ): new CookieManager( /*store, default*/null, CookiePolicy.ACCEPT_ALL ); final IDPair claimedID = IDPair.fromEmail( claimedUserIAddress.getAddress() ); try { final String errorMessage = MediaWiki.login( api, cookieHandler, claimedID.username(), passwordInput ); if( errorMessage != null ) { VSession.get().warn( errorMessage ); return; } final WikiAuthenticator.FailureMessage fM = authenticator.authenticateEmail( claimedID, cookieHandler ); if( fM != null ) { final String fMC = fM.content(); final BundleFormatter bun = cycle.bunW(); final String title = bun.l( "a.web.wic.authen.WP_WikiLogin.mailFail.title" ); final WP_Message messagePage; if( fM.isLocalized() ) messagePage = new WP_Message( title, fMC ); else { messagePage = new WP_Message( title, bun.l(fMC) ); if( "a.web.wic.authen.WP_WikiLogin.mailFailMessage.none".equals(fMC) || "a.web.wic.authen.WP_WikiLogin.mailFailMessage.wrong".equals(fMC) ) { messagePage.addLinks( new ExternalLink( "link", wiki.appendPageSpecifier( "Special:Preferences#mw-prefsection-personal" ).toString(), bun.l( "a.web.wic.authen.WP_WikiLogin.mailFail.link" ))); } } cycle.setResponsePage( messagePage ); return; } } catch( IOException|VotorolaException x ) { throw new RuntimeException( x ); } setUserInSession( claimedID, "pollwiki", persistent, /*toReplaceSession*/false, cycle ); // replacement can mess up history, as for WP_OpenIDLogin (see setUserInSession) respondWithReturnPage( cycle ); } } }