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}