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