001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019
020package org.apache.wiki.plugin;
021
022import java.security.Principal;
023import java.util.Map;
024
025import org.apache.commons.lang.StringUtils;
026import org.apache.oro.text.regex.*;
027import org.apache.wiki.WikiContext;
028import org.apache.wiki.WikiProvider;
029import org.apache.wiki.api.exceptions.PluginException;
030import org.apache.wiki.api.plugin.WikiPlugin;
031import org.apache.wiki.util.HttpUtil;
032import org.apache.wiki.util.TextUtil;
033
034/**
035 *  The IfPlugin allows parts of a WikiPage to be executed conditionally, and is intended as a flexible way
036 *  of customizing a page depending on certain conditions. Do not use it as a security mechanism to conditionally
037 *  hide content from users (use page ACLs for that).
038 *  
039 *  You can also use shorthand "If" to run it.
040 *  
041 *  Parameters:
042 *  <ul>
043 *    <li><b>group</b> - A "|" -separated list of group names.
044 *    <li><b>user</b>  - A "|" -separated list of user names.
045 *    <li><b>ip</b>    - A "|" -separated list of ip addresses.
046 *    <li><b>var</b>   - A wiki variable
047 *    <li><b>page</b>  - A page name
048 *    <li><b>contains</b> - A Perl5 regexp pattern
049 *    <li><b>is</b>    - A Perl5 regexp pattern
050 *    <li><b>exists</b> - "true" or "false".
051 *  </ul>
052 *
053 *  <p>If any of them match, the body of the plugin is executed.  You can
054 *  negate the content by prefixing it with a "!".  For example, to greet
055 *  all admins, put the following in your LeftMenu:</p>
056 *  <pre>
057 *  [{If group='Admin'
058 *
059 *  Hello, Admin, and your mighty powers!}]
060 *  </pre>
061 *
062 *  <p>In order to send a message to everybody except Jack use</p>
063 *  <pre>
064 *  [{If user='!Jack'
065 *
066 *  %%warning
067 *  Jack's surprise birthday party at eleven!
068 *  %%}]
069 *  </pre>
070 *
071 *  <p>Note that you can't use "!Jack|!Jill", because for Jack, !Jill matches;
072 *  and for Jill, !Jack matches.  These are not regular expressions (though
073 *  they might become so in the future).<p>
074 *
075 *  <p>To check for page content, use</p>
076 *  <pre>
077 *  [{If page='TestPage' contains='xyzzy'
078 *
079 *  Page contains the text "xyzzy"}]
080 *  </pre>
081 *
082 *  <p>The difference between "contains" and "is" is that "is" is always an exact match,
083 *  whereas "contains" just checks if a pattern is available.</p>
084 *
085 *  <p>To check for page existence, use</p>
086 *  <pre>
087 *  [{If page='TestPage' exists='true'
088 *
089 *  Page "TestPage" exists.}]
090 *  </pre>
091 *  <p>With the same mechanism, it's also possible to test for the existence
092 *  of a variable - just use "var" instead of "page".</p>
093 *  
094 *  <p>Another caveat is that the plugin body content is not counted
095 *  towards ReferenceManager links.  So any links do not appear on any reference
096 *  lists.  Depending on your position, this may be a good or a bad
097 *  thing.</p>
098 *
099 *  <h3>Calling Externally</h3>
100 *
101 *  <p>The functional, decision-making part of this plugin may be called from
102 *  other code (e.g., other plugins) since it is available as a static method
103 *  {@link #ifInclude(WikiContext,Map)}. Note that the plugin body may contain
104 *  references to other plugins.</p>
105 *
106 *  @since 2.6
107 */
108public class IfPlugin implements WikiPlugin
109{
110    /** The parameter name for setting the group to check.  Value is <tt>{@value}</tt>. */
111    public static final String PARAM_GROUP    = "group";
112
113    /** The parameter name for setting the user id to check.  Value is <tt>{@value}</tt>. */
114    public static final String PARAM_USER     = "user";
115    
116    /** The parameter name for setting the ip address to check.  Value is <tt>{@value}</tt>. */
117    public static final String PARAM_IP       = "ip";
118    
119    /** The parameter name for setting the page name to check.  Value is <tt>{@value}</tt>. */
120    public static final String PARAM_PAGE     = "page";
121    
122    /** The parameter name for setting the contents of the page to check.  Value is <tt>{@value}</tt>. */
123    public static final String PARAM_CONTAINS = "contains";
124    
125    /** The parameter name for setting the variable name to check.  Value is <tt>{@value}</tt>. */
126    public static final String PARAM_VAR      = "var";
127    
128    /** The parameter name for setting the exact content to check.  Value is <tt>{@value}</tt>. */
129    public static final String PARAM_IS       = "is";
130    
131    /** The parameter name for checking whether a page/var exists.  Value is <tt>{@value}</tt>. */
132    public static final String PARAM_EXISTS   = "exists";
133
134    /**
135     *  {@inheritDoc}
136     */
137    public String execute(WikiContext context, Map<String, String> params) throws PluginException
138    {
139        return ifInclude( context,params )
140                ? context.getEngine().textToHTML( context, params.get( DefaultPluginManager.PARAM_BODY ) )
141                : "" ;
142    }
143
144
145    /**
146     *  Returns a boolean result based on processing the WikiContext and
147     *  parameter Map as according to the rules stated in the IfPlugin
148     *  documentation. 
149     *  As a static method this may be called by other classes.
150     *
151     * @param context   The current WikiContext.
152     * @param params    The parameter Map which contains key-value pairs.
153     * @throws PluginException If something goes wrong
154     * @return True, if the condition holds.
155     */
156    public static boolean ifInclude( WikiContext context, Map<String, String> params ) throws PluginException
157    {
158        boolean include = false;
159
160        String group    = params.get( PARAM_GROUP );
161        String user     = params.get( PARAM_USER );
162        String ip       = params.get( PARAM_IP );
163        String page     = params.get( PARAM_PAGE );
164        String contains = params.get( PARAM_CONTAINS );
165        String var      = params.get( PARAM_VAR );
166        String is       = params.get( PARAM_IS );
167        String exists   = params.get( PARAM_EXISTS );
168
169        include |= checkGroup(context, group);
170        include |= checkUser(context, user);
171        include |= checkIP(context, ip);
172
173        if( page != null )
174        {
175            String content = context.getEngine().getPureText(page, WikiProvider.LATEST_VERSION).trim();
176            include |= checkContains(content,contains);
177            include |= checkIs(content,is);
178            include |= checkExists(context,page,exists);
179        }
180
181        if( var != null )
182        {
183            String content = context.getEngine().getVariable(context, var);
184            include |= checkContains(content,contains);
185            include |= checkIs(content,is);
186            include |= checkVarExists(content,exists);
187        }
188
189        return include;
190    }
191
192    private static boolean checkExists( WikiContext context, String page, String exists )
193    {
194        if( exists == null ) return false;
195        return !context.getEngine().pageExists(page) ^ TextUtil.isPositive(exists);
196    }
197
198    private static boolean checkVarExists( String varContent, String exists )
199    {
200        if( exists == null ) return false;
201        return (varContent == null ) ^ TextUtil.isPositive(exists);
202    }
203
204    private static boolean checkGroup( WikiContext context, String group )
205    {
206        if( group == null ) return false;
207        String[] groupList = StringUtils.split(group,'|');
208        boolean include = false;
209
210        for( int i = 0; i < groupList.length; i++ )
211        {
212            String gname = groupList[i];
213            boolean invert = false;
214            if( groupList[i].startsWith("!") )
215            {
216                if( groupList[i].length() > 1 ) 
217                {
218                    gname = groupList[i].substring( 1 );
219                }
220                invert = true;
221            }
222
223            Principal g = context.getEngine().getAuthorizationManager().resolvePrincipal(gname);
224
225            include |= context.getEngine().getAuthorizationManager().isUserInRole( context.getWikiSession(), g ) ^ invert;
226        }
227        return include;
228    }
229
230    private static boolean checkUser( WikiContext context, String user )
231    {
232        if( user == null || context.getCurrentUser() == null ) return false;
233
234        String[] list = StringUtils.split(user,'|');
235        boolean include = false;
236
237        for( int i = 0; i < list.length; i++ )
238        {
239            String userToCheck = list[i];
240            boolean invert = false;
241            if( list[i].startsWith("!") )
242            {
243                invert = true;
244                // strip !
245                if(  user.length() > 1 ) 
246                {
247                    userToCheck = list[i].substring( 1 );
248                }
249            }
250
251            include |= userToCheck.equals( context.getCurrentUser().getName() ) ^ invert;
252        }
253        return include;
254    }
255
256    // TODO: Add subnetwork matching, e.g. 10.0.0.0/8
257    private static boolean checkIP( WikiContext context, String ipaddr )
258    {
259        if( ipaddr == null || context.getHttpRequest() == null ) return false;
260
261        
262        String[] list = StringUtils.split(ipaddr,'|');
263        boolean include = false;
264
265        for( int i = 0; i < list.length; i++ )
266        {
267            String ipaddrToCheck = list[i];
268            boolean invert = false;
269            if( list[i].startsWith("!") )
270            {
271                invert = true;
272                // strip !
273                if(  list[i].length() > 1 ) 
274                {
275                    ipaddrToCheck = list[i].substring( 1 );
276                }
277            }
278
279            include |= ipaddrToCheck.equals( HttpUtil.getRemoteAddress(context.getHttpRequest()) ) ^ invert;
280        }
281        return include;
282    }
283
284    private static boolean doMatch( String content, String pattern )
285        throws PluginException
286    {
287        PatternCompiler compiler = new Perl5Compiler();
288        PatternMatcher  matcher  = new Perl5Matcher();
289
290        try
291        {
292            Pattern matchp = compiler.compile( pattern, Perl5Compiler.SINGLELINE_MASK );
293            // m_exceptPattern = compiler.compile( exceptPattern, Perl5Compiler.SINGLELINE_MASK );
294            return matcher.matches( content, matchp );
295        }
296        catch( MalformedPatternException e )
297        {
298            throw new PluginException("Faulty pattern "+pattern);
299        }
300
301    }
302
303    private static boolean checkContains( String pagecontent, String matchPattern )
304        throws PluginException
305    {
306        if( pagecontent == null || matchPattern == null ) return false;
307
308        return doMatch( pagecontent, ".*"+matchPattern+".*" );
309    }
310
311    private static boolean checkIs( String content, String matchPattern )
312        throws PluginException
313    {
314        if( content == null || matchPattern == null ) return false;
315
316        matchPattern = "^"+matchPattern+"$";
317
318        return doMatch(content, matchPattern);
319    }
320}