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}