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 jakarta.servlet.http.HttpServletRequest;
038import jakarta.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
157        for( final String value : THE_BIG_NO_NO_LIST ) {
158            if( name.equals( value ) ) {
159                return ""; // FIXME: Should this be something different?
160            }
161            if ("jspwiki.frontpage".equals(name)) continue;
162            if ("jspwiki.runfilters".equals(name) ) continue;
163            
164            if ( name.startsWith( "jspwiki" ) ) {
165                LOG.warn("variable manager is denying access to '" + name + "'");
166                return "";
167            }
168
169        }
170        
171        try {
172            //
173            //  Using reflection to get system variables adding a new system variable
174            //  now only involves creating a new method in the SystemVariables class
175            //  with a name starting with get and the first character of the name of
176            //  the variable capitalized. Example:
177            //    public String getMysysvar(){
178            //      return "Hello World";
179            //    }
180            //
181            final SystemVariables sysvars = new SystemVariables( context );
182            final String methodName = "get" + Character.toUpperCase( name.charAt( 0 ) ) + name.substring( 1 );
183            final Method method = sysvars.getClass().getMethod( methodName );
184            return ( String )method.invoke( sysvars );
185        } catch( final NoSuchMethodException e1 ) {
186            //
187            //  It is not a system var. Time to handle the other cases.
188            //
189            //  Check if such a context variable exists, returning its string representation.
190            //
191            if( ( context.getVariable( varName ) ) != null ) {
192                return context.getVariable( varName ).toString();
193            }
194
195            //
196            //  Well, I guess it wasn't a final straw.  We also allow variables from the session and the request (in this order).
197            //
198            final HttpServletRequest req = context.getHttpRequest();
199            if( req != null && req.getSession() != null ) {
200                final HttpSession session = req.getSession();
201
202                try {
203                    String s = ( String )session.getAttribute( varName );
204
205                    if( s != null ) {
206                        return s;
207                    }
208
209                    s = context.getHttpParameter( varName );
210                    if( s != null ) {
211                        return s;
212                    }
213                } catch( final ClassCastException e ) {
214                    LOG.debug( "Not a String: " + varName );
215                }
216            }
217
218            //
219            // And the final straw: see if the current page has named metadata.
220            //
221            final Page pg = context.getPage();
222            if( pg != null ) {
223                final Object metadata = pg.getAttribute( varName );
224                if( metadata != null ) {
225                    return metadata.toString();
226                }
227            }
228
229            //
230            // And the final straw part 2: see if the "real" current page has named metadata. This allows
231            // a parent page to control a inserted page through defining variables
232            //
233            final Page rpg = context.getRealPage();
234            if( rpg != null ) {
235                final Object metadata = rpg.getAttribute( varName );
236                if( metadata != null ) {
237                    return metadata.toString();
238                }
239            }
240
241            //
242            // Next-to-final straw: attempt to fetch using property name. We don't allow fetching any other
243            // properties than those starting with "jspwiki.".  I know my own code, but I can't vouch for bugs
244            // in other people's code... :-)
245            //
246            if( varName.startsWith("jspwiki.") ) {
247                final Properties props = context.getEngine().getWikiProperties();
248                final String s = props.getProperty( varName );
249                if( s != null ) {
250                    return s;
251                }
252            }
253
254            //
255            //  Final defaults for some known quantities.
256            //
257            if( varName.equals( VAR_ERROR ) || varName.equals( VAR_MSG ) ) {
258                return "";
259            }
260
261            throw new NoSuchVariableException( "No variable " + varName + " defined." );
262        } catch( final Exception e ) {
263            LOG.info("Interesting exception: cannot fetch variable value", e );
264        }
265        return "";
266    }
267
268    /**
269     *  This class provides the implementation for the different system variables.
270     *  It is called via Reflection - any access to a variable called $xxx is mapped
271     *  to getXxx() on this class.
272     *  <p>
273     *  This is a lot neater than using a huge if-else if branching structure
274     *  that we used to have before.
275     *  <p>
276     *  Note that since we are case insensitive for variables, and VariableManager
277     *  calls var.toLowerCase(), the getters for the variables do not have
278     *  capitalization anywhere.  This may look a bit odd, but then again, this
279     *  is not meant to be a public class.
280     *
281     *  @since 2.7.0
282     */
283    @SuppressWarnings( "unused" )
284    private static class SystemVariables {
285
286        private final Context m_context;
287
288        public SystemVariables( final Context context )
289        {
290            m_context=context;
291        }
292
293        public String getPagename()
294        {
295            return m_context.getPage().getName();
296        }
297
298        public String getApplicationname()
299        {
300            return m_context.getEngine().getApplicationName();
301        }
302
303        public String getJspwikiversion()
304        {
305            return Release.getVersionString();
306        }
307
308        public String getEncoding() {
309            return m_context.getEngine().getContentEncoding().displayName();
310        }
311
312        public String getTotalpages() {
313            return Integer.toString( m_context.getEngine().getManager( PageManager.class ).getTotalPageCount() );
314        }
315
316        public String getPageprovider() {
317            return m_context.getEngine().getManager( PageManager.class ).getCurrentProvider();
318        }
319
320        public String getPageproviderdescription() {
321            return m_context.getEngine().getManager( PageManager.class ).getProviderDescription();
322        }
323
324        public String getAttachmentprovider() {
325            final WikiProvider p = m_context.getEngine().getManager( AttachmentManager.class ).getCurrentProvider();
326            return (p != null) ? p.getClass().getName() : "-";
327        }
328
329        public String getAttachmentproviderdescription() {
330            final WikiProvider p = m_context.getEngine().getManager( AttachmentManager.class ).getCurrentProvider();
331            return (p != null) ? p.getProviderInfo() : "-";
332        }
333
334        public String getInterwikilinks() {
335
336            return m_context.getEngine().getAllInterWikiLinks().stream().map(link -> link + " --> " + m_context.getEngine().getInterWikiURL(link)).collect(Collectors.joining(", "));
337        }
338
339        public String getInlinedimages() {
340
341            return m_context.getEngine().getAllInlinedImagePatterns().stream().collect(Collectors.joining(", "));
342        }
343
344        public String getPluginpath() {
345            final String s = m_context.getEngine().getPluginSearchPath();
346
347            return ( s == null ) ? "-" : s;
348        }
349
350        public String getBaseurl()
351        {
352            return m_context.getEngine().getBaseURL();
353        }
354
355        public String getUptime() {
356            final Date now = new Date();
357            long secondsRunning = ( now.getTime() - m_context.getEngine().getStartTime().getTime() ) / 1_000L;
358
359            final long seconds = secondsRunning % 60;
360            final long minutes = (secondsRunning /= 60) % 60;
361            final long hours = (secondsRunning /= 60) % 24;
362            final long days = secondsRunning /= 24;
363
364            return days + "d, " + hours + "h " + minutes + "m " + seconds + "s";
365        }
366
367        public String getLoginstatus() {
368            final Session session = m_context.getWikiSession();
369            return Preferences.getBundle( m_context, InternationalizationManager.CORE_BUNDLE ).getString( "varmgr." + session.getStatus() );
370        }
371
372        public String getUsername() {
373            final Principal wup = m_context.getCurrentUser();
374            final ResourceBundle rb = Preferences.getBundle( m_context, InternationalizationManager.CORE_BUNDLE );
375            return wup != null ? wup.getName() : rb.getString( "varmgr.not.logged.in" );
376        }
377
378        public String getRequestcontext()
379        {
380            return m_context.getRequestContext();
381        }
382
383        public String getPagefilters() {
384            final FilterManager fm = m_context.getEngine().getManager( FilterManager.class );
385            final List< PageFilter > filters = fm.getFilterList();
386            final StringBuilder sb = new StringBuilder();
387            for( final PageFilter pf : filters ) {
388                final String f = pf.getClass().getName();
389                if( pf instanceof InternalModule ) {
390                    continue;
391                }
392
393                if( sb.length() > 0 ) {
394                    sb.append( ", " );
395                }
396                sb.append( f );
397            }
398            return sb.toString();
399        }
400    }
401
402}