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                else if (referenceValue.startsWith("tel:"))
110                {
111                    return CHECK.SUCCESS;
112                }
113            }
114            
115            if (getLogger().isDebugEnabled())
116            {
117                getLogger().debug("Cannot test external link '" + referenceValue + "'");
118            }
119            
120            return CHECK.UNKNOWN;
121        }
122        else
123        {
124            return resolver.checkLink(referenceValue, shortTest);
125        }
126    }
127    
128    /**
129     * Get the label of a reference.
130     * @param referenceType The type of the reference to test (can be 'attachment', 'explorer', 'metadata', '__external', etc...)
131     * @param referenceValue The value of the reference to test
132     * @return the element label.
133     */
134    public I18nizableText getLabel(String referenceType, String referenceValue)
135    {
136        URIResolver resolver = _uriResolverEP.getResolverForType(referenceType);
137        if (resolver == null)
138        {
139            return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_EXTERNAL_LABEL", Collections.singletonList(referenceValue));
140        }
141        else
142        {
143            return resolver.getLabel(referenceValue);
144        }
145    }
146    
147    /**
148     * Test an http link
149     * @param linkValue The http url to test
150     * @param shortTest true to make a short test (with short timeout)
151     * @return The state. UNKNOWN if test cannot be done fully
152     */
153    protected CHECK _checkHTTPLink(String linkValue, boolean shortTest)
154    {
155        try
156        {
157            CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
158            
159            URL url = new URL(linkValue.replaceAll(" ", "%20"));
160            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
161            if (shortTest)
162            {
163                connection.setConnectTimeout(1000);
164                connection.setReadTimeout(2000);
165            }
166            connection.setInstanceFollowRedirects(true);
167            connection.addRequestProperty("User-Agent", "Ametys/" + _getCMSVersion());
168                     
169            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK)
170            {
171                if (getLogger().isDebugEnabled())
172                {
173                    getLogger().debug("Check consistency OK for URI of external link with uri '" + linkValue + "'");
174                }
175                return CHECK.SUCCESS;
176            }
177            else if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP /* 302 */ || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM /* 301 */)
178            {
179                // redirection changing the protocol
180                return CHECK.UNKNOWN;
181            }
182            else
183            {
184                if (getLogger().isDebugEnabled())
185                {
186                    getLogger().debug("Check consistency " + connection.getResponseCode() + " for URI of external link with uri '" + linkValue + "'");
187                }
188                
189                int responseCode = connection.getResponseCode();
190                
191                switch (responseCode)
192                {
193                    case HttpURLConnection.HTTP_NOT_FOUND:
194                        return CHECK.NOT_FOUND;
195                    case HttpURLConnection.HTTP_FORBIDDEN:
196                    case HttpURLConnection.HTTP_UNAUTHORIZED:
197                        return CHECK.UNAUTHORIZED;
198                    case HttpURLConnection.HTTP_INTERNAL_ERROR:
199                    default:
200                        return CHECK.SERVER_ERROR;
201                }
202            }
203        }
204        catch (SSLHandshakeException e)
205        {
206            if (getLogger().isDebugEnabled())
207            {
208                getLogger().debug("Certificate error for link '" + linkValue + "'", e);
209            }
210            return CHECK.UNKNOWN;
211        }
212        catch (SocketTimeoutException e)
213        {
214            if (getLogger().isDebugEnabled())
215            {
216                getLogger().debug("Aborting test for link '" + linkValue + "' because too long", e);
217            }
218            return CHECK.UNKNOWN;
219        }
220        catch (UnknownHostException e)
221        {
222            if (getLogger().isDebugEnabled())
223            {
224                getLogger().debug("Unknown host for link '" + linkValue + "'", e);
225            }
226            return CHECK.NOT_FOUND;
227        }
228        catch (IOException e)
229        {
230            getLogger().error("Cannot test link '" + linkValue + "'", e);
231            return CHECK.SERVER_ERROR;
232        }
233        finally
234        {
235            ((CookieManager) CookieHandler.getDefault()).getCookieStore().removeAll();
236        }
237        
238    }
239    
240    private String _getCMSVersion()
241    {
242        Collection<Version> versions = _versionsHandler.getVersions();
243        for (Version version : versions)
244        {
245            if (version.getName().equals("CMS"))
246            {
247                return version.getVersion();
248            }
249        }
250        return null;
251    }
252}