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