Dienstag, 2. Juni 2009

Providing user specific style sheets (using Stripes)

Customers often want to have a web application in their own CI. Stripes doesn't ship with a ready to use solution for this problem - but that would be impossible since every application has its own requirements for such a feature. But Stripes ships with a feature called user friendly urls. IMHO there are a lot of other web frameworks providing this feature so the following solution is not strictly tied the Stripes framework. The main idea behind this is to include standard css files in a common way:
<link rel="stylesheet" type="text/css" href="/css/ci.css">
and override special style definitions with user specific ones:
<link rel="stylesheet" type="text/css" href="/user-styles/css/ci.css">
The folder css is a real existing one. The folder user-styles is an action bean bound to the url /user-styles/ with a default handler method. The part after /user-styles(/css/default.css) is mapped to a property called "requestedFile". The default handler now builds a real path in some way like this: "<userSpecificStyleFolder>/<userId>/requestedFile" Then the default handler loads this file using the real path(java.nio provides a fast way to read files) and streams it out. If all the images are defined in the css files using a relative path, these images will also be requested using the default handler. So even user specific images are supported. If the users are organized in companies a company wide style could be achieved if the userId is replaced with the companyId when building the real path("<userSpecificStyleFolder>/<companyId>/requestedFile"). In Stripes the action bean would look like this:
@StrictBinding
@UrlBinding("/user-styles/{requestedFile}")
public class UserStyleAction implements ActionBean {
 private static final Log log = LogFactory.getLog(UserStyleAction.class);
 static final String FILE_SEPARATOR = System.getProperty("file.separator");
 private ActionBeanContext actionBeanContext;
 private String requestedFile;
 private String basePath;
 private User user;

 public UserStyleAction() {}

 public UserStyleAction(String basePath) {
  this.basePath = basePath;
 }

 @Before(on="serveFile")
 public Resolution loadUserFromSessionAndSendErrorIfNotLoggedIn() {
  user = (User) actionBeanContext.getRequest().getSession().getAttribute(User.class.toString());
  if (user == null)
   return new ErrorResolution(404);
  return null;
 }

 @Before
 public void initBasePath() throws IOException {
  // This is done to make this project run in every eclipse --> use a config
  // file in real world apps
  String realPathOfClass = actionBeanContext.getServletContext().getRealPath("UserStyleAction.class");
  String webAppFolder = realPathOfClass.replace("UserStyleAction.class", "");
  basePath = new File(webAppFolder + FILE_SEPARATOR + "WEB-INF" + FILE_SEPARATOR + "user-styles").getCanonicalPath();
 }

 @DefaultHandler
 public Resolution serveFile() throws IOException {
  try {
   String mimeType = actionBeanContext.getServletContext().getMimeType(requestedFile);
   File file = new File(buildAbsoluteFilePath(basePath, user));
   throwExceptionInCaseOfDirectoryTraversalAttack(basePath, file);
   FileInputStream fileInputStream = new FileInputStream(file);
   StreamingResolution streamingResolution = new StreamingResolution(mimeType, fileInputStream);
   return streamingResolution;
  } catch (Exception e) {
   if (log.isDebugEnabled())
    log.debug("Error reading file " + requestedFile + " requested by user " + user, e);
   return new ErrorResolution(404);
  }
 }

 String buildAbsoluteFilePath(String basePath, User user) {
  return basePath + FILE_SEPARATOR + user.getId() + FILE_SEPARATOR + requestedFile;
 }

 void throwExceptionInCaseOfDirectoryTraversalAttack(String basePath, File file) throws IOException {
  if (!file.getCanonicalPath().startsWith(basePath + FILE_SEPARATOR + user.getId()))
   throw new RuntimeException("Attempt to hack the application");
 }

 @Validate
 public void setRequestedFile(String requestedFile) {
  this.requestedFile = requestedFile;
 }

 @Override
 public ActionBeanContext getContext() {
  return actionBeanContext;
 }

 @Override
 public void setContext(ActionBeanContext context) {
  this.actionBeanContext = context;
 }

}
A full sample is available at the svn repository

Keine Kommentare:

  © Blogger template 'Morning Drink' by Ourblogtemplates.com 2008

Back to TOP