001/*
002 * (C) Copyright 2017 Nuxeo (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 *     Funsho David
018 *
019 */
020
021package org.nuxeo.ecm.platform.tag;
022
023import static org.nuxeo.ecm.platform.tag.TagService.Feature.TAGS_BELONG_TO_DOCUMENT;
024
025import java.io.Serializable;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.stream.Collectors;
031
032import org.nuxeo.ecm.core.api.CoreInstance;
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.DocumentRef;
036import org.nuxeo.ecm.core.api.DocumentSecurityException;
037import org.nuxeo.ecm.core.api.IdRef;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
040import org.nuxeo.ecm.core.api.security.SecurityConstants;
041import org.nuxeo.ecm.core.event.Event;
042import org.nuxeo.ecm.core.event.EventService;
043import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
044import org.nuxeo.ecm.platform.query.api.PageProvider;
045import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
046import org.nuxeo.ecm.platform.query.api.PageProviderService;
047import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider;
048import org.nuxeo.runtime.api.Framework;
049import org.nuxeo.runtime.services.config.ConfigurationService;
050
051/**
052 * @since 9.3
053 */
054public abstract class AbstractTagService implements TagService {
055
056    public static final String TAG_SANITIZATION_PROP = "nuxeo.tag.sanitization.enabled";
057
058    protected enum PAGE_PROVIDERS {
059        GET_DOCUMENT_IDS_FOR_FACETED_TAG,
060        //
061        GET_DOCUMENT_IDS_FOR_TAG,
062        //
063        GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER,
064        //
065        GET_FIRST_TAGGING_FOR_DOC_AND_TAG,
066        //
067        GET_TAGS_FOR_DOCUMENT,
068        // core version: should keep on querying VCS
069        GET_TAGS_FOR_DOCUMENT_CORE,
070        //
071        GET_DOCUMENTS_FOR_TAG,
072        //
073        GET_TAGS_FOR_DOCUMENT_AND_USER,
074        // core version: should keep on querying VCS
075        GET_TAGS_FOR_DOCUMENT_AND_USER_CORE,
076        //
077        GET_DOCUMENTS_FOR_TAG_AND_USER,
078        //
079        GET_TAGS_TO_COPY_FOR_DOCUMENT,
080        //
081        GET_FACETED_TAG_SUGGESTIONS,
082        //
083        GET_TAG_SUGGESTIONS,
084        //
085        GET_TAG_SUGGESTIONS_FOR_USER,
086        //
087        GET_TAGGED_DOCUMENTS_UNDER,
088        //
089        GET_ALL_TAGS,
090        //
091        GET_ALL_TAGS_FOR_USER,
092        //
093        GET_TAGS_FOR_DOCUMENTS,
094        //
095        GET_TAGS_FOR_DOCUMENTS_AND_USER,
096    }
097
098    @Override
099    public boolean isEnabled() {
100        return true;
101    }
102
103    @Override
104    public void tag(CoreSession session, String docId, String label) throws DocumentSecurityException {
105        String cleanLabel = cleanLabel(label, true, false);
106        String username = cleanUsername(session.getPrincipal().getName());
107        CoreInstance.doPrivileged(session, s -> {
108            doTag(s, docId, cleanLabel, username);
109        });
110        fireUpdateEvent(session, docId);
111    }
112
113    @Override
114    public void tag(CoreSession session, String docId, String label, String username) {
115        tag(session, docId, label);
116    }
117
118    @Override
119    public void untag(CoreSession session, String docId, String label)
120            throws DocumentSecurityException {
121        // There's two allowed cases here:
122        // - document doesn't exist, we're here after documentRemoved event
123        // - regular case: check if user can remove this tag on document
124        if (!session.exists(new IdRef(docId)) || canUntag(session, docId, label)) {
125            String cleanLabel = cleanLabel(label, true, false);
126            CoreInstance.doPrivileged(session, s -> {
127                doUntag(s, docId, cleanLabel);
128            });
129            if (label != null) {
130                fireUpdateEvent(session, docId);
131            }
132        } else {
133            String principalName = session.getPrincipal().getName();
134            throw new DocumentSecurityException("User '" + principalName + "' is not allowed to remove tag '" + label
135                    + "' on document '" + docId + "'");
136        }
137    }
138
139    @Override
140    public void untag(CoreSession session, String docId, String label, String username)
141            throws DocumentSecurityException {
142        untag(session, docId, label);
143    }
144
145    @Override
146    public boolean canUntag(CoreSession session, String docId, String label) {
147        return session.hasPermission(new IdRef(docId), SecurityConstants.WRITE);
148    }
149
150    @Override
151    public Set<String> getTags(CoreSession session, String docId) {
152        return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTags(s, docId));
153    }
154
155    @Override
156    public List<Tag> getDocumentTags(CoreSession session, String docId, String username) {
157        return getTags(session, docId).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList());
158    }
159
160    @Override
161    public List<Tag> getDocumentTags(CoreSession session, String docId, String username, boolean useCore) {
162        return getTags(session, docId).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList());
163    }
164
165    @Override
166    public void removeTags(CoreSession session, String docId) {
167        untag(session, docId, null);
168    }
169
170    @Override
171    public void copyTags(CoreSession session, String srcDocId, String dstDocId) {
172        copyTags(session, srcDocId, dstDocId, false);
173    }
174
175    protected void copyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags) {
176        CoreInstance.doPrivileged(session, s -> {
177            doCopyTags(s, srcDocId, dstDocId, removeExistingTags);
178        });
179    }
180
181    @Override
182    public void replaceTags(CoreSession session, String srcDocId, String dstDocId) {
183        copyTags(session, srcDocId, dstDocId, true);
184    }
185
186    @Override
187    public List<String> getTagDocumentIds(CoreSession session, String label) {
188        String cleanLabel = cleanLabel(label, true, false);
189        return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagDocumentIds(s, cleanLabel));
190    }
191
192    @Override
193    public List<String> getTagDocumentIds(CoreSession session, String label, String username) {
194        return getTagDocumentIds(session, label);
195    }
196
197    @Override
198    public Set<String> getSuggestions(CoreSession session, String label) {
199        label = cleanLabel(label, true, true);
200        if (!isTagSanitizationEnabled()) {
201            // Escape character for LIKE statement
202            label = label.replace("\\", "\\\\");
203        }
204        if (!label.contains("%")) {
205            label += "%";
206        }
207        // effectively final for lambda
208        String l = label;
209        return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagSuggestions(s, l));
210    }
211
212    @Override
213    public List<Tag> getSuggestions(CoreSession session, String label, String username) {
214        return getSuggestions(session, label).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList());
215    }
216
217    protected boolean isTagSanitizationEnabled() {
218        return !hasFeature(TAGS_BELONG_TO_DOCUMENT)
219                || Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(TAG_SANITIZATION_PROP);
220    }
221
222    public abstract void doTag(CoreSession session, String docId, String label, String username);
223
224    public abstract void doUntag(CoreSession session, String docId, String label);
225
226    public abstract Set<String> doGetTags(CoreSession session, String docId);
227
228    public abstract void doCopyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags);
229
230    public abstract List<String> doGetTagDocumentIds(CoreSession session, String label);
231
232    public abstract Set<String> doGetTagSuggestions(CoreSession session, String label);
233
234    protected String cleanLabel(String label, boolean allowEmpty, boolean allowPercent) {
235        if (label == null) {
236            if (allowEmpty) {
237                return null;
238            }
239            throw new NuxeoException("Invalid empty tag");
240        }
241        label = label.toLowerCase(); // lowercase
242        if (isTagSanitizationEnabled()) {
243            label = label.replace(" ", ""); // no spaces
244            label = label.replace("/", ""); // no slash
245            label = label.replace("\\", ""); // dubious char
246            label = label.replace("'", ""); // dubious char
247        }
248        if (!allowPercent) {
249            label = label.replace("%", ""); // dubious char
250        }
251        if (label.length() == 0) {
252            throw new NuxeoException("Invalid empty tag");
253        }
254        return label;
255    }
256
257    protected static String cleanUsername(String username) {
258        return username == null ? null : username.replace("'", "");
259    }
260
261    /**
262     * Returns results from calls to {@link CoreSession#queryAndFetch(String, String, Object...)} using page providers.
263     *
264     * @since 6.0
265     */
266    @SuppressWarnings("unchecked")
267    protected static List<Map<String, Serializable>> getItems(String pageProviderName, CoreSession session,
268            Object... params) {
269        PageProviderService ppService = Framework.getService(PageProviderService.class);
270        if (ppService == null) {
271            throw new RuntimeException("Missing PageProvider service");
272        }
273        Map<String, Serializable> props = new HashMap<>();
274        // first retrieve potential props from definition
275        PageProviderDefinition def = ppService.getPageProviderDefinition(pageProviderName);
276        if (def != null) {
277            Map<String, String> defProps = def.getProperties();
278            if (defProps != null) {
279                props.putAll(defProps);
280            }
281        }
282        props.put(CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY, (Serializable) session);
283        PageProvider<Map<String, Serializable>> pp = (PageProvider<Map<String, Serializable>>) ppService.getPageProvider(
284                pageProviderName, null, null, null, props, params);
285        if (pp == null) {
286            throw new NuxeoException("Page provider not found: " + pageProviderName);
287        }
288        return pp.getCurrentPage();
289    }
290
291    protected void fireUpdateEvent(CoreSession session, String docId) {
292        DocumentRef documentRef = new IdRef(docId);
293        if (session.exists(documentRef)) {
294            DocumentModel documentModel = session.getDocument(documentRef);
295            DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), documentModel);
296            Event event = ctx.newEvent(DocumentEventTypes.DOCUMENT_TAG_UPDATED);
297            Framework.getService(EventService.class).fireEvent(event);
298        }
299    }
300
301}