001/*
002 * (C) Copyright 2017-2018 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.core.filter;
022
023import java.io.Serializable;
024import java.util.List;
025import java.util.stream.Collectors;
026
027import org.apache.commons.text.StringEscapeUtils;
028import org.nuxeo.ecm.core.api.DataModel;
029import org.nuxeo.ecm.core.api.DocumentModel;
030import org.nuxeo.ecm.core.api.impl.DataModelImpl;
031import org.nuxeo.ecm.core.api.model.DocumentPart;
032import org.nuxeo.ecm.core.api.model.Property;
033import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
034import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
035import org.nuxeo.ecm.core.api.model.impl.ListProperty;
036import org.nuxeo.ecm.core.api.model.impl.primitives.StringProperty;
037import org.nuxeo.runtime.model.ComponentInstance;
038import org.nuxeo.runtime.model.DefaultComponent;
039
040import com.google.common.base.CharMatcher;
041
042/**
043 * @since 9.1
044 */
045public class CharacterFilteringServiceImpl extends DefaultComponent implements CharacterFilteringService {
046
047    public static final String FILTERING_XP = "filtering";
048
049    protected CharacterFilteringServiceDescriptor desc;
050
051    protected CharMatcher charsToRemove;
052
053    @Override
054    public void registerContribution(Object contrib, String point, ComponentInstance contributor) {
055        if (FILTERING_XP.equals(point)) {
056
057            desc = (CharacterFilteringServiceDescriptor) contrib;
058
059            CharMatcher charsToPreserve = CharMatcher.anyOf("\r\n\t");
060            CharMatcher allButPreserved = charsToPreserve.negate();
061            charsToRemove = CharMatcher.javaIsoControl().and(allButPreserved);
062            charsToRemove = charsToRemove.or(CharMatcher.invisible().and(CharMatcher.whitespace().negate()));
063
064            List<String> additionalChars = desc.getDisallowedChars();
065            if (additionalChars != null && !additionalChars.isEmpty()) {
066                String otherCharsToRemove = additionalChars.stream().map(StringEscapeUtils::unescapeJava).collect(
067                        Collectors.joining());
068                charsToRemove = charsToRemove.or(CharMatcher.anyOf(otherCharsToRemove));
069            }
070        } else {
071            throw new RuntimeException("Unknown extension point: " + point);
072        }
073    }
074
075    @Override
076    public String filter(String value) {
077        return charsToRemove.removeFrom(value);
078    }
079
080    @Override
081    public void filter(DocumentModel docModel) {
082        if (desc.isEnabled()) {
083            // check only loaded datamodels to find the dirty ones
084            for (DataModel dm : docModel.getDataModelsCollection()) { // only loaded
085                if (!dm.isDirty()) {
086                    continue;
087                }
088                DocumentPart part = ((DataModelImpl) dm).getDocumentPart();
089                for (Property prop : part.getChildren()) {
090                    filterProperty(prop, docModel);
091                }
092            }
093        }
094    }
095
096    private void filterProperty(Property prop, DocumentModel docModel) {
097        if (!prop.isDirty()) {
098            return;
099        }
100        if (prop instanceof StringProperty) {
101            String p = (String) prop.getValue();
102            if (p != null && charsToRemove.matchesAnyOf(p)) {
103                String filteredProp = filter(p);
104                docModel.setPropertyValue(prop.getXPath(), filteredProp);
105            }
106        } else if (prop instanceof ArrayProperty) {
107            Serializable value = prop.getValue();
108            if (value instanceof Object[]) {
109                Object[] arrayProp = (Object[]) value;
110                boolean modified = false;
111                for (int i = 0; i < arrayProp.length; i++) {
112                    if (arrayProp[i] instanceof String) {
113                        String p = (String) arrayProp[i];
114                        if (charsToRemove.matchesAnyOf(p)) {
115                            arrayProp[i] = filter(p);
116                            modified = true;
117                        }
118                    }
119                }
120                if (modified) {
121                    docModel.setPropertyValue(prop.getXPath(), arrayProp);
122                }
123            }
124        } else if (prop instanceof ComplexProperty) {
125            ComplexProperty complexProp = (ComplexProperty) prop;
126            for (Property subProp : complexProp.getChildren()) {
127                filterProperty(subProp, docModel);
128            }
129        } else if (prop instanceof ListProperty) {
130            ListProperty listProp = (ListProperty) prop;
131            for (Property p : listProp) {
132                filterProperty(p, docModel);
133            }
134        }
135    }
136}