001/*
002 *  Copyright 2010 Anyware Services
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
017package org.ametys.cms.transformation;
018
019import java.net.CookieHandler;
020import java.net.CookieManager;
021import java.net.CookiePolicy;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Optional;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.logger.AbstractLogEnabled;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031
032import org.ametys.core.util.HttpUrlUtils;
033import org.ametys.core.util.HttpUrlUtils.HttpCheck;
034import org.ametys.core.util.HttpUrlUtils.HttpCheckReport;
035import org.ametys.core.version.Version;
036import org.ametys.core.version.VersionsHandler;
037import org.ametys.runtime.i18n.I18nizableText;
038
039/**
040 * Consistency checker component.
041 * Checks the state of the links extracted as outgoing references for a content.
042 */
043public class ConsistencyChecker extends AbstractLogEnabled implements Serviceable, Component
044{
045    /** Avalon role */
046    public static final String ROLE = ConsistencyChecker.class.getName();
047    
048    /** Constants for the special external type for outgoing references */
049    public static final String CONSISTENCY_EXTERNAL_REFERENCE_TYPE = "__external";
050    
051    private static final int __HTTP_TIMEOUT = 10_000;
052    
053    /** The state of the check */
054    public enum CHECK
055    {
056        /** If the check was all right */
057        SUCCESS,
058        /** Server Error. */
059        SERVER_ERROR,
060        /** Not Found.*/
061        NOT_FOUND,
062        /** Unauthorized. */
063        UNAUTHORIZED,
064        /** If the check may be too long */
065        UNKNOWN;
066        
067        /**
068         * Convert a {@link HttpCheck} value to a {@link CHECK} value
069         * @param httpCheck the HTTP check
070         * @return the CHECK value
071         */
072        public static CHECK from(HttpCheck httpCheck)
073        {
074            switch (httpCheck)
075            {
076                case SUCCESS:
077                    return CHECK.SUCCESS;
078                    
079                case TIMEOUT:
080                case REDIRECT:
081                    return CHECK.UNKNOWN;
082                  
083                case NOT_FOUND:
084                    return CHECK.NOT_FOUND;
085                    
086                case SECURITY_LEVEL_ERROR:
087                case SERVER_ERROR:
088                default:
089                    return CHECK.SERVER_ERROR;
090            }
091        }
092    }
093    
094    /** The ametys uri resolver */
095    protected URIResolverExtensionPoint _uriResolverEP;
096    /** The version handler */
097    protected VersionsHandler _versionsHandler;
098    /** The URL utils */
099    protected HttpUrlUtils _urlUtils;
100    
101    @Override
102    public void service(ServiceManager manager) throws ServiceException
103    {
104        _uriResolverEP = (URIResolverExtensionPoint) manager.lookup(URIResolverExtensionPoint.ROLE);
105        _versionsHandler = (VersionsHandler) manager.lookup(VersionsHandler.ROLE);
106        _urlUtils = (HttpUrlUtils) manager.lookup(HttpUrlUtils.ROLE);
107    }
108    
109    /**
110     * Check the consistency of a reference
111     * @param referenceType The type of the reference to test (can be 'attachment', 'explorer', 'metadata', '__external', etc...)
112     * @param referenceValue The value of the reference to test
113     * @param contentId The id of the content containing the reference
114     * @param dataPath The path of the data containing the reference
115     * @param quickTest true to make a quick test, that means that long tests will return UNKNOWN immediately.
116     * @return CHECK enum value
117     */
118    public CheckReport checkConsistency(String referenceType, String referenceValue, String contentId, String dataPath, boolean quickTest)
119    {
120        if (getLogger().isDebugEnabled())
121        {
122            getLogger().debug("Checking consistency for URI of type '" + referenceType + "' with uri '" + referenceValue + "'");
123        }
124        
125        URIResolver resolver = _uriResolverEP.getResolverForType(referenceType);
126        if (resolver == null)
127        {
128            // No resolver for external references
129            if (CONSISTENCY_EXTERNAL_REFERENCE_TYPE.equals(referenceType))
130            {
131                if (referenceValue.startsWith("http:") || referenceValue.startsWith("https:"))
132                {
133                    return _checkHTTPLink(referenceValue, quickTest);
134                }
135                else if (referenceValue.startsWith("mailto:"))
136                {
137                    return new CheckReport(CHECK.SUCCESS);
138                }
139                else if (referenceValue.startsWith("tel:"))
140                {
141                    return new CheckReport(CHECK.SUCCESS);
142                }
143            }
144            
145            if (getLogger().isDebugEnabled())
146            {
147                getLogger().debug("Cannot test external link '" + referenceValue + "'");
148            }
149            
150            return new CheckReport(CHECK.UNKNOWN);
151        }
152        else
153        {
154            String uri = _getURIFromAttachment(referenceType, referenceValue, contentId, dataPath);
155            return new CheckReport(resolver.checkLink(uri, quickTest));
156        }
157    }
158    
159    /**
160     * Get the label of a reference.
161     * @param referenceType The type of the reference to test (can be 'attachment', 'explorer', 'metadata', '__external', etc...)
162     * @param referenceValue The value of the reference to test
163     * @param contentId The id of the content containing the reference
164     * @param dataPath The path of the data containing the reference
165     * @return the element label.
166     */
167    public I18nizableText getLabel(String referenceType, String referenceValue, String contentId, String dataPath)
168    {
169        URIResolver resolver = _uriResolverEP.getResolverForType(referenceType);
170        if (resolver == null)
171        {
172            return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_EXTERNAL_LABEL", Collections.singletonList(referenceValue));
173        }
174        else
175        {
176            String uri = _getURIFromAttachment(referenceType, referenceValue, contentId, dataPath);
177            return resolver.getLabel(uri);
178        }
179    }
180    
181    private String _getURIFromAttachment(String referenceType, String referenceValue, String contentId, String dataPath)
182    {
183        return "local".equals(referenceType) ? contentId + "@" + dataPath + ";" + referenceValue : referenceValue;
184    }
185    
186    /**
187     * Test an http link
188     * @param linkValue The http url to test
189     * @param shortTest true to make a short test (with short timeout)
190     * @return The state. UNKNOWN if test cannot be done fully
191     */
192    protected CheckReport _checkHTTPLink(String linkValue, boolean shortTest)
193    {
194        try
195        {
196            CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
197            
198            String httpUrl = linkValue.replaceAll(" ", "%20");
199            String userAgent = "Ametys/" + _getCMSVersion();
200            int timeout = shortTest ? 1000 : __HTTP_TIMEOUT;
201            int readTimeout = shortTest ? 2000 : __HTTP_TIMEOUT;
202            HttpCheckReport checkReport = HttpUrlUtils.checkHttpUrl(httpUrl, userAgent, null, timeout, readTimeout, true);
203            
204            return new CheckReport(CHECK.from(checkReport.status()), checkReport.message());
205        }
206        finally
207        {
208            ((CookieManager) CookieHandler.getDefault()).getCookieStore().removeAll();
209        }
210    }
211    
212    private String _getCMSVersion()
213    {
214        Collection<Version> versions = _versionsHandler.getVersions();
215        for (Version version : versions)
216        {
217            if (version.getName().equals("CMS"))
218            {
219                return version.getVersion();
220            }
221        }
222        return null;
223    }
224    
225    /**
226     * Represent the result of a consistency check
227     * @param status the result status
228     * @param message a potential message
229     */
230    public record CheckReport(CHECK status, Optional<String> message) {
231        /**
232         * Create a report with no message
233         * @param result the result status
234         */
235        public CheckReport(CHECK result)
236        {
237            this(result, Optional.empty());
238        }
239    }
240}