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}