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}