001/*
002 * (C) Copyright 2009 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 *     Thomas Roger
018 */
019
020package org.nuxeo.ecm.platform.publisher.task;
021
022import java.io.Serializable;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.nuxeo.ecm.core.api.CoreSession;
029import org.nuxeo.ecm.core.api.DocumentModel;
030import org.nuxeo.ecm.core.api.DocumentModelList;
031import org.nuxeo.ecm.core.api.DocumentRef;
032import org.nuxeo.ecm.core.api.IdRef;
033import org.nuxeo.ecm.core.api.NuxeoException;
034import org.nuxeo.ecm.core.api.NuxeoPrincipal;
035import org.nuxeo.ecm.core.api.PathRef;
036import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
037import org.nuxeo.ecm.core.api.security.ACE;
038import org.nuxeo.ecm.core.api.security.ACL;
039import org.nuxeo.ecm.core.api.security.ACP;
040import org.nuxeo.ecm.core.api.security.SecurityConstants;
041import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
042import org.nuxeo.ecm.platform.ec.notification.NotificationConstants;
043import org.nuxeo.ecm.platform.publisher.api.PublicationNode;
044import org.nuxeo.ecm.platform.publisher.api.PublishedDocument;
045import org.nuxeo.ecm.platform.publisher.api.PublishedDocumentFactory;
046import org.nuxeo.ecm.platform.publisher.api.PublishingEvent;
047import org.nuxeo.ecm.platform.publisher.impl.core.CoreFolderPublicationNode;
048import org.nuxeo.ecm.platform.publisher.impl.core.CoreProxyFactory;
049import org.nuxeo.ecm.platform.publisher.impl.core.SimpleCorePublishedDocument;
050import org.nuxeo.ecm.platform.publisher.rules.ValidatorsRule;
051import org.nuxeo.ecm.platform.task.Task;
052import org.nuxeo.ecm.platform.task.TaskEventNames;
053import org.nuxeo.ecm.platform.task.TaskService;
054import org.nuxeo.runtime.api.Framework;
055
056/**
057 * Implementation of the {@link PublishedDocumentFactory} for core implementation using native proxy system with
058 * validation workflow.
059 *
060 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
061 * @author <a href="mailto:tmartins@nuxeo.com">Thierry Martins</a>
062 * @author <a href="mailto:ataillefer@nuxeo.com">Antoine Taillefer</a>
063 */
064public class CoreProxyWithWorkflowFactory extends CoreProxyFactory implements PublishedDocumentFactory {
065
066    public static final String TASK_NAME = "org.nuxeo.ecm.platform.publisher.task.CoreProxyWithWorkflowFactory";
067
068    public static final String ACL_NAME = "org.nuxeo.ecm.platform.publisher.task.CoreProxyWithWorkflowFactory";
069
070    // XXX ataillefer: remove if refactor old JBPM ACL name
071    public static final String JBPM_ACL_NAME = "org.nuxeo.ecm.platform.publisher.jbpm.CoreProxyWithWorkflowFactory";
072
073    public static final String PUBLISH_TASK_TYPE = "publish_moderate";
074
075    public static final String LOOKUP_STATE_PARAM_KEY = "lookupState";
076
077    public static final String LOOKUP_STATE_PARAM_BYACL = "byACL";
078
079    public static final String LOOKUP_STATE_PARAM_BYTASK = "byTask";
080
081    protected LookupState lookupState = new LookupStateByACL();
082
083    @Override
084    public void init(CoreSession coreSession, ValidatorsRule validatorsRule, Map<String, String> parameters) {
085        super.init(coreSession, validatorsRule, parameters);
086        // setup lookup state strategy if requested
087        String lookupState = parameters.get(LOOKUP_STATE_PARAM_KEY);
088        if (lookupState != null) {
089            if (LOOKUP_STATE_PARAM_BYACL.equals(lookupState)) {
090                setLookupByACL();
091            } else if (LOOKUP_STATE_PARAM_BYTASK.equals(lookupState)) {
092                setLookupByTask();
093            }
094        }
095    }
096
097    public void setLookupByTask() {
098        this.lookupState = new LookupStateByTask();
099    }
100
101    public void setLookupByACL() {
102        this.lookupState = new LookupStateByACL();
103    }
104
105    @Override
106    public PublishedDocument publishDocument(DocumentModel doc, PublicationNode targetNode,
107            Map<String, String> params) {
108        DocumentModel targetDocModel;
109        if (targetNode instanceof CoreFolderPublicationNode) {
110            CoreFolderPublicationNode coreNode = (CoreFolderPublicationNode) targetNode;
111            targetDocModel = coreNode.getTargetDocumentModel();
112        } else {
113            targetDocModel = coreSession.getDocument(new PathRef(targetNode.getPath()));
114        }
115        NuxeoPrincipal principal = coreSession.getPrincipal();
116        DocumentPublisherUnrestricted runner = new DocumentPublisherUnrestricted(coreSession, doc.getRef(),
117                targetDocModel.getRef(), principal, null);
118        runner.runUnrestricted();
119        return runner.getPublishedDocument();
120    }
121
122    protected boolean isPublishedDocWaitingForPublication(DocumentModel doc, CoreSession session) {
123        return !lookupState.isPublished(doc, session);
124    }
125
126    protected boolean isValidator(DocumentModel document, NuxeoPrincipal principal) {
127        String[] validators = getValidatorsFor(document);
128        for (String s : validators) {
129            if (principal.getName().equals(s) || principal.isMemberOf(s)) {
130                return true;
131            }
132        }
133        return false;
134    }
135
136    protected void restrictPermission(DocumentModel newProxy, NuxeoPrincipal principal, CoreSession coreSession,
137            ACL acl) {
138        ChangePermissionUnrestricted permissionChanger = new ChangePermissionUnrestricted(coreSession, newProxy,
139                getValidatorsFor(newProxy), principal, ACL_NAME, acl);
140        permissionChanger.runUnrestricted();
141    }
142
143    protected void createTask(DocumentModel document, CoreSession session, NuxeoPrincipal principal) {
144        String[] actorIds = getValidatorsFor(document);
145        Map<String, String> variables = new HashMap<>();
146        variables.put(Task.TaskVariableName.needi18n.name(), "true");
147        variables.put(Task.TaskVariableName.taskType.name(), PUBLISH_TASK_TYPE);
148        variables.put(TaskService.VariableName.documentId.name(), document.getId());
149        variables.put(TaskService.VariableName.documentRepositoryName.name(), document.getRepositoryName());
150        variables.put(TaskService.VariableName.initiator.name(), principal.getName());
151
152        getTaskService().createTask(session, principal, document, TASK_NAME, Arrays.asList(actorIds), false, TASK_NAME,
153                null, null, variables, null);
154        DocumentEventContext ctx = new DocumentEventContext(session, principal, document);
155        ctx.setProperty(NotificationConstants.RECIPIENTS_KEY, actorIds);
156        getEventProducer().fireEvent(ctx.newEvent(TaskEventNames.WORKFLOW_TASK_START));
157    }
158
159    protected TaskService getTaskService() {
160        return Framework.getService(TaskService.class);
161    }
162
163    protected void removeExistingProxiesOnPreviousVersions(DocumentModel newProxy) {
164        new UnrestrictedSessionRunner(coreSession) {
165            @Override
166            public void run() {
167                DocumentModel sourceVersion = session.getSourceDocument(newProxy.getRef());
168                DocumentModel dm = session.getSourceDocument(sourceVersion.getRef());
169                DocumentModelList brothers = session.getProxies(dm.getRef(), newProxy.getParentRef());
170                if (brothers != null && brothers.size() > 1) {
171                    // we remove the brothers of the published document if any
172                    // the use case is:
173                    // v1 is published, v2 is waiting for publication and was just
174                    // validated
175                    // v1 is removed and v2 is now being published
176                    for (DocumentModel doc : brothers) {
177                        if (!doc.getId().equals(newProxy.getId())) {
178                            session.removeDocument(doc.getRef());
179                        }
180                    }
181                }
182            }
183        }.runUnrestricted();
184    }
185
186    @Override
187    public void validatorPublishDocument(PublishedDocument publishedDocument, String comment) {
188        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
189        NuxeoPrincipal principal = coreSession.getPrincipal();
190        removeExistingProxiesOnPreviousVersions(proxy);
191        removeACL(proxy, coreSession);
192        endTask(proxy, principal, coreSession, comment, PublishingEvent.documentPublicationApproved);
193        notifyEvent(PublishingEvent.documentPublicationApproved, proxy, coreSession);
194        notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
195        ((SimpleCorePublishedDocument) publishedDocument).setPending(false);
196    }
197
198    protected void removeACL(DocumentModel document, CoreSession coreSession) {
199        RemoveACLUnrestricted remover = new RemoveACLUnrestricted(coreSession, document, ACL_NAME, JBPM_ACL_NAME);
200        remover.runUnrestricted();
201    }
202
203    protected void endTask(DocumentModel document, NuxeoPrincipal currentUser, CoreSession session, String comment,
204            PublishingEvent event) {
205        List<Task> tis = getTaskService().getTaskInstances(document, currentUser, session);
206        String initiator = null;
207        for (Task task : tis) {
208            if (task.getName().equals(TASK_NAME)) {
209                initiator = task.getVariable(TaskService.VariableName.initiator.name());
210                task.end(session);
211                // make sure taskDoc is attached to prevent sending event with null session
212                DocumentModel taskDocument = task.getDocument();
213                if (!taskDocument.isAttached()) {
214                    taskDocument.attach(coreSession);
215                }
216                session.saveDocument(taskDocument);
217                break;
218            }
219        }
220        DocumentModel liveDoc = getLiveDocument(session, document);
221        Map<String, Serializable> properties = new HashMap<>();
222        if (initiator != null) {
223            properties.put(NotificationConstants.RECIPIENTS_KEY, new String[] { initiator });
224        }
225        notifyEvent(event.name(), properties, comment, null, liveDoc, session);
226    }
227
228    protected DocumentModel getLiveDocument(CoreSession session, DocumentModel proxy) {
229        GetsProxySourceDocumentsUnrestricted runner = new GetsProxySourceDocumentsUnrestricted(session, proxy);
230        runner.runUnrestricted();
231        return runner.liveDocument;
232    }
233
234    @Override
235    public void validatorRejectPublication(PublishedDocument publishedDocument, String comment) {
236        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
237        NuxeoPrincipal principal = coreSession.getPrincipal();
238        notifyEvent(PublishingEvent.documentPublicationRejected, proxy, coreSession);
239        endTask(proxy, principal, coreSession, comment, PublishingEvent.documentPublicationRejected);
240        removeProxy(proxy, coreSession);
241    }
242
243    protected void removeProxy(DocumentModel doc, CoreSession coreSession) {
244        DeleteDocumentUnrestricted deleter = new DeleteDocumentUnrestricted(coreSession, doc);
245        deleter.runUnrestricted();
246    }
247
248    @Override
249    public PublishedDocument wrapDocumentModel(DocumentModel doc) {
250        final SimpleCorePublishedDocument publishedDocument = (SimpleCorePublishedDocument) super.wrapDocumentModel(
251                doc);
252
253        new UnrestrictedSessionRunner(coreSession) {
254            @Override
255            public void run() {
256                if (!isPublished(publishedDocument, session)) {
257                    publishedDocument.setPending(true);
258                }
259
260            }
261        }.runUnrestricted();
262
263        return publishedDocument;
264    }
265
266    protected boolean isPublished(PublishedDocument publishedDocument, CoreSession session) {
267        // FIXME: should be cached
268        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
269        return lookupState.isPublished(proxy, session);
270    }
271
272    @Override
273    public boolean canManagePublishing(PublishedDocument publishedDocument) {
274        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
275        NuxeoPrincipal currentUser = coreSession.getPrincipal();
276        return proxy.isProxy() && hasValidationTask(proxy, currentUser);
277    }
278
279    protected boolean hasValidationTask(DocumentModel proxy, NuxeoPrincipal currentUser) {
280        assert currentUser != null;
281        List<Task> tasks = getTaskService().getTaskInstances(proxy, currentUser, coreSession);
282        for (Task task : tasks) {
283            if (task.getName().equals(TASK_NAME)) {
284                return true;
285            }
286        }
287        return false;
288    }
289
290    @Override
291    public boolean hasValidationTask(PublishedDocument publishedDocument) {
292        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
293        NuxeoPrincipal currentUser = coreSession.getPrincipal();
294        return hasValidationTask(proxy, currentUser);
295    }
296
297    private class GetsProxySourceDocumentsUnrestricted extends UnrestrictedSessionRunner {
298
299        public DocumentModel liveDocument;
300
301        private DocumentModel sourceDocument;
302
303        private final DocumentModel document;
304
305        public GetsProxySourceDocumentsUnrestricted(CoreSession session, DocumentModel proxy) {
306            super(session);
307            this.document = proxy;
308        }
309
310        @Override
311        public void run() {
312            sourceDocument = session.getDocument(new IdRef(document.getSourceId()));
313            liveDocument = session.getDocument(new IdRef(sourceDocument.getSourceId()));
314        }
315    }
316
317    /**
318     * @author arussel
319     */
320    protected class DocumentPublisherUnrestricted extends UnrestrictedSessionRunner {
321
322        protected PublishedDocument result;
323
324        protected DocumentRef docRef;
325
326        protected DocumentRef targetRef;
327
328        protected NuxeoPrincipal principal;
329
330        protected String comment = "";
331
332        public DocumentPublisherUnrestricted(CoreSession session, DocumentRef docRef, DocumentRef targetRef,
333                NuxeoPrincipal principal, String comment) {
334            super(session);
335            this.docRef = docRef;
336            this.targetRef = targetRef;
337            this.principal = principal;
338            this.comment = comment;
339        }
340
341        public PublishedDocument getPublishedDocument() {
342            return result;
343        }
344
345        @Override
346        public void run() {
347            DocumentModelList list = session.getProxies(docRef, targetRef);
348            DocumentModel proxy = null;
349            if (list.isEmpty()) {// first publication
350                proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
351                SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
352                session.save();
353                if (!isValidator(proxy, principal)) {
354                    notifyEvent(PublishingEvent.documentWaitingPublication, coreSession.getDocument(proxy.getRef()),
355                            coreSession);
356                    restrictPermission(proxy, principal, session, null);
357                    createTask(proxy, coreSession, principal);
358                    publishedDocument.setPending(true);
359                } else {
360                    notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
361                }
362                result = publishedDocument;
363            } else if (list.size() == 1) {
364                // one doc is already published or waiting for publication
365                if (isPublishedDocWaitingForPublication(list.get(0), session)) {
366                    proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
367                    if (!isValidator(proxy, principal)) {
368                        // we're getting the old proxy acl
369                        ACL acl = list.get(0).getACP().getACL(ACL_NAME);
370                        acl.add(0, new ACE(principal.getName(), SecurityConstants.READ, true));
371                        ACP acp = proxy.getACP();
372                        acp.addACL(acl);
373                        session.setACP(proxy.getRef(), acp, true);
374                        session.save();
375                        SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
376                        publishedDocument.setPending(true);
377                        result = publishedDocument;
378                    } else {
379                        endTask(proxy, principal, coreSession, "", PublishingEvent.documentPublicationApproved);
380                        notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
381                        ACP acp = proxy.getACP();
382                        acp.removeACL(ACL_NAME);
383                        session.setACP(proxy.getRef(), acp, true);
384                        session.save();
385                        result = new SimpleCorePublishedDocument(proxy);
386                    }
387                } else {
388                    if (!isValidator(list.get(0), principal)) {
389                        proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef),
390                                false);
391                        // save needed to have the proxy visible from other
392                        // sessions in non-JCA mode
393                        session.save();
394                        SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
395                        notifyEvent(PublishingEvent.documentWaitingPublication, proxy, coreSession);
396                        restrictPermission(proxy, principal, coreSession, null);
397                        session.save(); // process invalidations (non-JCA)
398                        createTask(proxy, coreSession, principal);
399                        publishedDocument.setPending(true);
400                        result = publishedDocument;
401                    } else {
402                        proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
403                        // save needed to have the proxy visible from other
404                        // sessions in non-JCA mode
405                        session.save();
406                        notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
407                        SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
408                        result = publishedDocument;
409                    }
410                }
411            } else if (list.size() == 2) {
412                DocumentModel waitingForPublicationDoc = null;
413                session.save(); // process invalidations (non-JCA)
414                for (DocumentModel dm : list) {
415                    if (session.getACP(dm.getRef()).getACL(ACL_NAME) != null) {
416                        waitingForPublicationDoc = dm;
417                    }
418                }
419                if (waitingForPublicationDoc == null) {
420                    throw new NuxeoException("No proxy with correct ACL");
421                }
422                if (!isValidator(waitingForPublicationDoc, principal)) {
423                    // we're getting the old proxy acl
424                    ACL acl = waitingForPublicationDoc.getACP().getACL(ACL_NAME);
425                    acl.add(0, new ACE(principal.getName(), SecurityConstants.READ, true));
426                    // remove publishedDoc
427                    ACP acp = session.getACP(waitingForPublicationDoc.getRef());
428                    acp.addACL(acl);
429                    session.setACP(waitingForPublicationDoc.getRef(), acp, true);
430                    SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(
431                            waitingForPublicationDoc);
432                    publishedDocument.setPending(true);
433                    result = publishedDocument;
434                } else {
435                    endTask(waitingForPublicationDoc, principal, coreSession, comment,
436                            PublishingEvent.documentPublicationApproved);
437                    session.removeDocument(waitingForPublicationDoc.getRef());
438                    proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
439                    notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
440                    SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
441                    result = publishedDocument;
442                }
443            }
444            if (proxy != null) {
445                proxy.detach(true);
446            }
447        }
448    }
449
450}