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.io.IOException;
020import java.net.CookieHandler;
021import java.net.CookieManager;
022import java.net.CookiePolicy;
023import java.net.HttpURLConnection;
024import java.net.SocketTimeoutException;
025import java.net.URL;
026import java.net.UnknownHostException;
027import java.util.Collection;
028import java.util.Collections;
029
030import javax.net.ssl.SSLHandshakeException;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.logger.AbstractLogEnabled;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037
038import org.ametys.core.version.Version;
039import org.ametys.core.version.VersionsHandler;
040import org.ametys.runtime.i18n.I18nizableText;
041
042/**
043 * Consistency checker component.
044 * Checks the state of the links extracted as outgoing references for a content.
045 */
046public class ConsistencyChecker extends AbstractLogEnabled implements Serviceable, Component
047{
048    /** Avalon role */
049    public static final String ROLE = ConsistencyChecker.class.getName();
050    
051    /** Constants for the special external type for outgoing references */
052    public static final String CONSISTENCY_EXTERNAL_REFERENCE_TYPE = "__external";
053    
054    /** The state of the check */
055    public enum CHECK
056    {
057        /** If the check was all right */
058        SUCCESS,
059        /** Server Error. */
060        SERVER_ERROR,
061        /** Not Found.*/
062        NOT_FOUND,
063        /** Unauthorized. */
064        UNAUTHORIZED,
065        /** If the check may be too long */
066        UNKNOWN
067    }
068    
069    /** The ametys uri resolver */
070    protected URIResolverExtensionPoint _uriResolverEP;
071    /** The version handler */
072    protected VersionsHandler _versionsHandler;
073    
074    @Override
075    public void service(ServiceManager manager) throws ServiceException
076    {
077        _uriResolverEP = (URIResolverExtensionPoint) manager.lookup(URIResolverExtensionPoint.ROLE);
078        _versionsHandler = (VersionsHandler) manager.lookup(VersionsHandler.ROLE);
079    }
080    
081    /**
082     * Check the consistency of a reference
083     * @param referenceType The type of the reference to test (can be 'attachment', 'explorer', 'metadata', '__external', etc...)
084     * @param referenceValue The value of the reference to test
085     * @param shortTest true to make a short test, that means that long tests will return UNKNOWN immediately. If false, you will always have a SUCCESS or a FAILURE.
086     * @return CHECK enum value
087     */
088    public CHECK checkConsistency(String referenceType, String referenceValue, boolean shortTest)
089    {
090        if (getLogger().isDebugEnabled())
091        {
092            getLogger().debug("Checking consistency for URI of type '" + referenceType + "' with uri '" + referenceValue + "'");
093        }
094        
095        URIResolver resolver = _uriResolverEP.getResolverForType(referenceType);
096        if (resolver == null)
097        {
098            // No resolver for external references
099            if (CONSISTENCY_EXTERNAL_REFERENCE_TYPE.equals(referenceType))
100            {
101                if (referenceValue.startsWith("http:") || referenceValue.startsWith("https:"))
102                {
103                    return _checkHTTPLink(referenceValue, shortTest);
104                }
105                else if (referenceValue.startsWith("mailto:"))
106                {
107                    return CHECK.SUCCESS;
108                }
109            }
110            
111            if (getLogger().isDebugEnabled())
112            {
113                getLogger().debug("Cannot test external link '" + referenceValue + "'");
114            }
115            
116            return CHECK.UNKNOWN;
117        }
118        else
119        {
120            return resolver.checkLink(referenceValue, shortTest);
121        }
122    }
123    
124    /**
125     * Get the label of a reference.
126     * @param referenceType The type of the reference to test (can be 'attachment', 'explorer', 'metadata', '__external', etc...)
127     * @param referenceValue The value of the reference to test
128     * @return the element label.
129     */
130    public I18nizableText getLabel(String referenceType, String referenceValue)
131    {
132        URIResolver resolver = _uriResolverEP.getResolverForType(referenceType);
133        if (resolver == null)
134        {
135            return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_EXTERNAL_LABEL", Collections.singletonList(referenceValue));
136        }
137        else
138        {
139            return resolver.getLabel(referenceValue);
140        }
141    }
142    
143    /**
144     * Test an http link
145     * @param linkValue The http url to test
146     * @param shortTest true to make a short test (with short timeout)
147     * @return The state. UNKNOWN if test cannot be done fully
148     */
149    protected CHECK _checkHTTPLink(String linkValue, boolean shortTest)
150    {
151        try
152        {
153            CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
154            
155            URL url = new URL(linkValue.replaceAll(" ", "%20"));
156            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
157            if (shortTest)
158            {
159                connection.setConnectTimeout(1000);
160                connection.setReadTimeout(2000);
161            }
162            connection.setInstanceFollowRedirects(true);
163            connection.addRequestProperty("User-Agent", "Ametys/" + _getCMSVersion());
164                     
165            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK)
166            {
167                if (getLogger().isDebugEnabled())
168                {
169                    getLogger().debug("Check consistency OK for URI of external link with uri '" + linkValue + "'");
170                }
171                return CHECK.SUCCESS;
172            }
173            else if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP /* 302 */ || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM /* 301 */)
174            {
175                // redirection changing the protocol
176                return CHECK.UNKNOWN;
177            }
178            else
179            {
180                if (getLogger().isDebugEnabled())
181                {
182                    getLogger().debug("Check consistency " + connection.getResponseCode() + " for URI of external link with uri '" + linkValue + "'");
183                }
184                
185                int responseCode = connection.getResponseCode();
186                
187                switch (responseCode)
188                {
189                    case HttpURLConnection.HTTP_NOT_FOUND:
190                        return CHECK.NOT_FOUND;
191                    case HttpURLConnection.HTTP_FORBIDDEN:
192                    case HttpURLConnection.HTTP_UNAUTHORIZED:
193                        return CHECK.UNAUTHORIZED;
194                    case HttpURLConnection.HTTP_INTERNAL_ERROR:
195                    default:
196                        return CHECK.SERVER_ERROR;
197                }
198            }
199        }
200        catch (SSLHandshakeException e)
201        {
202            if (getLogger().isDebugEnabled())
203            {
204                getLogger().debug("Certificate error for link '" + linkValue + "'", e);
205            }
206            return CHECK.UNKNOWN;
207        }
208        catch (SocketTimeoutException e)
209        {
210            if (getLogger().isDebugEnabled())
211            {
212                getLogger().debug("Aborting test for link '" + linkValue + "' because too long", e);
213            }
214            return CHECK.UNKNOWN;
215        }
216        catch (UnknownHostException e)
217        {
218            if (getLogger().isDebugEnabled())
219            {
220                getLogger().debug("Unknown host for link '" + linkValue + "'", e);
221            }
222            return CHECK.NOT_FOUND;
223        }
224        catch (IOException e)
225        {
226            getLogger().error("Cannot test link '" + linkValue + "'", e);
227            return CHECK.SERVER_ERROR;
228        }
229        finally
230        {
231            ((CookieManager) CookieHandler.getDefault()).getCookieStore().removeAll();
232        }
233        
234    }
235    
236    private String _getCMSVersion()
237    {
238        Collection<Version> versions = _versionsHandler.getVersions();
239        for (Version version : versions)
240        {
241            if (version.getName().equals("CMS"))
242            {
243                return version.getVersion();
244            }
245        }
246        return null;
247    }
248}