001/* 002 * (C) Copyright 2011-2012 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Wojciech Sulejman 016 * Florent Guillaume 017 */ 018package org.nuxeo.ecm.platform.signature.web.sign; 019 020import static org.jboss.seam.international.StatusMessage.Severity.ERROR; 021import static org.jboss.seam.international.StatusMessage.Severity.INFO; 022import static org.jboss.seam.international.StatusMessage.Severity.WARN; 023 024import java.io.Serializable; 025import java.security.Principal; 026import java.security.cert.X509Certificate; 027import java.text.SimpleDateFormat; 028import java.util.Collections; 029import java.util.Date; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033 034import org.apache.commons.io.FilenameUtils; 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.jboss.seam.ScopeType; 038import org.jboss.seam.annotations.In; 039import org.jboss.seam.annotations.Name; 040import org.jboss.seam.annotations.Scope; 041import org.jboss.seam.faces.FacesMessages; 042import org.nuxeo.ecm.core.api.Blob; 043import org.nuxeo.ecm.core.api.DocumentModel; 044import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 045import org.nuxeo.ecm.core.api.event.DocumentEventCategories; 046import org.nuxeo.ecm.core.event.EventProducer; 047import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 048import org.nuxeo.ecm.platform.signature.api.exception.CertException; 049import org.nuxeo.ecm.platform.signature.api.exception.SignException; 050import org.nuxeo.ecm.platform.signature.api.pki.CertService; 051import org.nuxeo.ecm.platform.signature.api.sign.SignatureService; 052import org.nuxeo.ecm.platform.signature.api.sign.SignatureService.SigningDisposition; 053import org.nuxeo.ecm.platform.signature.api.sign.SignatureService.StatusWithBlob; 054import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 055import org.nuxeo.ecm.platform.usermanager.UserManager; 056import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor; 057import org.nuxeo.runtime.api.Framework; 058 059/** 060 * Document signing actions 061 */ 062@Name("signActions") 063@Scope(ScopeType.CONVERSATION) 064public class SignActions implements Serializable { 065 066 private static final long serialVersionUID = 1L; 067 068 private static final Log log = LogFactory.getLog(SignActions.class); 069 070 /** 071 * If this system property is set to "true", then signature will use PDF/A. 072 */ 073 public static final String SIGNATURE_USE_PDFA_PROP = "org.nuxeo.ecm.signature.pdfa"; 074 075 /** 076 * Signature disposition for PDF files. Can be "replace", "archive" or "attach". 077 */ 078 public static final String SIGNATURE_DISPOSITION_PDF = "org.nuxeo.ecm.signature.disposition.pdf"; 079 080 /** 081 * Signature disposition for non-PDF files. Can be "replace", "archive" or "attach". 082 */ 083 public static final String SIGNATURE_DISPOSITION_NOTPDF = "org.nuxeo.ecm.signature.disposition.notpdf"; 084 085 public static final String SIGNATURE_ARCHIVE_FILENAME_FORMAT_PROP = "org.nuxeo.ecm.signature.archive.filename.format"; 086 087 /** Used with {@link SimpleDateFormat}. */ 088 public static final String DEFAULT_ARCHIVE_FORMAT = " ('archive' yyyy-MM-dd HH:mm:ss)"; 089 090 protected static final String LABEL_SIGN_DOCUMENT_MISSING = "label.sign.document.missing"; 091 092 protected static final String NOTIFICATION_SIGN_PROBLEM = "notification.sign.problem"; 093 094 protected static final String NOTIFICATION_SIGN_CERTIFICATE_ACCESS_PROBLEM = "notification.sign.certificate.access.problem"; 095 096 protected static final String NOTIFICATION_SIGN_SIGNED = "notification.sign.signed"; 097 098 public static final String MIME_TYPE_PDF = "application/pdf"; 099 100 public static final String DOCUMENT_SIGNED = "documentSigned"; 101 102 public static final String DOCUMENT_SIGNED_COMMENT = "PDF signed"; 103 104 @In(create = true) 105 protected transient SignatureService signatureService; 106 107 @In(create = true) 108 protected transient CertService certService; 109 110 @In(create = true) 111 protected transient NavigationContext navigationContext; 112 113 @In(create = true) 114 protected ResourcesAccessor resourcesAccessor; 115 116 @In(create = true, required = false) 117 protected FacesMessages facesMessages; 118 119 @In(create = true) 120 protected transient UserManager userManager; 121 122 @In(create = true) 123 protected Principal currentUser; 124 125 protected void info(String msg) { 126 facesMessages.add(INFO, getMessage(msg)); 127 } 128 129 protected void warn(String msg) { 130 facesMessages.add(WARN, getMessage(msg)); 131 } 132 133 protected void error(String msg) { 134 facesMessages.add(ERROR, getMessage(msg)); 135 } 136 137 protected String getMessage(String msg) { 138 return resourcesAccessor.getMessages().get(msg); 139 } 140 141 protected DocumentModel getCurrentUserModel() { 142 return userManager.getUserModel(currentUser.getName()); 143 } 144 145 /** 146 * Signs digitally a PDF blob contained in the current document, modifies the document status and updates UI & 147 * auditing messages related to signing 148 * 149 * @param signingReason 150 * @param password 151 * @throws SignException 152 */ 153 public void signCurrentDoc(String signingReason, String password) { 154 155 DocumentModel currentDoc = navigationContext.getCurrentDocument(); 156 DocumentModel currentUserModel = getCurrentUserModel(); 157 StatusWithBlob swb = signatureService.getSigningStatus(currentDoc, currentUserModel); 158 if (swb.status == StatusWithBlob.UNSIGNABLE) { 159 error(LABEL_SIGN_DOCUMENT_MISSING); 160 return; 161 } 162 163 Blob originalBlob = currentDoc.getAdapter(BlobHolder.class).getBlob(); 164 boolean originalIsPdf = MIME_TYPE_PDF.equals(originalBlob.getMimeType()); 165 166 // decide if we want PDF/A 167 boolean pdfa = getPDFA(); 168 169 // decide disposition 170 SigningDisposition disposition = getDisposition(originalIsPdf); 171 172 // decide archive filename 173 String filename = originalBlob.getFilename(); 174 String archiveFilename = getArchiveFilename(filename); 175 176 try { 177 signatureService.signDocument(currentDoc, currentUserModel, password, signingReason, pdfa, disposition, 178 archiveFilename); 179 } catch (CertException e) { 180 log.debug("Signing problem: " + e.getMessage(), e); 181 error(NOTIFICATION_SIGN_CERTIFICATE_ACCESS_PROBLEM); 182 return; 183 } catch (SignException e) { 184 log.debug("Signing problem: " + e.getMessage(), e); 185 error(NOTIFICATION_SIGN_PROBLEM); 186 facesMessages.add(ERROR, e.getMessage()); 187 return; 188 } 189 190 // important to save doc now 191 navigationContext.saveCurrentDocument(); 192 193 // write to the audit log 194 Map<String, Serializable> properties = new HashMap<String, Serializable>(); 195 String comment = DOCUMENT_SIGNED_COMMENT; 196 notifyEvent(DOCUMENT_SIGNED, currentDoc, properties, comment); 197 198 // display a signing message 199 facesMessages.add(INFO, filename + " " + getMessage(NOTIFICATION_SIGN_SIGNED)); 200 } 201 202 protected boolean getPDFA() { 203 return Framework.isBooleanPropertyTrue(SIGNATURE_USE_PDFA_PROP); 204 } 205 206 protected SigningDisposition getDisposition(boolean originalIsPdf) { 207 String disp; 208 if (originalIsPdf) { 209 disp = Framework.getProperty(SIGNATURE_DISPOSITION_PDF, SigningDisposition.ARCHIVE.name()); 210 } else { 211 disp = Framework.getProperty(SIGNATURE_DISPOSITION_NOTPDF, SigningDisposition.ATTACH.name()); 212 } 213 try { 214 return Enum.valueOf(SigningDisposition.class, disp.toUpperCase()); 215 } catch (RuntimeException e) { 216 log.warn("Invalid signing disposition: " + disp); 217 return SigningDisposition.ATTACH; 218 } 219 } 220 221 protected String getArchiveFilename(String filename) { 222 String format = Framework.getProperty(SIGNATURE_ARCHIVE_FILENAME_FORMAT_PROP, DEFAULT_ARCHIVE_FORMAT); 223 return FilenameUtils.getBaseName(filename) + new SimpleDateFormat(format).format(new Date()) + "." 224 + FilenameUtils.getExtension(filename); 225 } 226 227 /** 228 * Gets the signing status for the current document. 229 * 230 * @return the signing status 231 */ 232 public StatusWithBlob getSigningStatus() { 233 DocumentModel currentDoc = navigationContext.getCurrentDocument(); 234 return signatureService.getSigningStatus(currentDoc, getCurrentUserModel()); 235 } 236 237 /** 238 * Returns info about the certificates contained in the current document. 239 */ 240 public List<X509Certificate> getCertificateList() throws SignException { 241 242 DocumentModel currentDoc = navigationContext.getCurrentDocument(); 243 if (currentDoc == null) { 244 error(LABEL_SIGN_DOCUMENT_MISSING); 245 return Collections.emptyList(); 246 } 247 248 return signatureService.getCertificates(currentDoc); 249 // certificate.getSubjectDN() 250 // certificate.getIssuerDN() 251 // certificate.getNotAfter() 252 } 253 254 protected void notifyEvent(String eventId, DocumentModel source, Map<String, Serializable> properties, 255 String comment) { 256 properties.put(DocumentEventContext.COMMENT_PROPERTY_KEY, comment); 257 properties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, DocumentEventCategories.EVENT_DOCUMENT_CATEGORY); 258 259 DocumentEventContext eventContext = new DocumentEventContext(source.getCoreSession(), 260 source.getCoreSession().getPrincipal(), source); 261 262 eventContext.setProperties(properties); 263 264 Framework.getLocalService(EventProducer.class).fireEvent(eventContext.newEvent(eventId)); 265 } 266 267}