View Javadoc
1   package org.argeo.cms.ui;
2   
3   import static org.argeo.naming.SharedSecret.X_SHARED_SECRET;
4   
5   import java.io.IOException;
6   import java.security.PrivilegedAction;
7   import java.util.HashMap;
8   import java.util.Map;
9   
10  import javax.jcr.Node;
11  import javax.jcr.PathNotFoundException;
12  import javax.jcr.Property;
13  import javax.jcr.Repository;
14  import javax.jcr.RepositoryException;
15  import javax.jcr.Session;
16  import javax.jcr.nodetype.NodeType;
17  import javax.security.auth.Subject;
18  import javax.security.auth.callback.Callback;
19  import javax.security.auth.callback.UnsupportedCallbackException;
20  import javax.security.auth.login.LoginContext;
21  import javax.security.auth.login.LoginException;
22  import javax.servlet.http.HttpServletRequest;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.argeo.api.NodeConstants;
27  import org.argeo.cms.CmsException;
28  import org.argeo.cms.auth.CurrentUser;
29  import org.argeo.cms.auth.HttpRequestCallback;
30  import org.argeo.cms.auth.HttpRequestCallbackHandler;
31  import org.argeo.eclipse.ui.specific.UiContext;
32  import org.argeo.jcr.JcrUtils;
33  import org.argeo.naming.AuthPassword;
34  import org.argeo.naming.SharedSecret;
35  import org.eclipse.rap.rwt.RWT;
36  import org.eclipse.rap.rwt.application.AbstractEntryPoint;
37  import org.eclipse.rap.rwt.client.WebClient;
38  import org.eclipse.rap.rwt.client.service.BrowserNavigation;
39  import org.eclipse.rap.rwt.client.service.BrowserNavigationEvent;
40  import org.eclipse.rap.rwt.client.service.BrowserNavigationListener;
41  import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
42  import org.eclipse.swt.widgets.Composite;
43  import org.eclipse.swt.widgets.Display;
44  import org.eclipse.swt.widgets.Shell;
45  
46  /** Manages history and navigation */
47  public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implements CmsView {
48  	private static final long serialVersionUID = 906558779562569784L;
49  
50  	private final Log log = LogFactory.getLog(AbstractCmsEntryPoint.class);
51  
52  	// private final Subject subject;
53  	private LoginContext loginContext;
54  
55  	private final Repository repository;
56  	private final String workspace;
57  	private final String defaultPath;
58  	private final Map<String, String> factoryProperties;
59  
60  	// Current state
61  	private Session session;
62  	private Node node;
63  	private String nodePath;// useful when changing auth
64  	private String state;
65  	private Throwable exception;
66  
67  	// Client services
68  	private final JavaScriptExecutor jsExecutor;
69  	private final BrowserNavigation browserNavigation;
70  
71  	public AbstractCmsEntryPoint(Repository repository, String workspace, String defaultPath,
72  			Map<String, String> factoryProperties) {
73  		this.repository = repository;
74  		this.workspace = workspace;
75  		this.defaultPath = defaultPath;
76  		this.factoryProperties = new HashMap<String, String>(factoryProperties);
77  		// subject = new Subject();
78  
79  		// Initial login
80  		LoginContext lc;
81  		try {
82  			lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
83  					new HttpRequestCallbackHandler(UiContext.getHttpRequest(), UiContext.getHttpResponse()));
84  			lc.login();
85  		} catch (LoginException e) {
86  			try {
87  				lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS);
88  				lc.login();
89  			} catch (LoginException e1) {
90  				throw new CmsException("Cannot log in as anonymous", e1);
91  			}
92  		}
93  		authChange(lc);
94  
95  		jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
96  		browserNavigation = RWT.getClient().getService(BrowserNavigation.class);
97  		if (browserNavigation != null)
98  			browserNavigation.addBrowserNavigationListener(new CmsNavigationListener());
99  	}
100 
101 	@Override
102 	protected Shell createShell(Display display) {
103 		Shell shell = super.createShell(display);
104 		shell.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_SHELL);
105 		display.disposeExec(new Runnable() {
106 
107 			@Override
108 			public void run() {
109 				if (log.isTraceEnabled())
110 					log.trace("Logging out " + session);
111 				JcrUtils.logoutQuietly(session);
112 			}
113 		});
114 		return shell;
115 	}
116 
117 	@Override
118 	protected final void createContents(final Composite parent) {
119 		UiContext.setData(CmsView.KEY, this);
120 		Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
121 			@Override
122 			public Void run() {
123 				try {
124 					initUi(parent);
125 				} catch (Exception e) {
126 					throw new CmsException("Cannot create entrypoint contents", e);
127 				}
128 				return null;
129 			}
130 		});
131 	}
132 
133 	/** Create UI */
134 	protected abstract void initUi(Composite parent);
135 
136 	/** Recreate UI after navigation or auth change */
137 	protected abstract void refresh();
138 
139 	/**
140 	 * The node to return when no node was found (for authenticated users and
141 	 * anonymous)
142 	 */
143 //	private Node getDefaultNode(Session session) throws RepositoryException {
144 //		if (!session.hasPermission(defaultPath, "read")) {
145 //			String userId = session.getUserID();
146 //			if (userId.equals(NodeConstants.ROLE_ANONYMOUS))
147 //				// TODO throw a special exception
148 //				throw new CmsException("Login required");
149 //			else
150 //				throw new CmsException("Unauthorized");
151 //		}
152 //		return session.getNode(defaultPath);
153 //	}
154 
155 	protected String getBaseTitle() {
156 		return factoryProperties.get(WebClient.PAGE_TITLE);
157 	}
158 
159 	public void navigateTo(String state) {
160 		exception = null;
161 		String title = setState(state);
162 		doRefresh();
163 		if (browserNavigation != null)
164 			browserNavigation.pushState(state, title);
165 	}
166 
167 	// @Override
168 	// public synchronized Subject getSubject() {
169 	// return subject;
170 	// }
171 
172 	// @Override
173 	// public LoginContext getLoginContext() {
174 	// return loginContext;
175 	// }
176 	protected Subject getSubject() {
177 		return loginContext.getSubject();
178 	}
179 
180 	@Override
181 	public boolean isAnonymous() {
182 		return CurrentUser.isAnonymous(getSubject());
183 	}
184 
185 	@Override
186 	public synchronized void logout() {
187 		if (loginContext == null)
188 			throw new CmsException("Login context should not be null");
189 		try {
190 			CurrentUser.logoutCmsSession(loginContext.getSubject());
191 			loginContext.logout();
192 			LoginContext anonymousLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS);
193 			anonymousLc.login();
194 			authChange(anonymousLc);
195 		} catch (LoginException e) {
196 			log.error("Cannot logout", e);
197 		}
198 	}
199 
200 	@Override
201 	public synchronized void authChange(LoginContext lc) {
202 		if (lc == null)
203 			throw new CmsException("Login context cannot be null");
204 		// logout previous login context
205 		if (this.loginContext != null)
206 			try {
207 				this.loginContext.logout();
208 			} catch (LoginException e1) {
209 				log.warn("Could not log out: " + e1);
210 			}
211 		this.loginContext = lc;
212 		Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
213 
214 			@Override
215 			public Void run() {
216 				try {
217 					JcrUtils.logoutQuietly(session);
218 					session = repository.login(workspace);
219 					if (nodePath != null)
220 						try {
221 							node = session.getNode(nodePath);
222 						} catch (PathNotFoundException e) {
223 							navigateTo("~");
224 						}
225 
226 					// refresh UI
227 					doRefresh();
228 				} catch (RepositoryException e) {
229 					throw new CmsException("Cannot perform auth change", e);
230 				}
231 				return null;
232 			}
233 
234 		});
235 	}
236 
237 	@Override
238 	public void exception(final Throwable e) {
239 		AbstractCmsEntryPoint.this.exception = e;
240 		log.error("Unexpected exception in CMS", e);
241 		doRefresh();
242 	}
243 
244 	protected synchronized void doRefresh() {
245 		Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
246 			@Override
247 			public Void run() {
248 				refresh();
249 				return null;
250 			}
251 		});
252 	}
253 
254 	/** Sets the state of the entry point and retrieve the related JCR node. */
255 	protected synchronized String setState(String newState) {
256 		String previousState = this.state;
257 
258 		String newNodePath = null;
259 		String prefix = null;
260 		this.state = newState;
261 		if (newState.equals("~"))
262 			this.state = "";
263 
264 		try {
265 			int firstSlash = state.indexOf('/');
266 			if (firstSlash == 0) {
267 				newNodePath = state;
268 				prefix = "";
269 			} else if (firstSlash > 0) {
270 				prefix = state.substring(0, firstSlash);
271 				newNodePath = state.substring(firstSlash);
272 			} else {
273 				newNodePath = defaultPath;
274 				prefix = state;
275 
276 			}
277 
278 			// auth
279 			int colonIndex = prefix.indexOf('$');
280 			if (colonIndex > 0) {
281 				SharedSecret token = new SharedSecret(new AuthPassword(X_SHARED_SECRET + '$' + prefix)) {
282 
283 					@Override
284 					public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
285 						super.handle(callbacks);
286 						// handle HTTP context
287 						for (Callback callback : callbacks) {
288 							if (callback instanceof HttpRequestCallback) {
289 								((HttpRequestCallback) callback).setRequest(UiContext.getHttpRequest());
290 								((HttpRequestCallback) callback).setResponse(UiContext.getHttpResponse());
291 							}
292 						}
293 					}
294 				};
295 				LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
296 				lc.login();
297 				authChange(lc);// sets the node as well
298 				// } else {
299 				// // TODO check consistency
300 				// }
301 			} else {
302 				Node newNode = null;
303 				if (session.nodeExists(newNodePath))
304 					newNode = session.getNode(newNodePath);
305 				else {
306 //					throw new CmsException("Data " + newNodePath + " does not exist");
307 					newNode = null;
308 				}
309 				setNode(newNode);
310 			}
311 			String title = publishMetaData(getNode());
312 
313 			if (log.isTraceEnabled())
314 				log.trace("node=" + newNodePath + ", state=" + state + " (prefix=" + prefix + ")");
315 
316 			return title;
317 		} catch (Exception e) {
318 			log.error("Cannot set state '" + state + "'", e);
319 			if (state.equals("") || newState.equals("~") || newState.equals(previousState))
320 				return "Unrecoverable exception : " + e.getClass().getSimpleName();
321 			if (previousState.equals(""))
322 				previousState = "~";
323 			navigateTo(previousState);
324 			throw new CmsException("Unexpected issue when accessing #" + newState, e);
325 		}
326 	}
327 
328 	private String publishMetaData(Node node) throws RepositoryException {
329 		// Title
330 		String title;
331 		if (node != null && node.isNodeType(NodeType.MIX_TITLE) && node.hasProperty(Property.JCR_TITLE))
332 			title = node.getProperty(Property.JCR_TITLE).getString() + " - " + getBaseTitle();
333 		else
334 			title = getBaseTitle();
335 
336 		HttpServletRequest request = UiContext.getHttpRequest();
337 		if (request == null)
338 			return null;
339 
340 		StringBuilder js = new StringBuilder();
341 		if (title == null)
342 			title = "";
343 		title = title.replace("'", "\\'");// sanitize
344 		js.append("document.title = '" + title + "';");
345 		jsExecutor.execute(js.toString());
346 		return title;
347 	}
348 
349 	// Simply remove some illegal character
350 	// private String clean(String stringToClean) {
351 	// return stringToClean.replaceAll("'", "").replaceAll("\\n", "")
352 	// .replaceAll("\\t", "");
353 	// }
354 
355 	protected synchronized Node getNode() {
356 		return node;
357 	}
358 
359 	private synchronized void setNode(Node node) throws RepositoryException {
360 		this.node = node;
361 		this.nodePath = node == null ? null : node.getPath();
362 	}
363 
364 	protected String getState() {
365 		return state;
366 	}
367 
368 	protected Throwable getException() {
369 		return exception;
370 	}
371 
372 	protected void resetException() {
373 		exception = null;
374 	}
375 
376 	protected Session getSession() {
377 		return session;
378 	}
379 
380 	private class CmsNavigationListener implements BrowserNavigationListener {
381 		private static final long serialVersionUID = -3591018803430389270L;
382 
383 		@Override
384 		public void navigated(BrowserNavigationEvent event) {
385 			setState(event.getState());
386 			doRefresh();
387 		}
388 	}
389 }