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}