001: package ru.emdev.EmForge.wiki.web;
002:
003: import java.io.File;
004: import java.io.IOException;
005: import java.io.InputStream;
006: import java.io.OutputStream;
007: import java.net.SocketException;
008: import java.security.Permission;
009: import java.security.Principal;
010: import java.util.Collection;
011: import java.util.Iterator;
012: import java.util.Properties;
013:
014: import javax.servlet.ServletConfig;
015: import javax.servlet.ServletContext;
016: import javax.servlet.ServletException;
017: import javax.servlet.http.HttpServletRequest;
018: import javax.servlet.http.HttpServletResponse;
019:
020: import org.apache.commons.fileupload.FileItem;
021: import org.apache.commons.fileupload.FileItemFactory;
022: import org.apache.commons.fileupload.FileUploadException;
023: import org.apache.commons.fileupload.disk.DiskFileItemFactory;
024: import org.apache.commons.fileupload.servlet.ServletFileUpload;
025: import org.apache.commons.io.FilenameUtils;
026: import org.apache.commons.lang.StringUtils;
027: import org.apache.commons.logging.Log;
028: import org.apache.commons.logging.LogFactory;
029: import org.springframework.transaction.PlatformTransactionManager;
030: import org.springframework.transaction.TransactionStatus;
031: import org.springframework.transaction.support.TransactionCallbackWithoutResult;
032: import org.springframework.transaction.support.TransactionTemplate;
033: import org.springframework.web.context.WebApplicationContext;
034: import org.springframework.web.context.support.WebApplicationContextUtils;
035:
036: import com.ecyrd.jspwiki.TextUtil;
037: import com.ecyrd.jspwiki.WikiContext;
038: import com.ecyrd.jspwiki.WikiEngine;
039: import com.ecyrd.jspwiki.WikiProvider;
040: import com.ecyrd.jspwiki.attachment.Attachment;
041: import com.ecyrd.jspwiki.attachment.AttachmentManager;
042: import com.ecyrd.jspwiki.auth.permissions.PagePermission;
043: import com.ecyrd.jspwiki.dav.AttachmentDavProvider;
044: import com.ecyrd.jspwiki.dav.DavPath;
045: import com.ecyrd.jspwiki.dav.DavProvider;
046: import com.ecyrd.jspwiki.dav.WebdavServlet;
047: import com.ecyrd.jspwiki.dav.methods.DavMethod;
048: import com.ecyrd.jspwiki.dav.methods.PropFindMethod;
049: import com.ecyrd.jspwiki.filters.RedirectException;
050: import com.ecyrd.jspwiki.providers.ProviderException;
051: import com.ecyrd.jspwiki.util.HttpUtil;
052:
053: /** Wiki Attachment Servlet
054: *
055: * This servlet extends attachment servlet from JspWiki to get WikiEngine
056: * not from configuration files but from Spring Framework
057: *
058: * @todo Spring Framework has own way to implement servlets -
059: * we should try to use it
060: *
061: * Unfortunatelly since we cannot access m_engine form base class we
062: * implemented our own copy of servlet with copiyng code from AttachmentServlet
063: *
064: *
065: * @web.servlet
066: * name="WikiAttachmentServlet"
067: * display-name="Wiki Attachment Servlet"
068: *
069: * @web.servlet-mapping
070: * url-pattern="/attach/*"
071: */
072: public class WikiAttachmentServlet extends WebdavServlet {
073: private static final long serialVersionUID = -276655889351822283L;
074:
075: protected final Log log = LogFactory.getLog(getClass());
076:
077: private WikiEngine m_engine;
078: private TransactionTemplate transactionTemplate;
079:
080: public static final String HDR_VERSION = "version";
081: public static final String HDR_NAME = "page";
082:
083: /** Default expiry period is 1 day */
084: protected static final long DEFAULT_EXPIRY = 1 * 24 * 60 * 60
085: * 1000;
086:
087: private String m_tmpDir;
088:
089: private DavProvider m_attachmentProvider;
090:
091: /**
092: * The maximum size that an attachment can be.
093: */
094: private int m_maxSize = Integer.MAX_VALUE;
095:
096: //
097: // Not static as DateFormat objects are not thread safe.
098: // Used to handle the RFC date format = Sat, 13 Apr 2002 13:23:01 GMT
099: //
100: //private final DateFormat rfcDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
101:
102: /** Initialize Servlet
103: *
104: * Parent class make all initialization here.
105: * For example it gets wiki engine from configuration.
106: * So, we should skip it here and initialize from Spring later,
107: * then we will get HttpRequest and will able to get Spring's WebApplication
108: */
109: public void init(ServletConfig i_config) throws ServletException {
110: // [AKA] nothing else to do - all initialization will be done later
111: super .init(i_config);
112: }
113:
114: /** Do real initialization by getting Spring's WebApplication from request
115: *
116: * @param i_request
117: */
118: public void init(HttpServletRequest i_request)
119: throws ServletException {
120: if (m_engine == null) {
121: WebApplicationContext appContext = WebApplicationContextUtils
122: .getWebApplicationContext(i_request.getSession()
123: .getServletContext());
124:
125: //get wiki engine from spring
126: m_engine = (WikiEngine) appContext.getBean("wikiEngine");
127:
128: Properties props = m_engine.getWikiProperties();
129:
130: m_attachmentProvider = new AttachmentDavProvider(m_engine);
131: m_tmpDir = m_engine.getWorkDir() + File.separator
132: + "attach-tmp";
133:
134: m_maxSize = TextUtil.getIntegerProperty(props,
135: AttachmentManager.PROP_MAXSIZE, Integer.MAX_VALUE);
136:
137: File f = new File(m_tmpDir);
138: if (!f.exists()) {
139: f.mkdirs();
140: } else if (!f.isDirectory()) {
141: log
142: .fatal("A file already exists where the temporary dir is supposed to be: "
143: + m_tmpDir + ". Please remove it.");
144: }
145:
146: // initialize transaction manager
147: transactionTemplate = new TransactionTemplate(
148: (PlatformTransactionManager) appContext
149: .getBean("txManager"));
150: transactionTemplate.setReadOnly(true);
151:
152: log.debug("UploadServlet initialized. Using " + m_tmpDir
153: + " for temporary storage.");
154: }
155: }
156:
157: /** Code form JspWiki Attachment Servlet */
158:
159: public void doPropFind(HttpServletRequest req,
160: HttpServletResponse res) throws IOException,
161: ServletException {
162: // [AKA] Added initialization
163: init(req);
164:
165: DavMethod dm = new PropFindMethod(m_attachmentProvider);
166:
167: String p = new String(req.getPathInfo().getBytes("ISO-8859-1"),
168: "UTF-8");
169:
170: DavPath path = new DavPath(p);
171:
172: dm.execute(req, res, path);
173: }
174:
175: protected void doOptions(HttpServletRequest req,
176: HttpServletResponse res) {
177: res.setHeader("DAV", "1"); // We support only Class 1
178: res
179: .setHeader("Allow",
180: "GET, PUT, POST, OPTIONS, PROPFIND, PROPPATCH, MOVE, COPY, DELETE");
181: res.setStatus(HttpServletResponse.SC_OK);
182: }
183:
184: /**
185: * Serves a GET with two parameters: 'wikiname' specifying the wikiname
186: * of the attachment, 'version' specifying the version indicator.
187: */
188:
189: // FIXME: Messages would need to be localized somehow.
190: public void doGet(HttpServletRequest req,
191: final HttpServletResponse res) throws IOException,
192: ServletException {
193: // [AKA] Added initialization
194: init(req);
195:
196: final WikiContext context = m_engine.createContext(req,
197: WikiContext.ATTACH);
198:
199: String version = req.getParameter(HDR_VERSION);
200: String nextPage = req.getParameter("nextpage");
201:
202: String msg = "An error occurred. Ouch.";
203: int ver = WikiProvider.LATEST_VERSION;
204:
205: final AttachmentManager mgr = m_engine.getAttachmentManager();
206: // AuthorizationManager authmgr = m_engine.getAuthorizationManager();
207:
208: String page = context.getPage().getName();
209:
210: if (page == null) {
211: log.info("Invalid attachment name.");
212: res.sendError(HttpServletResponse.SC_BAD_REQUEST);
213: return;
214: }
215:
216: try {
217: log.debug("Attempting to download att " + page
218: + ", version " + version);
219: if (version != null) {
220: ver = Integer.parseInt(version);
221: }
222:
223: final Attachment att = mgr.getAttachmentInfo(page, ver);
224:
225: if (att != null) {
226: //
227: // Check if the user has permission for this attachment
228: //
229:
230: /*
231: Permission permission = PermissionFactory.getPagePermission( att, "view" );
232: if( !authmgr.checkPermission( context.getWikiSession(), permission ) )
233: {
234: log.debug("User does not have permission for this");
235: res.sendError( HttpServletResponse.SC_FORBIDDEN );
236: return;
237: }
238: */
239:
240: //
241: // Check if the client already has a version of this attachment.
242: //
243: if (HttpUtil.checkFor304(req, att)) {
244: log
245: .debug("Client has latest version already, sending 304...");
246: res.sendError(HttpServletResponse.SC_NOT_MODIFIED);
247: return;
248: }
249:
250: String mimetype = getMimeType(context, att
251: .getFileName());
252:
253: res.setContentType(mimetype);
254:
255: //
256: // We use 'inline' instead of 'attachment' so that user agents
257: // can try to automatically open the file.
258: //
259:
260: res.addHeader("Content-Disposition",
261: "inline; filename=\"" + att.getFileName()
262: + "\";");
263:
264: res.addDateHeader("Last-Modified", att
265: .getLastModified().getTime());
266:
267: if (!att.isCacheable()) {
268: res.addHeader("Pragma", "no-cache");
269: res.addHeader("Cache-control", "no-cache");
270: }
271:
272: // If a size is provided by the provider, report it.
273: if (att.getSize() >= 0) {
274: // log.info("size:"+att.getSize());
275: res.setContentLength((int) att.getSize());
276: }
277:
278: transactionTemplate
279: .execute(new TransactionCallbackWithoutResult() {
280:
281: protected void doInTransactionWithoutResult(
282: TransactionStatus status) {
283: OutputStream out = null;
284: InputStream in = null;
285: try {
286: out = res.getOutputStream();
287: in = mgr.getAttachmentStream(
288: context, att);
289:
290: int read = 0;
291: byte[] buffer = new byte[8192];
292:
293: while ((read = in.read(buffer)) > -1) {
294: out.write(buffer, 0, read);
295: }
296: } catch (Exception ex) {
297: log
298: .error(
299: "Cannot get attachment contents",
300: ex);
301: } finally {
302: if (in != null) {
303: try {
304: in.close();
305: } catch (IOException e) {
306: }
307: }
308:
309: //
310: // Quite often, aggressive clients close the connection when they have
311: // received the last bits. Therefore, we close the output, but ignore
312: // any exception that might come out of it.
313: //
314: if (out != null) {
315: try {
316: out.close();
317: } catch (IOException e) {
318: }
319: }
320:
321: }
322: }
323: });
324:
325: if (log.isDebugEnabled()) {
326: msg = "Attachment " + att.getFileName()
327: + " sent to " + req.getRemoteUser()
328: + " on " + req.getRemoteAddr();
329: log.debug(msg);
330: }
331: if (nextPage != null)
332: res.sendRedirect(nextPage);
333:
334: return;
335: }
336:
337: msg = "Attachment '" + page + "', version " + ver
338: + " does not exist.";
339:
340: log.info(msg);
341: res.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
342: return;
343: } catch (ProviderException pe) {
344: msg = "Provider error: " + pe.getMessage();
345:
346: log.debug("Provider failed while reading", pe);
347: //
348: // This might fail, if the response is already committed. So in that
349: // case we just log it.
350: //
351: try {
352: res.sendError(
353: HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
354: msg);
355: } catch (IllegalStateException e) {
356: }
357: return;
358: } catch (NumberFormatException nfe) {
359: msg = "Invalid version number (" + version + ")";
360: res.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
361: return;
362: } catch (SocketException se) {
363: //
364: // These are very common in download situations due to aggressive
365: // clients. No need to try and send an error.
366: //
367: log.debug("I/O exception during download", se);
368: return;
369: } catch (IOException ioe) {
370: //
371: // Client dropped the connection or something else happened.
372: // We don't know where the error came from, so we'll at least
373: // try to send an error and catch it quietly if it doesn't quite work.
374: //
375: msg = "Error: " + ioe.getMessage();
376: log.debug("I/O exception during download", ioe);
377:
378: try {
379: res.sendError(
380: HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
381: msg);
382: } catch (IllegalStateException e) {
383: }
384: return;
385: }
386: }
387:
388: /**
389: * Returns the mime type for this particular file. Case does not matter.
390: *
391: * @param ctx WikiContext; required to access the ServletContext of the request.
392: * @param fileName The name to check for.
393: * @return A valid mime type, or application/binary, if not recognized
394: */
395: private static String getMimeType(WikiContext ctx, String fileName) {
396: String mimetype = null;
397:
398: HttpServletRequest req = ctx.getHttpRequest();
399: if (req != null) {
400: ServletContext s = req.getSession().getServletContext();
401:
402: if (s != null) {
403: mimetype = s.getMimeType(fileName.toLowerCase());
404: }
405: }
406:
407: if (mimetype == null) {
408: mimetype = "application/binary";
409: }
410:
411: return mimetype;
412: }
413:
414: /**
415: * Grabs mime/multipart data and stores it into the temporary area.
416: * Uses other parameters to determine which name to store as.
417: *
418: * <p>The input to this servlet is generated by an HTML FORM with
419: * two parts. The first, named 'page', is the WikiName identifier
420: * for the parent file. The second, named 'content', is the binary
421: * content of the file.
422: */
423: public void doPost(HttpServletRequest req, HttpServletResponse res)
424: throws IOException, ServletException {
425: // should not be called - since upload now done via AttachmentsUploadController.
426: }
427:
428: public void doPut(HttpServletRequest req, HttpServletResponse res)
429: throws IOException, ServletException {
430: // [AKA] Added initialization
431: init(req);
432:
433: String errorPage = m_engine.getURL(WikiContext.ERROR, "", null,
434: false); // If something bad happened, Upload should be able to take care of most stuff
435:
436: String p = new String(req.getPathInfo().getBytes("ISO-8859-1"),
437: "UTF-8");
438: DavPath path = new DavPath(p);
439:
440: try {
441: InputStream data = req.getInputStream();
442:
443: WikiContext context = m_engine.createContext(req,
444: WikiContext.UPLOAD);
445:
446: String wikipage = path.get(0);
447:
448: errorPage = context.getURL(WikiContext.UPLOAD, wikipage);
449:
450: boolean created = executeUpload(context, data, path
451: .getName(), errorPage, wikipage, req
452: .getContentLength());
453:
454: if (created)
455: res.sendError(HttpServletResponse.SC_CREATED);
456: else
457: res.sendError(HttpServletResponse.SC_OK);
458: } catch (ProviderException e) {
459: res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
460: e.getMessage());
461: } catch (RedirectException e) {
462: res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
463: e.getMessage());
464: }
465: }
466:
467: /**
468: * Uploads a specific mime multipart input set, intercepts exceptions.
469: *
470: * @return The page to which we should go next.
471: */
472: protected String upload(HttpServletRequest req)
473: throws RedirectException, IOException {
474: String msg = "";
475: String attName = "(unknown)";
476: String errorPage = m_engine.getURL(WikiContext.ERROR, "", null,
477: false); // If something bad happened, Upload should be able to take care of most stuff
478: String nextPage = errorPage;
479:
480: try {
481: // Create the context _before_ Multipart operations, otherwise
482: // strict servlet containers may fail when setting encoding.
483: WikiContext context = m_engine.createContext(req,
484: WikiContext.ATTACH);
485:
486: // Create a factory for disk-based file items
487: FileItemFactory factory = new DiskFileItemFactory();
488:
489: // Create a new file upload handler
490: ServletFileUpload upload = new ServletFileUpload(factory);
491:
492: /*
493: nextPage = multi.getURLParameter( "nextpage" );
494: String wikipage = multi.getURLParameter( "page" );
495:
496: //
497: // FIXME: Kludge alert. We must end up with the parent page name,
498: // if this is an upload of a new revision
499: //
500:
501: int x = wikipage.indexOf("/");
502:
503: if( x != -1 ) wikipage = wikipage.substring(0,x);
504:
505: errorPage = context.getURL( WikiContext.UPLOAD,
506: wikipage );
507:
508: */
509: String wikipage = null;
510:
511: Collection /* FileItem */items = upload.parseRequest(req);
512: Iterator iter = items.iterator();
513: FileItem fileItem;
514:
515: while (iter.hasNext()) {
516: fileItem = (FileItem) iter.next();
517: if (fileItem.isFormField()) {
518: // this is form field
519: String name = fileItem.getFieldName();
520: String value = fileItem.getString();
521:
522: if (name.equals("page")) {
523: wikipage = value;
524:
525: int x = wikipage.indexOf("/");
526:
527: if (x != -1)
528: wikipage = wikipage.substring(0, x);
529:
530: errorPage = context.getURL(WikiContext.UPLOAD,
531: wikipage);
532: } else if (name.equals("nextpage")) {
533: nextPage = value;
534: }
535: } else {
536: // this is file to upload
537: String filename = fileItem.getName();
538: InputStream in = fileItem.getInputStream();
539:
540: assert wikipage != null;
541: executeUpload(context, in, filename, nextPage,
542: wikipage, req.getContentLength());
543: }
544: }
545:
546: // Inform the JSP page of which file we are handling:
547: // req.setAttribute( ATTR_ATTACHMENT, wikiname );
548: } catch (ProviderException e) {
549: msg = "Upload failed because the provider failed: "
550: + e.getMessage();
551: log.warn(msg + " (attachment: " + attName + ")", e);
552:
553: throw new IOException(msg);
554: } catch (IOException e) {
555: // Show the submit page again, but with a bit more
556: // intimidating output.
557: msg = "Upload failure: " + e.getMessage();
558: log.warn(msg + " (attachment: " + attName + ")", e);
559:
560: throw e;
561: } catch (FileUploadException fue) {
562: msg = "Upload failed because the provider failed: "
563: + fue.getMessage();
564: log.warn(msg + " (attachment: " + attName + ")", fue);
565:
566: throw new IOException(msg);
567: } finally {
568: // FIXME: In case of exceptions should absolutely
569: // remove the uploaded file.
570: }
571:
572: return nextPage;
573: }
574:
575: /**
576: *
577: * @param context the wiki context
578: * @param data the input stream data
579: * @param filename the name of the file to upload
580: * @param errorPage the place to which you want to get a redirection
581: * @param parentPage the page to which the file should be attached
582: * @return <code>true</code> if upload results in the creation of a new page;
583: * <code>false</code> otherwise
584: * @throws RedirectException
585: * @throws IOException
586: * @throws ProviderException
587: */
588: protected boolean executeUpload(WikiContext context,
589: InputStream data, String filename, String errorPage,
590: String parentPage, long contentLength)
591: throws RedirectException, IOException, ProviderException {
592: boolean created = false;
593:
594: //
595: // FIXME: This has the unfortunate side effect that it will receive the
596: // contents. But we can't figure out the page to redirect to
597: // before we receive the file, due to the stupid constructor of MultipartRequest.
598: //
599: if (contentLength > m_maxSize) {
600: // FIXME: Does not delete the received files.
601: throw new RedirectException("File exceeds maximum size ("
602: + m_maxSize + " bytes)", errorPage);
603: }
604:
605: Principal user = context.getCurrentUser();
606:
607: AttachmentManager mgr = m_engine.getAttachmentManager();
608:
609: if (filename == null || filename.trim().length() == 0) {
610: log.error("Empty file name given.");
611:
612: throw new RedirectException("Empty file name given.",
613: errorPage);
614: }
615:
616: //
617: // Should help with IE 5.22 on OSX
618: //
619: filename = filename.trim();
620:
621: //
622: // Remove any characters that might be a problem. Most
623: // importantly - characters that might stop processing
624: // of the URL.
625: //
626: filename = StringUtils.replaceChars(filename, "#?\"'", "____");
627:
628: // IE 6.0 give us full file name.
629: // We should get only name from it
630: //FileUtils fileUtils = new FileUtils();
631: filename = FilenameUtils.getName(filename);
632:
633: log.debug("file=" + filename);
634:
635: if (data == null) {
636: log.error("File could not be opened.");
637:
638: throw new RedirectException("File could not be opened.",
639: errorPage);
640: }
641:
642: //
643: // Check whether we already have this kind of a page.
644: // If the "page" parameter already defines an attachment
645: // name for an update, then we just use that file.
646: // Otherwise we create a new attachment, and use the
647: // filename given. Incidentally, this will also mean
648: // that if the user uploads a file with the exact
649: // same name than some other previous attachment,
650: // then that attachment gains a new version.
651: //
652:
653: Attachment att = mgr.getAttachmentInfo(context.getPage()
654: .getName());
655:
656: if (att == null) {
657: att = new Attachment(m_engine, parentPage, filename);
658: created = true;
659: }
660:
661: //
662: // Check if we're allowed to do this?
663: //
664:
665: Permission permission = new PagePermission(att, "upload");
666: if (m_engine.getAuthorizationManager().checkPermission(
667: context.getWikiSession(), permission)) {
668: if (user != null) {
669: att.setAuthor(user.getName());
670: }
671:
672: m_engine.getAttachmentManager().storeAttachment(att, data);
673:
674: log.info("User " + user + " uploaded attachment to "
675: + parentPage + " called " + filename + ", size "
676: + att.getSize());
677: } else {
678: throw new RedirectException(
679: "No permission to upload a file", errorPage);
680: }
681:
682: return created;
683: }
684:
685: /**
686: * Produces debug output listing parameters and files.
687: */
688: /*
689: private void debugContentList( MultipartRequest multi )
690: {
691: StringBuffer sb = new StringBuffer();
692:
693: sb.append( "Upload information: parameters: [" );
694:
695: Enumeration params = multi.getParameterNames();
696: while( params.hasMoreElements() )
697: {
698: String name = (String)params.nextElement();
699: String value = multi.getURLParameter( name );
700: sb.append( "[" + name + " = " + value + "]" );
701: }
702:
703: sb.append( " files: [" );
704: Enumeration files = multi.getFileParameterNames();
705: while( files.hasMoreElements() )
706: {
707: String name = (String)files.nextElement();
708: String filename = multi.getFileSystemName( name );
709: String type = multi.getContentType( name );
710: File f = multi.getFile( name );
711: sb.append( "[name: " + name );
712: sb.append( " temp_file: " + filename );
713: sb.append( " type: " + type );
714: if (f != null)
715: {
716: sb.append( " abs: " + f.getPath() );
717: sb.append( " size: " + f.length() );
718: }
719: sb.append( "]" );
720: }
721: sb.append( "]" );
722:
723:
724: log.debug( sb.toString() );
725: }
726: */
727:
728: }
|