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) {
105        String cleanLabel = cleanLabel(label, true, false);
106        String username = cleanUsername(session.getPrincipal().getName());
107        CoreInstance.doPrivileged(session, (CoreSession s) -> doTag(s, docId, cleanLabel, username));
108        fireUpdateEvent(session, docId);
109    }
110
111    @Override
112    public void tag(CoreSession session, String docId, String label, String username) {
113        tag(session, docId, label);
114    }
115
116    @Override
117    public void untag(CoreSession session, String docId, String label) {
118        // There's two allowed cases here:
119        // - document doesn't exist, we're here after documentRemoved event
120        // - regular case: check if user can remove this tag on document
121        if (!session.exists(new IdRef(docId)) || canUntag(session, docId, label)) {
122            Set<String> tags = getTags(session, docId);
123            // if the document has the exact given tag label, remove it
124            // otherwise remove the cleaned label
125            String l = tags.contains(label) ? label : cleanLabel(label, true, false);
126            CoreInstance.doPrivileged(session, (CoreSession s) -> doUntag(s, docId, l));
127            if (label != null) {
128                fireUpdateEvent(session, docId);
129            }
130        } else {
131            String principalName = session.getPrincipal().getName();
132            throw new DocumentSecurityException("User '" + principalName + "' is not allowed to remove tag '" + label
133                    + "' on document '" + docId + "'");
134        }
135    }
136
137    @Override
138    public void untag(CoreSession session, String docId, String label, String username) {
139        untag(session, docId, label);
140    }
141
142    @Override
143    public boolean canUntag(CoreSession session, String docId, String label) {
144        return session.hasPermission(new IdRef(docId), SecurityConstants.WRITE);
145    }
146
147    @Override
148    public Set<String> getTags(CoreSession session, String docId) {
149        return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTags(s, docId));
150    }
151
152    @Override
153    public List<Tag> getDocumentTags(CoreSession session, String docId, String username) {
154        return getTags(session, docId).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList());
155    }
156
157    @Override
158    public List<Tag> getDocumentTags(CoreSession session, String docId, String username, boolean useCore) {
159        return getTags(session, docId).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList());
160    }
161
162    @Override
163    public void removeTags(CoreSession session, String docId) {
164        untag(session, docId, null);
165    }
166
167    @Override
168    public void copyTags(CoreSession session, String srcDocId, String dstDocId) {
169        copyTags(session, srcDocId, dstDocId, false);
170    }
171
172    protected void copyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags) {
173        CoreInstance.doPrivileged(session, (CoreSession s) -> doCopyTags(s, srcDocId, dstDocId, removeExistingTags));
174    }
175
176    @Override
177    public void replaceTags(CoreSession session, String srcDocId, String dstDocId) {
178        copyTags(session, srcDocId, dstDocId, true);
179    }
180
181    @Override
182    public List<String> getTagDocumentIds(CoreSession session, String label) {
183        String cleanLabel = cleanLabel(label, true, false);
184        return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagDocumentIds(s, cleanLabel));
185    }
186
187    @Override
188    public List<String> getTagDocumentIds(CoreSession session, String label, String username) {
189        return getTagDocumentIds(session, label);
190    }
191
192    @Override
193    public Set<String> getSuggestions(CoreSession session, String label) {
194        label = cleanLabel(label, true, true);
195        if (!isTagSanitizationEnabled()) {
196            // Escape character for LIKE statement
197            label = label.replace("\\", "\\\\");
198        }
199        if (!label.contains("%")) {
200            label += "%";
201        }
202        // effectively final for lambda
203        String l = label;
204        return CoreInstance.doPrivileged(session, (CoreSession s) -> doGetTagSuggestions(s, l));
205    }
206
207    @Override
208    public List<Tag> getSuggestions(CoreSession session, String label, String username) {
209        return getSuggestions(session, label).stream().map(t -> new Tag(t, 0)).collect(Collectors.toList());
210    }
211
212    protected boolean isTagSanitizationEnabled() {
213        return !hasFeature(TAGS_BELONG_TO_DOCUMENT)
214                || Framework.getService(ConfigurationService.class).isBooleanTrue(TAG_SANITIZATION_PROP);
215    }
216
217    public abstract void doTag(CoreSession session, String docId, String label, String username);
218
219    public abstract void doUntag(CoreSession session, String docId, String label);
220
221    public abstract Set<String> doGetTags(CoreSession session, String docId);
222
223    public abstract void doCopyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags);
224
225    public abstract List<String> doGetTagDocumentIds(CoreSession session, String label);
226
227    public abstract Set<String> doGetTagSuggestions(CoreSession session, String label);
228
229    protected String cleanLabel(String label, boolean allowEmpty, boolean allowPercent) {
230        if (label == null) {
231            if (allowEmpty) {
232                return null;
233            }
234            throw new NuxeoException("Invalid empty tag");
235        }
236        label = label.toLowerCase(); // lowercase
237        if (isTagSanitizationEnabled()) {
238            label = label.replace(" ", ""); // no spaces
239            label = label.replace("/", ""); // no slash
240            label = label.replace("\\", ""); // dubious char
241            label = label.replace("'", ""); // dubious char
242        }
243        if (!allowPercent) {
244            label = label.replace("%", ""); // dubious char
245        }
246        if (label.length() == 0) {
247            throw new NuxeoException("Invalid empty tag");
248        }
249        return label;
250    }
251
252    protected static String cleanUsername(String username) {
253        return username == null ? null : username.replace("'", "");
254    }
255
256    /**
257     * Returns results from calls to {@link CoreSession#queryAndFetch(String, String, Object...)} using page providers.
258     *
259     * @since 6.0
260     */
261    @SuppressWarnings("unchecked")
262    protected static List<Map<String, Serializable>> getItems(String pageProviderName, CoreSession session,
263            Object... params) {
264        PageProviderService ppService = Framework.getService(PageProviderService.class);
265        if (ppService == null) {
266            throw new NuxeoException("Missing PageProvider service");
267        }
268        Map<String, Serializable> props = new HashMap<>();
269        // first retrieve potential props from definition
270        PageProviderDefinition def = ppService.getPageProviderDefinition(pageProviderName);
271        if (def != null) {
272            Map<String, String> defProps = def.getProperties();
273            if (defProps != null) {
274                props.putAll(defProps);
275            }
276        }
277        props.put(CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY, (Serializable) session);
278        PageProvider<Map<String, Serializable>> pp = (PageProvider<Map<String, Serializable>>) ppService.getPageProvider(
279                pageProviderName, null, null, null, props, params);
280        if (pp == null) {
281            throw new NuxeoException("Page provider not found: " + pageProviderName);
282        }
283        return pp.getCurrentPage();
284    }
285
286    protected void fireUpdateEvent(CoreSession session, String docId) {
287        DocumentRef documentRef = new IdRef(docId);
288        if (session.exists(documentRef)) {
289            DocumentModel documentModel = session.getDocument(documentRef);
290            DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), documentModel);
291            Event event = ctx.newEvent(DocumentEventTypes.DOCUMENT_TAG_UPDATED);
292            Framework.getService(EventService.class).fireEvent(event);
293        }
294    }
295
296}