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 */
019package org.apache.wiki.variables;
020
021import org.apache.logging.log4j.LogManager;
022import org.apache.logging.log4j.Logger;
023import org.apache.wiki.api.Release;
024import org.apache.wiki.api.core.Context;
025import org.apache.wiki.api.core.Page;
026import org.apache.wiki.api.core.Session;
027import org.apache.wiki.api.exceptions.NoSuchVariableException;
028import org.apache.wiki.api.filters.PageFilter;
029import org.apache.wiki.api.providers.WikiProvider;
030import org.apache.wiki.attachment.AttachmentManager;
031import org.apache.wiki.filters.FilterManager;
032import org.apache.wiki.i18n.InternationalizationManager;
033import org.apache.wiki.modules.InternalModule;
034import org.apache.wiki.pages.PageManager;
035import org.apache.wiki.preferences.Preferences;
036
037import javax.servlet.http.HttpServletRequest;
038import javax.servlet.http.HttpSession;
039import java.lang.reflect.Method;
040import java.security.Principal;
041import java.util.Date;
042import java.util.List;
043import java.util.Properties;
044import java.util.ResourceBundle;
045import java.util.stream.Collectors;
046
047
048/**
049 *  Manages variables.  Variables are case-insensitive.  A list of all available variables is on a Wiki page called "WikiVariables".
050 *
051 *  @since 1.9.20.
052 */
053public class DefaultVariableManager implements VariableManager {
054
055    private static final Logger LOG = LogManager.getLogger( DefaultVariableManager.class );
056
057    /**
058     *  Contains a list of those properties that shall never be shown. Put names here in lower case.
059     */
060    static final String[] THE_BIG_NO_NO_LIST = {
061        "jspwiki.auth.masterpassword"
062    };
063
064    /**
065     *  Creates a VariableManager object using the property list given.
066     *  @param props The properties.
067     */
068    public DefaultVariableManager( final Properties props ) {
069    }
070
071    /**
072     *  {@inheritDoc}
073     */
074    @Override
075    public String parseAndGetValue( final Context context, final String link ) throws IllegalArgumentException, NoSuchVariableException {
076        if( !link.startsWith( "{$" ) ) {
077            throw new IllegalArgumentException( "Link does not start with {$" );
078        }
079        if( !link.endsWith( "}" ) ) {
080            throw new IllegalArgumentException( "Link does not end with }" );
081        }
082        final String varName = link.substring( 2, link.length() - 1 );
083
084        return getValue( context, varName.trim() );
085    }
086
087    /**
088     *  {@inheritDoc}
089     */
090    @Override
091    // FIXME: somewhat slow.
092    public String expandVariables( final Context context, final String source ) {
093        final StringBuilder result = new StringBuilder();
094        for( int i = 0; i < source.length(); i++ ) {
095            if( source.charAt(i) == '{' ) {
096                if( i < source.length()-2 && source.charAt(i+1) == '$' ) {
097                    final int end = source.indexOf( '}', i );
098
099                    if( end != -1 ) {
100                        final String varname = source.substring( i+2, end );
101                        String value;
102
103                        try {
104                            value = getValue( context, varname );
105                        } catch( final NoSuchVariableException | IllegalArgumentException e ) {
106                            value = e.getMessage();
107                        }
108
109                        result.append( value );
110                        i = end;
111                    }
112                } else {
113                    result.append( '{' );
114                }
115            } else {
116                result.append( source.charAt(i) );
117            }
118        }
119
120        return result.toString();
121    }
122
123    /**
124     *  {@inheritDoc}
125     */
126    @Override
127    public String getValue( final Context context, final String varName, final String defValue ) {
128        try {
129            return getValue( context, varName );
130        } catch( final NoSuchVariableException e ) {
131            return defValue;
132        }
133    }
134
135    /**
136     *  {@inheritDoc}
137     */
138    @Override
139    public String getVariable( final Context context, final String name ) {
140        return getValue( context, name, null );
141    }
142
143    /**
144     *  {@inheritDoc}
145     */
146    @Override
147    public String getValue( final Context context, final String varName ) throws IllegalArgumentException, NoSuchVariableException {
148        if( varName == null ) {
149            throw new IllegalArgumentException( "Null variable name." );
150        }
151        if( varName.isEmpty() ) {
152            throw new IllegalArgumentException( "Zero length variable name." );
153        }
154        // Faster than doing equalsIgnoreCase()
155        final String name = varName.toLowerCase();
156        if (!"jspwiki.frontpage".equals(name) && 
157            !"jspwiki.runfilters".equals(name) && 
158            name.startsWith( "jspwiki" ) ) {
159            String whitelist = context.getEngine().getWikiProperties().getProperty("jspwiki.variablemanager.whitelist");
160            if (whitelist!=null && !whitelist.contains(name)) {
161                 LOG.warn("variable manager is denying access to '" + name + "'. to override this behavior, "
162                         + "you can add this to jspwiki.variablemanager.whitelist in the properties file.");
163            return "";
164            }
165           
166        }
167        for( final String value : THE_BIG_NO_NO_LIST ) {
168            if( name.equals( value ) ) {
169                return ""; // FIXME: Should this be something different?
170            }
171        }
172        
173        try {
174            //
175            //  Using reflection to get system variables adding a new system variable
176            //  now only involves creating a new method in the SystemVariables class
177            //  with a name starting with get and the first character of the name of
178            //  the variable capitalized. Example:
179            //    public String getMysysvar(){
180            //      return "Hello World";
181            //    }
182            //
183            final SystemVariables sysvars = new SystemVariables( context );
184            final String methodName = "get" + Character.toUpperCase( name.charAt( 0 ) ) + name.substring( 1 );
185            final Method method = sysvars.getClass().getMethod( methodName );
186            return ( String )method.invoke( sysvars );
187        } catch( final NoSuchMethodException e1 ) {
188            //
189            //  It is not a system var. Time to handle the other cases.
190            //
191            //  Check if such a context variable exists, returning its string representation.
192            //
193            if( ( context.getVariable( varName ) ) != null ) {
194                return context.getVariable( varName ).toString();
195            }
196
197            //
198            //  Well, I guess it wasn't a final straw.  We also allow variables from the session and the request (in this order).
199            //
200            final HttpServletRequest req = context.getHttpRequest();
201            if( req != null && req.getSession() != null ) {
202                final HttpSession session = req.getSession();
203
204                try {
205                    String s = ( String )session.getAttribute( varName );
206
207                    if( s != null ) {
208                        return s;
209                    }
210
211                    s = context.getHttpParameter( varName );
212                    if( s != null ) {
213                        return s;
214                    }
215                } catch( final ClassCastException e ) {
216                    LOG.debug( "Not a String: " + varName );
217                }
218            }
219
220            //
221            // And the final straw: see if the current page has named metadata.
222            //
223            final Page pg = context.getPage();
224            if( pg != null ) {
225                final Object metadata = pg.getAttribute( varName );
226                if( metadata != null ) {
227                    return metadata.toString();
228                }
229            }
230
231            //
232            // And the final straw part 2: see if the "real" current page has named metadata. This allows
233            // a parent page to control a inserted page through defining variables
234            //
235            final Page rpg = context.getRealPage();
236            if( rpg != null ) {
237                final Object metadata = rpg.getAttribute( varName );
238                if( metadata != null ) {
239                    return metadata.toString();
240                }
241            }
242
243            //
244            // Next-to-final straw: attempt to fetch using property name. We don't allow fetching any other
245            // properties than those starting with "jspwiki.".  I know my own code, but I can't vouch for bugs
246            // in other people's code... :-)
247            //
248            if( varName.startsWith("jspwiki.") ) {
249                final Properties props = context.getEngine().getWikiProperties();
250                final String s = props.getProperty( varName );
251                if( s != null ) {
252                    return s;
253                }
254            }
255
256            //
257            //  Final defaults for some known quantities.
258            //
259            if( varName.equals( VAR_ERROR ) || varName.equals( VAR_MSG ) ) {
260                return "";
261            }
262
263            throw new NoSuchVariableException( "No variable " + varName + " defined." );
264        } catch( final Exception e ) {
265            LOG.info("Interesting exception: cannot fetch variable value", e );
266        }
267        return "";
268    }
269
270    /**
271     *  This class provides the implementation for the different system variables.
272     *  It is called via Reflection - any access to a variable called $xxx is mapped
273     *  to getXxx() on this class.
274     *  <p>
275     *  This is a lot neater than using a huge if-else if branching structure
276     *  that we used to have before.
277     *  <p>
278     *  Note that since we are case insensitive for variables, and VariableManager
279     *  calls var.toLowerCase(), the getters for the variables do not have
280     *  capitalization anywhere.  This may look a bit odd, but then again, this
281     *  is not meant to be a public class.
282     *
283     *  @since 2.7.0
284     */
285    @SuppressWarnings( "unused" )
286    private static class SystemVariables {
287
288        private final Context m_context;
289
290        public SystemVariables( final Context context )
291        {
292            m_context=context;
293        }
294
295        public String getPagename()
296        {
297            return m_context.getPage().getName();
298        }
299
300        public String getApplicationname()
301        {
302            return m_context.getEngine().getApplicationName();
303        }
304
305        public String getJspwikiversion()
306        {
307            return Release.getVersionString();
308        }
309
310        public String getEncoding() {
311            return m_context.getEngine().getContentEncoding().displayName();
312        }
313
314        public String getTotalpages() {
315            return Integer.toString( m_context.getEngine().getManager( PageManager.class ).getTotalPageCount() );
316        }
317
318        public String getPageprovider() {
319            return m_context.getEngine().getManager( PageManager.class ).getCurrentProvider();
320        }
321
322        public String getPageproviderdescription() {
323            return m_context.getEngine().getManager( PageManager.class ).getProviderDescription();
324        }
325
326        public String getAttachmentprovider() {
327            final WikiProvider p = m_context.getEngine().getManager( AttachmentManager.class ).getCurrentProvider();
328            return (p != null) ? p.getClass().getName() : "-";
329        }
330
331        public String getAttachmentproviderdescription() {
332            final WikiProvider p = m_context.getEngine().getManager( AttachmentManager.class ).getCurrentProvider();
333            return (p != null) ? p.getProviderInfo() : "-";
334        }
335
336        public String getInterwikilinks() {
337
338            return m_context.getEngine().getAllInterWikiLinks().stream().map(link -> link + " --> " + m_context.getEngine().getInterWikiURL(link)).collect(Collectors.joining(", "));
339        }
340
341        public String getInlinedimages() {
342
343            return m_context.getEngine().getAllInlinedImagePatterns().stream().collect(Collectors.joining(", "));
344        }
345
346        public String getPluginpath() {
347            final String s = m_context.getEngine().getPluginSearchPath();
348
349            return ( s == null ) ? "-" : s;
350        }
351
352        public String getBaseurl()
353        {
354            return m_context.getEngine().getBaseURL();
355        }
356
357        public String getUptime() {
358            final Date now = new Date();
359            long secondsRunning = ( now.getTime() - m_context.getEngine().getStartTime().getTime() ) / 1_000L;
360
361            final long seconds = secondsRunning % 60;
362            final long minutes = (secondsRunning /= 60) % 60;
363            final long hours = (secondsRunning /= 60) % 24;
364            final long days = secondsRunning /= 24;
365
366            return days + "d, " + hours + "h " + minutes + "m " + seconds + "s";
367        }
368
369        public String getLoginstatus() {
370            final Session session = m_context.getWikiSession();
371            return Preferences.getBundle( m_context, InternationalizationManager.CORE_BUNDLE ).getString( "varmgr." + session.getStatus() );
372        }
373
374        public String getUsername() {
375            final Principal wup = m_context.getCurrentUser();
376            final ResourceBundle rb = Preferences.getBundle( m_context, InternationalizationManager.CORE_BUNDLE );
377            return wup != null ? wup.getName() : rb.getString( "varmgr.not.logged.in" );
378        }
379
380        public String getRequestcontext()
381        {
382            return m_context.getRequestContext();
383        }
384
385        public String getPagefilters() {
386            final FilterManager fm = m_context.getEngine().getManager( FilterManager.class );
387            final List< PageFilter > filters = fm.getFilterList();
388            final StringBuilder sb = new StringBuilder();
389            for( final PageFilter pf : filters ) {
390                final String f = pf.getClass().getName();
391                if( pf instanceof InternalModule ) {
392                    continue;
393                }
394
395                if( sb.length() > 0 ) {
396                    sb.append( ", " );
397                }
398                sb.append( f );
399            }
400            return sb.toString();
401        }
402    }
403
404}