support for CAPTCHAs
[mir.git] / source / mircoders / localizer / basic / MirBasicPostingSessionHandler.java
index ed0a12d..7483a78 100755 (executable)
  */
 package mircoders.localizer.basic;
 
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.Vector;
-
 import mir.config.MirPropertiesConfiguration;
 import mir.log.LoggerWrapper;
-import mir.session.Request;
-import mir.session.Response;
-import mir.session.Session;
-import mir.session.SessionExc;
-import mir.session.SessionFailure;
-import mir.session.SessionHandler;
-import mir.session.UploadedFile;
-import mir.session.ValidationError;
-import mir.session.ValidationHelper;
-import mir.storage.StorageObject;
-import mir.util.ExceptionFunctions;
+import mir.session.*;
+import mir.storage.Database;
+import mir.util.ExceptionRoutines;
+import mir.util.FileRoutines;
+import mir.util.IORoutines;
 import mircoders.global.MirGlobal;
-import mircoders.module.ModuleMediaType;
+import mircoders.media.UnsupportedMediaTypeExc;
+import mircoders.localizer.MirOpenPostingLocalizer;
+
+import java.io.*;
+import java.util.*;
 
 /**
- *
- * <p>Title: Experimental session handler for comment postings </p>
- * <p>Description: </p>
- * <p>Copyright: Copyright (c) 2003</p>
- * <p>Company: </p>
- * @author not attributable
- * @version 1.0
+ * Extensible handler for open postings.
+ * Behaviour can be altered by overriding methods.
  */
-
 public abstract class MirBasicPostingSessionHandler implements SessionHandler {
-  protected LoggerWrapper logger;
-  protected MirPropertiesConfiguration configuration;
+  protected static LoggerWrapper logger = new LoggerWrapper("Localizer.OpenPosting");
+  protected MirPropertiesConfiguration configuration = MirPropertiesConfiguration.instance();
+
+  /** Previously uploaded files */
+  protected final List attachments = new ArrayList();
+  /** counter to generate unique field names for uploaded files */
+  protected int uploadedFileIndex = 0;
 
   private String normalResponseGenerator;
   private String dupeResponseGenerator;
   private String unsupportedMediaTypeResponseGenerator;
   private String finalResponseGenerator;
 
+  private boolean persistentUploadedFiles;
 
-  public MirBasicPostingSessionHandler() {
-    logger = new LoggerWrapper("Localizer.OpenPosting");
-    try {
-      configuration = MirPropertiesConfiguration.instance();
-    }
-    catch (Throwable t) {
-      logger.fatal("Cannot load configuration: " + t.toString());
-
-      throw new RuntimeException("Cannot load configuration: " + t.toString());
-    }
+  public MirBasicPostingSessionHandler(boolean aPersistentUploadedFiles) {
+    persistentUploadedFiles = aPersistentUploadedFiles;
   }
 
   protected void setNormalResponseGenerator(String aGenerator) {
     normalResponseGenerator = aGenerator;
   }
 
-  protected void setResponseGenerators(String aNormalResponseGenerator, String aDupeResponseGenerator,
-        String anUnsupportedMediaTypeResponseGenerator, String aFinalResponseGenerator) {
+  protected void setResponseGenerators(String aNormalResponseGenerator,
+        String aDupeResponseGenerator, String anUnsupportedMediaTypeResponseGenerator,
+        String aFinalResponseGenerator) {
     setNormalResponseGenerator(aNormalResponseGenerator);
     dupeResponseGenerator = aDupeResponseGenerator;
     unsupportedMediaTypeResponseGenerator = anUnsupportedMediaTypeResponseGenerator;
@@ -110,7 +94,7 @@ public abstract class MirBasicPostingSessionHandler implements SessionHandler {
         subsequentRequest(aRequest, aSession, aResponse);
       }
     }
-  };
+  }
 
   protected void initialRequest(Request aRequest, Session aSession, Response aResponse) throws SessionExc, SessionFailure {
     initializeSession(aRequest, aSession);
@@ -118,11 +102,32 @@ public abstract class MirBasicPostingSessionHandler implements SessionHandler {
     makeInitialResponse(aRequest, aSession, aResponse);
   }
 
+  protected void processAttachments(Request aRequest, Session aSession, Response aResponse) {
+    Iterator i = attachments.iterator();
+    while (i.hasNext()) {
+      Attachment attachment = (Attachment) i.next();
+      try{
+        processAttachment(aRequest, aSession, attachment);
+      }
+      catch (Throwable t) {
+        try {
+          processAttachmentError(aRequest, aSession, attachment, t);
+        }
+        catch (Throwable u) {
+          logger.error("Error while processing attachment error", u);
+        }
+        logger.error("Error while processing attachment", t);
+      }
+    }
+  }
+
   public void subsequentRequest(Request aRequest, Session aSession, Response aResponse) throws SessionExc, SessionFailure {
     try {
-
       try {
-        List validationErrors = new Vector();
+        List validationErrors = new ArrayList();
+
+        preprocessPreviousAttachments(aRequest, aSession);
+        preProcessNewAttachments(aRequest, aSession);
 
         if (!shouldProcessRequest(aRequest, aSession, validationErrors)) {
           initializeResponseData(aRequest, aSession, aResponse);
@@ -130,10 +135,8 @@ public abstract class MirBasicPostingSessionHandler implements SessionHandler {
         }
         else {
           preProcessRequest(aRequest, aSession);
-          Iterator i = aRequest.getUploadedFiles().iterator();
-          while (i.hasNext()) {
-            processUploadedFile(aRequest, aSession, (UploadedFile) i.next());
-          }
+
+          processAttachments(aRequest, aSession, aResponse);
           postProcessRequest(aRequest, aSession);
           initializeResponseData(aRequest, aSession, aResponse);
           makeFinalResponse(aRequest, aSession, aResponse);
@@ -153,148 +156,296 @@ public abstract class MirBasicPostingSessionHandler implements SessionHandler {
     }
   }
 
+  /**
+   * Initializes a session.
+   * This may happen in the case of a new session being initiated, but also
+   * when an older session gets re-initiated after a session timeout.
+   */
   protected void initializeSession(Request aRequest, Session aSession) throws SessionExc, SessionFailure {
-    if (MirGlobal.abuse().getOpenPostingPassword()) {
-      String password = (String) aSession.getAttribute("password");
-      if (password==null) {
-        password = generateOnetimePassword();
-        aSession.setAttribute("password", password);
-      }
-    }
-    else {
-      aSession.deleteAttribute("password");
-    }
-
-    logger.debug("referrer = " + aRequest.getHeader("Referer"));
-
     aSession.setAttribute("referer", aRequest.getHeader("Referer"));
-    aSession.setAttribute("nrmediaitems",
-        new Integer(configuration.getInt("ServletModule.OpenIndy.DefaultMediaUploadItems")));
   }
 
+  /**
+   * Called every time a response is being prepared.
+   * This may be at the initial response, or on a subsequent one.
+   */
   protected void initializeResponseData(Request aRequest, Session aSession, Response aResponse) throws SessionExc, SessionFailure {
-    int nrMediaItems = ((Integer) aSession.getAttribute("nrmediaitems")).intValue();
-    List mediaItems = new Vector();
-    int i=0;
+    int nrMediaItems = configuration.getInt("ServletModule.OpenIndy.DefaultMediaUploadItems", 5);
 
-    while (i<nrMediaItems) {
-      i++;
+    if (aSession.getAttribute("nrmediaitems")!=null) {
+      nrMediaItems = ((Integer) aSession.getAttribute("nrmediaitems")).intValue();
+    }
+
+    try {
+      nrMediaItems = Math.min(configuration.getInt("ServletModule.OpenIndy.MaxMediaUploadItems"), Integer.parseInt(aRequest.getParameter("nrmediaitems")));
+    }
+    catch (Throwable t) {
+      logger.warn("Error while retrieving configuration setting " +
+              "ServletModule.OpenIndy.MaxMediaUploadItems", t);
+    }
+
+    aSession.setAttribute("nrmediaitems", new Integer(nrMediaItems));
+
+    List mediaItems = new ArrayList();
+    for (int i=1; i<=nrMediaItems; i++) {
       mediaItems.add(new Integer(i));
     }
 
     aResponse.setResponseValue("nrmediaitems", new Integer(nrMediaItems));
     aResponse.setResponseValue("mediaitems", mediaItems);
-    aResponse.setResponseValue("password", aSession.getAttribute("password"));
+
+    if (MirGlobal.abuse().getRequireCaptcha()) {
+      aResponse.setResponseValue("password", Boolean.TRUE);
+    }
     aResponse.setResponseValue("referer", aSession.getAttribute("referer"));
     aResponse.setResponseValue("errors", null);
+
+    if (configuration.getBoolean("Localizer.OpenSession.AllowFTPUploads", false)) {
+      aResponse.setResponseValue("ftpfiles",
+          FileRoutines.getDirectoryContentsAsList(configuration.getFile("Localizer.OpenSession.FTPDirectory"),
+              new FilenameFilter() {
+                public boolean accept(File aDir, String aName) {
+                  return !(new File(aDir, aName).isDirectory());
+                }
+          }));
+    }
+    else {
+      aResponse.setResponseValue("ftpfiles", null);
+    }
+
+    initializeAttachmentResponseData(aRequest, aSession, aResponse);
+  }
+
+  /**
+   * Process possible changes to previously uploaded files
+   */
+  protected void initializeAttachmentResponseData(Request aRequest, Session aSession, Response aResponse) throws SessionExc, SessionFailure {
+    List result = new ArrayList();
+    if (persistentUploadedFiles) {
+      Iterator i = attachments.iterator();
+      while (i.hasNext()) {
+        Attachment attachment = (Attachment) i.next();
+        Map attachmentData = new HashMap();
+        attachmentData.putAll(attachment.getAllAttributes());
+        attachmentData.put("fieldname", attachment.getFieldName());
+        attachmentData.put("filename", attachment.getFileName());
+        result.add(attachmentData);
+      }
+    }
+
+    aResponse.setResponseValue("attachments", result);
+  }
+
+  /**
+   * Process possible changes to previously uploaded files
+   */
+  protected void preprocessPreviousAttachments(Request aRequest, Session aSession) throws SessionExc, SessionFailure {
+    synchronized (attachments) {
+      List previouslyUploadedFiles = new ArrayList(attachments);
+      attachments.clear();
+
+      if (persistentUploadedFiles) {
+        Iterator i = previouslyUploadedFiles.iterator();
+        while (i.hasNext()) {
+          Attachment uploadedFile = (Attachment) i.next();
+          if (!(aRequest.getParameter(uploadedFile.getFieldName()+"_cancel")!=null)) {
+            addAttachment(aRequest, aSession, uploadedFile, uploadedFile.getFieldName());
+          }
+        }
+      }
+    }
+  }
+
+  protected void addAttachment(Request aRequest, Session aSession, Attachment anAttachment, String aFieldName) throws SessionExc, SessionFailure {
+    List parameters = aRequest.getPrefixedParameterNames(aFieldName+"_");
+    Iterator j = parameters.iterator();
+    while (j.hasNext()) {
+      String parameter = ((String) j.next());
+      anAttachment.setAttribute(
+          parameter.substring(aFieldName.length()+1),
+          aRequest.getParameter(parameter));
+    }
+    attachments.add(anAttachment);
+  }
+
+  public void preprocessNewAttachment(Request aRequest, Session aSession, UploadedFile aFile) throws SessionExc, SessionFailure {
+      Attachment uploadedFile =
+          new Attachment(aFile, aFile.getFileName(), "attachment"+ ++uploadedFileIndex, aFile.getContentType());
+
+      addAttachment(aRequest, aSession, uploadedFile, aFile.getFieldName());
+  }
+
+
+  /**
+   * Process newly uploaded files
+   */
+  protected void preProcessNewAttachments(Request aRequest, Session aSession) throws SessionExc, SessionFailure {
+    Iterator i = aRequest.getUploadedFiles().iterator();
+    while (i.hasNext()) {
+      preprocessNewAttachment(aRequest, aSession, (UploadedFile) i.next());
+    }
+
+    if (configuration.getBoolean("Localizer.OpenSession.AllowFTPUploads", false)) {
+      File FTPDirectory = configuration.getFile("Localizer.OpenSession.FTPDirectory");
+
+      List ftpUploads = new ArrayList(aRequest.getPrefixedParameterNames("ftpupload"));
+      Collections.sort(ftpUploads, new Comparator() {
+        public int compare(Object o1, Object o2) {
+          if (o1 instanceof String && o2 instanceof String) {
+            return ((String) o1).compareTo((String) o2);
+          }
+          else {
+            return 0;
+          }
+        }
+      });
+
+      i = ftpUploads.iterator();
+      while (i.hasNext()) {
+        final String fieldName = (String) i.next();
+
+        if (fieldName.indexOf("_")<0) {
+          final String fileName = aRequest.getParameter(fieldName);
+
+          if (fileName!=null && fileName.trim().length()>0) {
+            final File sourceFile = new File(FTPDirectory, fileName);
+
+            if (sourceFile.getParentFile().equals(FTPDirectory)) {
+              preprocessNewAttachment(aRequest, aSession, new UploadedFile() {
+                public void writeToFile(File aFile) throws SessionFailure {
+                  try {
+                    FileRoutines.move(sourceFile, aFile);
+                  }
+                  catch (IOException e) {
+                    throw new SessionFailure(e);
+                  }
+                }
+
+                public void writeToStream(OutputStream aStream) throws SessionFailure {
+                  try {
+                    IORoutines.copyStream(getInputStream(), aStream);
+                  }
+                  catch (IOException e) {
+                    throw new SessionFailure(e);
+                  }
+                }
+
+                public InputStream getInputStream() throws SessionFailure {
+                  try {
+                    return new FileInputStream(sourceFile);
+                  }
+                  catch (IOException e) {
+                    throw new SessionFailure(e);
+                  }
+                }
+
+                public String getFileName() {
+                  return fileName;
+                }
+
+                public String getFieldName() {
+                  return fieldName;
+                }
+
+                public String getContentType() {
+                  return null;
+                }
+              });
+            }
+          }
+        }
+      }
+    }
   }
 
   protected void makeInitialResponse(Request aRequest, Session aSession, Response aResponse) throws SessionExc, SessionFailure {
     aResponse.setResponseGenerator(normalResponseGenerator);
-  };
+  }
 
   protected void makeResponse(Request aRequest, Session aSession, Response aResponse, List anErrors) throws SessionExc, SessionFailure {
     aResponse.setResponseValue("errors", anErrors);
     aResponse.setResponseGenerator(normalResponseGenerator);
-  };
+  }
 
   protected void makeFinalResponse(Request aRequest, Session aSession, Response aResponse) throws SessionExc, SessionFailure {
     aResponse.setResponseGenerator(finalResponseGenerator);
-  };
+  }
 
   protected void makeErrorResponse(Request aRequest, Session aSession, Response aResponse, Throwable anError) throws SessionExc, SessionFailure {
-    anError.printStackTrace();
-    Throwable rootCause = ExceptionFunctions.traceCauseException(anError);
+    Throwable rootCause = ExceptionRoutines.traceCauseException(anError);
 
     if (rootCause instanceof DuplicatePostingExc)
       aResponse.setResponseGenerator(dupeResponseGenerator);
-    if (rootCause instanceof ModuleMediaType.UnsupportedMimeTypeExc) {
-      aResponse.setResponseValue("mimetype", ((ModuleMediaType.UnsupportedMimeTypeExc) rootCause).getMimeType());
+    if (rootCause instanceof UnsupportedMediaTypeExc) {
+      aResponse.setResponseValue("mimetype", ((UnsupportedMediaTypeExc) rootCause).getMimeType());
       aResponse.setResponseGenerator(unsupportedMediaTypeResponseGenerator);
     }
     else {
-      aResponse.setResponseValue("errorstring", anError.getMessage());
-      aResponse.setResponseGenerator(configuration.getString("Localizer.OpenSession.ErrorTemplate"));
+      List errors = new ArrayList();
+      errors.add(new ValidationError("", "general.unexpectederror",
+          new Object[] {anError.getMessage()}));
+      makeResponse(aRequest, aSession, aResponse, errors);
     }
-  };
+  }
 
   protected void makeOpenPostingDisabledResponse(Request aRequest, Session aSession, Response aResponse) {
     aResponse.setResponseGenerator(configuration.getString("ServletModule.OpenIndy.PostingDisabledTemplate"));
   }
 
+  /**
+   *
+   */
   protected void preProcessRequest(Request aRequest, Session aSession) throws SessionExc, SessionFailure {
-  };
-  public void processUploadedFile(Request aRequest, Session aSession, UploadedFile aFile) throws SessionExc, SessionFailure {
-  };
+  }
+  public void processAttachment(Request aRequest, Session aSession, Attachment aFile) throws SessionExc, SessionFailure {
+  }
+  public void processAttachmentError(Request aRequest, Session aSession, Attachment aFile, Throwable anError) {
+  }
   protected void postProcessRequest(Request aRequest, Session aSession) throws SessionExc, SessionFailure {
-  };
+  }
 
+  /**
+   * Determine whether the request shoudl be processed: that is, the validate,
+   * preprocess, postprocess methods should be called to perform validations,
+   * store data, etc
+   */
   protected boolean shouldProcessRequest(Request aRequest, Session aSession, List aValidationErrors) throws SessionExc, SessionFailure {
-    int nrMediaItems = ((Integer) aSession.getAttribute("nrmediaitems")).intValue();
-    try {
-      nrMediaItems = Math.min(configuration.getInt("ServletModule.OpenIndy.MaxMediaUploadItems"), Integer.parseInt(aRequest.getParameter("nrmediaitems")));
-    }
-    catch (Throwable t) {
-    }
-
-    aSession.setAttribute("nrmediaitems", new Integer(nrMediaItems));
-
-    if (aRequest.getParameter("post")==null)
+    if (aRequest.getParameter("post")==null) {
       return false;
-    else {
-      validate(aValidationErrors, aRequest, aSession);
-      return (aValidationErrors == null || aValidationErrors.size() == 0);
     }
+    validate(aValidationErrors, aRequest, aSession);
+    
+    return (aValidationErrors == null || aValidationErrors.size() == 0);
   }
 
+  /**
+   * Method used to validate user input.
+   * Multiple {@link ValidationError}s may be added to the
+   * <code>aResults</code> parameter.
+   * The request is considered validated if, after calling this method,
+   * <code>aResults</code> is empty.
+   */
   protected void validate(List aResults, Request aRequest, Session aSession) throws SessionExc, SessionFailure {
-    String password = (String) aSession.getAttribute("password");
-
-    if (password!=null) {
-      String submittedPassword= aRequest.getParameter("password").trim();
+    if (MirGlobal.abuse().getRequireCaptcha()) {
+      String submittedPassword = aRequest.getParameter("password");
+      MirOpenPostingLocalizer.Captcha captcha = (MirOpenPostingLocalizer.Captcha) aSession.getAttribute("captcha");
 
-      if (!password.equals(submittedPassword)) {
-        aResults.add(new ValidationError("password", "passwordmismatch"));
+      if (captcha == null || !captcha.validateAnswer(submittedPassword)) {
+        aResults.add(new ValidationError("password", "validationerror.passwordmismatch"));
       }
     }
   }
 
 
-  /**
-   * Method to generate a one-time password
-   *
-   * @return a password, to be used once
-   */
-
-  protected String generateOnetimePassword() {
-    Random r = new Random();
-    int random = r.nextInt();
-
-    long l = System.currentTimeMillis();
-
-    l = (l*l*l*l)/random;
-    if (l<0)
-      l = l * -1;
-
-    String returnString = ""+l;
-
-    return returnString.substring(5);
-  }
-
 
   /**
-   *
-   * @param aRequest
-   * @param aStorage
-   * @return
-   * @throws SessionExc
-   * @throws SessionFailure
+   * Method to filter the attributes and their values of a request
+   * based on the fields of a database object.
    */
-
-  protected static final Map getIntersectingValues(Request aRequest, StorageObject aStorage) throws SessionExc, SessionFailure {
+  protected static Map getIntersectingValues(Request aRequest, Database aStorage) throws SessionFailure {
     Map result = new HashMap();
 
-    Iterator i = aStorage.getFields().iterator();
+    Iterator i = aStorage.getFieldNames().iterator();
 
     while (i.hasNext()) {
       String fieldName = (String) i.next();
@@ -306,10 +457,73 @@ public abstract class MirBasicPostingSessionHandler implements SessionHandler {
     return result;
   }
 
+  /**
+   * Exception to be thrown when an article or comment was already posted
+   */
   protected static class DuplicatePostingExc extends SessionExc {
     public DuplicatePostingExc(String aMessage) {
       super(aMessage);
     }
   }
 
+  /**
+   * A file that has been attached to a session
+   */
+  protected static class Attachment implements UploadedFile {
+    private UploadedFile uploadedFile;
+    private String filename;
+    private String fieldName;
+    private String contentType;
+    private Map attributes;
+
+    public Attachment(UploadedFile anUploadedFile, String aFilename, String aFieldName, String aContentType) {
+      attributes = new HashMap();
+      filename = aFilename;
+      fieldName = aFieldName;
+      contentType = aContentType;
+      uploadedFile = anUploadedFile;
+    }
+
+    public void writeToFile(File aFile) throws SessionExc, SessionFailure {
+      uploadedFile.writeToFile(aFile);
+    }
+
+    public void writeToStream(OutputStream aStream) throws SessionExc, SessionFailure {
+      try {
+        IORoutines.copyStream(uploadedFile.getInputStream(), aStream);
+      }
+      catch (IOException e) {
+        throw new SessionFailure(e);
+      }
+    }
+
+    public InputStream getInputStream() throws SessionExc, SessionFailure {
+      return uploadedFile.getInputStream();
+    }
+
+    public String getFileName() {
+      return filename;
+    }
+
+    public String getFieldName() {
+      return fieldName;
+    }
+
+    public String getContentType() {
+      return contentType;
+    }
+
+    public String getAttribute(String anAttribute) {
+      return (String) attributes.get(anAttribute);
+    }
+
+    public void setAttribute(String anAttribute, String aValue) {
+      attributes.put(anAttribute, aValue);
+    }
+
+    public Map getAllAttributes() {
+      return Collections.unmodifiableMap(attributes);
+    }
+  }
 }
+