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}