    001/*
    002    Copyright (C) 2003 Janne Jalkanen (Janne.Jalkanen@iki.fi)
    003
    004    Licensed to the Apache Software Foundation (ASF) under one
    005    or more contributor license agreements.  See the NOTICE file
    006    distributed with this work for additional information
    007    regarding copyright ownership.  The ASF licenses this file
    008    to you under the Apache License, Version 2.0 (the
    009    "License"); you may not use this file except in compliance
    010    with the License.  You may obtain a copy of the License at
    011
    012       http://www.apache.org/licenses/LICENSE-2.0
    013
    014    Unless required by applicable law or agreed to in writing,
    015    software distributed under the License is distributed on an
    016    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    017    KIND, either express or implied.  See the License for the
    018    specific language governing permissions and limitations
    019    under the License.
    020 */
    021package org.apache.wiki.plugin;
    022
    023import org.apache.logging.log4j.LogManager;
    024import org.apache.logging.log4j.Logger;
    025import org.apache.oro.text.GlobCompiler;
    026import org.apache.oro.text.regex.MalformedPatternException;
    027import org.apache.oro.text.regex.Pattern;
    028import org.apache.oro.text.regex.PatternCompiler;
    029import org.apache.oro.text.regex.PatternMatcher;
    030import org.apache.oro.text.regex.Perl5Matcher;
    031import org.apache.wiki.api.core.Context;
    032import org.apache.wiki.api.exceptions.PluginException;
    033import org.apache.wiki.api.plugin.Plugin;
    034import org.apache.wiki.util.TextUtil;
    035
    036import jakarta.servlet.http.HttpServletRequest;
    037import java.io.IOException;
    038import java.io.InputStream;
    039import java.net.URL;
    040import java.util.ArrayList;
    041import java.util.Enumeration;
    042import java.util.List;
    043import java.util.Locale;
    044import java.util.Map;
    045import java.util.Properties;
    046import java.util.ResourceBundle;
    047
    048/**
    049 *  Denounces a link by removing it from any search engine.
    050 *  <br> The bots are listed in org/apache/wiki/plugin/denounce.properties.
    051 *
    052 *  <p>Parameters : </p>
    053 *  <ul>
    054 *  <li><b>link</b> - The link to be denounced, this parameter is required</li>
    055 *  <li><b>text</b> - The text to use, defaults to the link</li>
    056 *  </ul>
    057 *
    058 *  @since 2.1.40.
    059 */
    060public class Denounce implements Plugin {
    061
    062    private static final Logger LOG = LogManager.getLogger( Denounce.class );
    063
    064    /** Parameter name for setting the link.  Value is <tt>{@value}</tt>. */
    065    public static final String PARAM_LINK = "link";
    066    /** Parameter name for setting the text.  Value is <tt>{@value}</tt>. */
    067    public static final String PARAM_TEXT = "text";
    068
    069    private static final String PROPERTYFILE = "org/apache/wiki/plugin/denounce.properties";
    070    private static final String PROP_AGENTPATTERN   = "denounce.agentpattern.";
    071    private static final String PROP_HOSTPATTERN    = "denounce.hostpattern.";
    072    private static final String PROP_REFERERPATTERN = "denounce.refererpattern.";
    073
    074    private static final String PROP_DENOUNCETEXT   = "denounce.denouncetext";
    075
    076    private static final ArrayList< Pattern > c_refererPatterns = new ArrayList<>();
    077    private static final ArrayList< Pattern > c_agentPatterns   = new ArrayList<>();
    078    private static final ArrayList< Pattern > c_hostPatterns    = new ArrayList<>();
    079
    080    private static String c_denounceText = "";
    081
    082    /*
    083     *  Prepares the different patterns for later use.  Compiling is
    084     *  (probably) expensive, so we do it statically at class load time.
    085     */
    086    static {
    087        try {
    088            final PatternCompiler compiler = new GlobCompiler();
    089            final ClassLoader loader = Denounce.class.getClassLoader();
    090            final InputStream in = loader.getResourceAsStream( PROPERTYFILE );
    091            if( in == null ) {
    092                throw new IOException( "No property file found! (Check the installation, it should be there.)" );
    093            }
    094
    095            final Properties props = new Properties();
    096            props.load( in );
    097
    098            c_denounceText = props.getProperty( PROP_DENOUNCETEXT, c_denounceText );
    099
    100            for( final Enumeration< ? > e = props.propertyNames(); e.hasMoreElements(); ) {
    101                final String name = (String) e.nextElement();
    102
    103                try {
    104                    if( name.startsWith( PROP_REFERERPATTERN ) ) {
    105                        c_refererPatterns.add( compiler.compile( props.getProperty(name) ) );
    106                    } else if( name.startsWith( PROP_AGENTPATTERN ) ) {
    107                        c_agentPatterns.add( compiler.compile( props.getProperty(name) ) );
    108                    } else if( name.startsWith( PROP_HOSTPATTERN ) ) {
    109                        c_hostPatterns.add( compiler.compile( props.getProperty(name) ) );
    110                    }
    111                } catch( final MalformedPatternException ex ) {
    112                    LOG.error( "Malformed URL pattern in "+PROPERTYFILE+": "+props.getProperty(name), ex );
    113                }
    114            }
    115
    116            LOG.debug( "Added " + c_refererPatterns.size() + c_agentPatterns.size() + c_hostPatterns.size() + " crawlers to denounce list." );
    117        } catch( final IOException e ) {
    118            LOG.error( "Unable to load URL patterns from " + PROPERTYFILE, e );
    119        } catch( final Exception e ) {
    120            LOG.error( "Unable to initialize Denounce plugin", e );
    121        }
    122    }
    123
    124    @Override
    125    public String getDisplayName(Locale locale) {
    126
    127        final ResourceBundle rb = ResourceBundle.getBundle(PluginManager.PLUGIN_I18N_RESOURCE, locale);
    128        return rb.getString(this.getClass().getSimpleName());
    129    }
    130    /**
    131     *  {@inheritDoc}
    132     */
    133    @Override
    134    public String execute( final Context context, final Map<String, String> params ) throws PluginException {
    135        final String link = TextUtil.replaceEntities( params.get( PARAM_LINK ) );
    136        //final String link = params.get( PARAM_LINK );
    137        String text = params.get( PARAM_TEXT );
    138        boolean linkAllowed = true;
    139
    140        if( link == null ) {
    141            throw new PluginException( "Denounce: No parameter "+PARAM_LINK+" defined!" );
    142        }
    143        if( !isLinkValid( link ) ) {
    144            throw new PluginException( "Denounce: Not a valid link " + link );
    145        }
    146
    147        final HttpServletRequest request = context.getHttpRequest();
    148        if( request != null ) {
    149            linkAllowed = !matchHeaders( request );
    150        }
    151
    152        if( text == null ) {
    153            text = link;
    154        }
    155
    156        if( linkAllowed ) {
    157            return "<a href=\"" + link + "\">" + TextUtil.replaceEntities( text ) + "</a>";
    158        }
    159
    160        return c_denounceText;
    161    }
    162
    163    boolean isLinkValid( final String link ) {
    164        try {
    165            new URL( link ).toURI().parseServerAuthority();
    166        } catch ( final Exception e ) {
    167            LOG.debug( "invalid link {} - {}", link, e.getMessage() );
    168            return false;
    169        }
    170        return true;
    171    }
    172
    173    /**
    174     *  Returns true, if the path is found among the referers.
    175     */
    176    private boolean matchPattern( final List< Pattern > list, final String path ) {
    177        final PatternMatcher matcher = new Perl5Matcher();
    178        return list.stream().anyMatch(pattern -> matcher.matches(path, pattern));
    179    }
    180
    181    private boolean matchHeaders( final HttpServletRequest request ) {
    182        //  User Agent
    183        final String userAgent = request.getHeader( "User-Agent" );
    184        if( userAgent != null && matchPattern( c_agentPatterns, userAgent ) ) {
    185            LOG.debug( "Matched user agent " + userAgent + " for denounce." );
    186            return true;
    187        }
    188
    189        //  Referrer header
    190        final String refererPath = request.getHeader( "Referer" );
    191        if( refererPath != null && matchPattern( c_refererPatterns, refererPath ) ) {
    192            LOG.debug( "Matched referer " + refererPath + " for denounce." );
    193            return true;
    194        }
    195
    196        //  Host
    197        final String host = request.getRemoteHost();
    198        if( host != null && matchPattern( c_hostPatterns, host ) ) {
    199            LOG.debug( "Matched host " + host + " for denounce." );
    200            return true;
    201        }
    202
    203        return false;
    204    }
    205
    206}