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.providers;
020
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileOutputStream;
025import java.io.FilenameFilter;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStreamWriter;
029import java.io.PrintWriter;
030import java.io.UnsupportedEncodingException;
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.Date;
034import java.util.Enumeration;
035import java.util.List;
036import java.util.Map;
037import java.util.Properties;
038import java.util.TreeSet;
039
040import org.apache.commons.io.IOUtils;
041import org.apache.commons.lang.StringUtils;
042import org.apache.log4j.Logger;
043import org.apache.wiki.InternalWikiException;
044import org.apache.wiki.WikiEngine;
045import org.apache.wiki.WikiPage;
046import org.apache.wiki.WikiProvider;
047import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
048import org.apache.wiki.api.exceptions.ProviderException;
049import org.apache.wiki.search.QueryItem;
050import org.apache.wiki.search.SearchMatcher;
051import org.apache.wiki.search.SearchResult;
052import org.apache.wiki.search.SearchResultComparator;
053import org.apache.wiki.util.FileUtil;
054import org.apache.wiki.util.TextUtil;
055
056
057/**
058 *  Provides a simple directory based repository for Wiki pages.
059 *  <P>
060 *  All files have ".txt" appended to make life easier for those
061 *  who insist on using Windows or other software which makes assumptions
062 *  on the files contents based on its name.
063 *  <p>
064 *  This class functions as a superclass to all file based providers.
065 *
066 *  @since 2.1.21.
067 *
068 */
069public abstract class AbstractFileProvider
070    implements WikiPageProvider
071{
072    private static final Logger   log = Logger.getLogger(AbstractFileProvider.class);
073    private String m_pageDirectory = "/tmp/";
074
075    protected String m_encoding;
076
077    protected WikiEngine m_engine;
078
079    public static final String PROP_CUSTOMPROP_MAXLIMIT = "custom.pageproperty.max.allowed";
080    public static final String PROP_CUSTOMPROP_MAXKEYLENGTH = "custom.pageproperty.key.length";
081    public static final String PROP_CUSTOMPROP_MAXVALUELENGTH = "custom.pageproperty.value.length";
082
083    public static final int DEFAULT_MAX_PROPLIMIT = 200;
084    public static final int DEFAULT_MAX_PROPKEYLENGTH = 255;
085    public static final int DEFAULT_MAX_PROPVALUELENGTH = 4096;
086
087    /**
088     * This parameter limits the number of custom page properties allowed on a page
089     */
090    public static int MAX_PROPLIMIT = DEFAULT_MAX_PROPLIMIT;
091    /**
092     * This number limits the length of a custom page property key length
093     * The default value here designed with future JDBC providers in mind.
094     */
095    public static int MAX_PROPKEYLENGTH = DEFAULT_MAX_PROPKEYLENGTH;
096    /**
097     * This number limits the length of a custom page property value length
098     * The default value here designed with future JDBC providers in mind.
099     */
100    public static int MAX_PROPVALUELENGTH = DEFAULT_MAX_PROPVALUELENGTH;
101
102    /**
103     *  Name of the property that defines where page directories are.
104     */
105    public static final String      PROP_PAGEDIR = "jspwiki.fileSystemProvider.pageDir";
106
107    /**
108     *  All files should have this extension to be recognized as JSPWiki files.
109     *  We default to .txt, because that is probably easiest for Windows users,
110     *  and guarantees correct handling.
111     */
112    public static final String FILE_EXT = ".txt";
113
114    /** The default encoding. */
115    public static final String DEFAULT_ENCODING = "ISO-8859-1";
116
117    private boolean m_windowsHackNeeded = false;
118
119    /**
120     *  {@inheritDoc}
121     *  @throws FileNotFoundException If the specified page directory does not exist.
122     *  @throws IOException In case the specified page directory is a file, not a directory.
123     */
124    public void initialize( WikiEngine engine, Properties properties )
125        throws NoRequiredPropertyException,
126               IOException, FileNotFoundException
127    {
128        log.debug("Initing FileSystemProvider");
129        m_pageDirectory = TextUtil.getCanonicalFilePathProperty(properties, PROP_PAGEDIR,
130                System.getProperty("user.home") + File.separator + "jspwiki-files");
131
132        File f = new File(m_pageDirectory);
133
134        if( !f.exists() )
135        {
136            if( !f.mkdirs() )
137            {
138                throw new IOException( "Failed to create page directory " + f.getAbsolutePath() + " , please check property "
139                                       + PROP_PAGEDIR );
140            }
141        }
142        else
143        {
144            if( !f.isDirectory() )
145            {
146                throw new IOException( "Page directory is not a directory: " + f.getAbsolutePath() );
147            }
148            if( !f.canWrite() )
149            {
150                throw new IOException( "Page directory is not writable: " + f.getAbsolutePath() );
151            }
152        }
153
154        m_engine = engine;
155
156        m_encoding = properties.getProperty( WikiEngine.PROP_ENCODING, DEFAULT_ENCODING );
157
158        String os = System.getProperty( "os.name" ).toLowerCase();
159
160        if( os.startsWith("windows") || os.equals("nt") )
161        {
162            m_windowsHackNeeded = true;
163        }
164
165        if (properties != null) {
166            MAX_PROPLIMIT = TextUtil.getIntegerProperty(properties,PROP_CUSTOMPROP_MAXLIMIT,DEFAULT_MAX_PROPLIMIT);
167            MAX_PROPKEYLENGTH = TextUtil.getIntegerProperty(properties,PROP_CUSTOMPROP_MAXKEYLENGTH,DEFAULT_MAX_PROPKEYLENGTH);
168            MAX_PROPVALUELENGTH = TextUtil.getIntegerProperty(properties,PROP_CUSTOMPROP_MAXVALUELENGTH,DEFAULT_MAX_PROPVALUELENGTH);
169        }
170
171        log.info( "Wikipages are read from '" + m_pageDirectory + "'" );
172    }
173
174
175    String getPageDirectory()
176    {
177        return m_pageDirectory;
178    }
179
180    private static final String[] WINDOWS_DEVICE_NAMES =
181    {
182        "con", "prn", "nul", "aux", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
183        "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9"
184    };
185
186    /**
187     *  This makes sure that the queried page name
188     *  is still readable by the file system.  For example, all XML entities
189     *  and slashes are encoded with the percent notation.
190     *
191     *  @param pagename The name to mangle
192     *  @return The mangled name.
193     */
194    protected String mangleName( String pagename )
195    {
196        pagename = TextUtil.urlEncode( pagename, m_encoding );
197
198        pagename = TextUtil.replaceString( pagename, "/", "%2F" );
199
200        //
201        //  Names which start with a dot must be escaped to prevent problems.
202        //  Since we use URL encoding, this is invisible in our unescaping.
203        //
204        if( pagename.startsWith( "." ) )
205        {
206            pagename = "%2E" + pagename.substring( 1 );
207        }
208
209        if( m_windowsHackNeeded )
210        {
211            String pn = pagename.toLowerCase();
212            for( int i = 0; i < WINDOWS_DEVICE_NAMES.length; i++ )
213            {
214                if( WINDOWS_DEVICE_NAMES[i].equals(pn) )
215                {
216                    pagename = "$$$" + pagename;
217                }
218            }
219        }
220
221        return pagename;
222    }
223
224    /**
225     *  This makes the reverse of mangleName.
226     *
227     *  @param filename The filename to unmangle
228     *  @return The unmangled name.
229     */
230    protected String unmangleName( String filename )
231    {
232        // The exception should never happen.
233        try
234        {
235            if( m_windowsHackNeeded && filename.startsWith( "$$$") && filename.length() > 3 )
236            {
237                filename = filename.substring(3);
238            }
239
240            return TextUtil.urlDecode( filename, m_encoding );
241        }
242        catch( UnsupportedEncodingException e )
243        {
244            throw new InternalWikiException("Faulty encoding; should never happen", e);
245        }
246    }
247
248    /**
249     *  Finds a Wiki page from the page repository.
250     *
251     *  @param page The name of the page.
252     *  @return A File to the page.  May be null.
253     */
254    protected File findPage( String page )
255    {
256        return new File( m_pageDirectory, mangleName(page)+FILE_EXT );
257    }
258
259    /**
260     *  {@inheritDoc}
261     */
262    public boolean pageExists( String page )
263    {
264        File pagefile = findPage( page );
265
266        return pagefile.exists();
267    }
268
269    /**
270     *  {@inheritDoc}
271     */
272    public boolean pageExists( String page, int version )
273    {
274        return pageExists (page);
275    }
276
277    /**
278     *  This implementation just returns the current version, as filesystem
279     *  does not provide versioning information for now.
280     *
281     *  @param page {@inheritDoc}
282     *  @param version {@inheritDoc}
283     *  @throws {@inheritDoc}
284     */
285    public String getPageText( String page, int version )
286        throws ProviderException
287    {
288        return getPageText( page );
289    }
290
291    /**
292     *  Read the text directly from the correct file.
293     */
294    private String getPageText( String page )
295    {
296        String result  = null;
297        InputStream in = null;
298
299        File pagedata = findPage( page );
300
301        if( pagedata.exists() )
302        {
303            if( pagedata.canRead() )
304            {
305                try
306                {
307                    in = new FileInputStream( pagedata );
308                    result = FileUtil.readContents( in, m_encoding );
309                }
310                catch( IOException e )
311                {
312                    log.error("Failed to read", e);
313                }
314                finally
315                {
316                    IOUtils.closeQuietly( in );
317                }
318            }
319            else
320            {
321                log.warn("Failed to read page '"+page+"' from '"+pagedata.getAbsolutePath()+"', possibly a permissions problem");
322            }
323        }
324        else
325        {
326            // This is okay.
327            log.info("New page '"+page+"'");
328        }
329
330        return result;
331    }
332
333    /**
334     *  {@inheritDoc}
335     */
336    public void putPageText( WikiPage page, String text )
337        throws ProviderException
338    {
339        File file = findPage( page.getName() );
340        PrintWriter out = null;
341
342        try
343        {
344            out = new PrintWriter(new OutputStreamWriter( new FileOutputStream( file ),
345                                                          m_encoding ));
346
347            out.print( text );
348        }
349        catch( IOException e )
350        {
351            log.error( "Saving failed", e );
352        }
353        finally
354        {
355            IOUtils.closeQuietly( out );
356        }
357    }
358
359    /**
360     *  {@inheritDoc}
361     */
362    public Collection getAllPages()
363        throws ProviderException
364    {
365        log.debug("Getting all pages...");
366
367        ArrayList<WikiPage> set = new ArrayList<WikiPage>();
368
369        File wikipagedir = new File( m_pageDirectory );
370
371        File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() );
372
373        if( wikipages == null )
374        {
375            log.error("Wikipages directory '" + m_pageDirectory + "' does not exist! Please check " + PROP_PAGEDIR + " in jspwiki.properties.");
376            throw new InternalWikiException("Page directory does not exist");
377        }
378
379        for( int i = 0; i < wikipages.length; i++ )
380        {
381            String wikiname = wikipages[i].getName();
382            int cutpoint = wikiname.lastIndexOf( FILE_EXT );
383
384            WikiPage page = getPageInfo( unmangleName(wikiname.substring(0,cutpoint)),
385                                         WikiPageProvider.LATEST_VERSION );
386            if( page == null )
387            {
388                // This should not really happen.
389                // FIXME: Should we throw an exception here?
390                log.error("Page "+wikiname+" was found in directory listing, but could not be located individually.");
391                continue;
392            }
393
394            set.add( page );
395        }
396
397        return set;
398    }
399
400    /**
401     *  Does not work.
402     *
403     *  @param date {@inheritDoc}
404     *  @return {@inheritDoc}
405     */
406    public Collection getAllChangedSince( Date date )
407    {
408        return new ArrayList(); // FIXME
409    }
410
411    /**
412     *  {@inheritDoc}
413     */
414    public int getPageCount()
415    {
416        File wikipagedir = new File( m_pageDirectory );
417
418        File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() );
419
420        return wikipages.length;
421    }
422
423    /**
424     * Iterates through all WikiPages, matches them against the given query,
425     * and returns a Collection of SearchResult objects.
426     *
427     * @param query {@inheritDoc}
428     * @return {@inheritDoc}
429     */
430    public Collection findPages( QueryItem[] query )
431    {
432        File wikipagedir = new File( m_pageDirectory );
433        TreeSet<SearchResult> res = new TreeSet<SearchResult>( new SearchResultComparator() );
434        SearchMatcher matcher = new SearchMatcher( m_engine, query );
435
436        File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() );
437
438        for( int i = 0; i < wikipages.length; i++ )
439        {
440            FileInputStream input = null;
441
442            // log.debug("Searching page "+wikipages[i].getPath() );
443
444            String filename = wikipages[i].getName();
445            int cutpoint    = filename.lastIndexOf( FILE_EXT );
446            String wikiname = filename.substring( 0, cutpoint );
447
448            wikiname = unmangleName( wikiname );
449
450            try
451            {
452                input = new FileInputStream( wikipages[i] );
453                String pagetext = FileUtil.readContents( input, m_encoding );
454                SearchResult comparison = matcher.matchPageContent( wikiname, pagetext );
455                if( comparison != null )
456                {
457                    res.add( comparison );
458                }
459            }
460            catch( IOException e )
461            {
462                log.error( "Failed to read " + filename, e );
463            }
464            finally
465            {
466                IOUtils.closeQuietly( input );
467            }
468        }
469
470        return res;
471    }
472
473    /**
474     *  Always returns the latest version, since FileSystemProvider
475     *  does not support versioning.
476     *
477     *  @param page {@inheritDoc}
478     *  @param version {@inheritDoc}
479     *  @return {@inheritDoc}
480     *  @throws {@inheritDoc}
481     */
482    public WikiPage getPageInfo( String page, int version )
483        throws ProviderException
484    {
485        File file = findPage( page );
486
487        if( !file.exists() )
488        {
489            return null;
490        }
491
492        WikiPage p = new WikiPage( m_engine, page );
493        p.setLastModified( new Date(file.lastModified()) );
494
495        return p;
496    }
497
498    /**
499     *  The FileSystemProvider provides only one version.
500     *
501     *  @param page {@inheritDoc}
502     *  @throws {@inheritDoc}
503     *  @return {@inheritDoc}
504     */
505    public List getVersionHistory( String page )
506        throws ProviderException
507    {
508        ArrayList<WikiPage> list = new ArrayList<WikiPage>();
509
510        list.add( getPageInfo( page, WikiPageProvider.LATEST_VERSION ) );
511
512        return list;
513    }
514
515    /**
516     *  {@inheritDoc}
517     */
518    public String getProviderInfo()
519    {
520        return "";
521    }
522
523    /**
524     *  {@inheritDoc}
525     */
526    public void deleteVersion( String pageName, int version )
527        throws ProviderException
528    {
529        if( version == WikiProvider.LATEST_VERSION )
530        {
531            File f = findPage( pageName );
532
533            f.delete();
534        }
535    }
536
537    /**
538     *  {@inheritDoc}
539     */
540    public void deletePage( String pageName )
541        throws ProviderException
542    {
543        File f = findPage( pageName );
544
545        f.delete();
546    }
547
548    /**
549     * Set the custom properties provided into the given page.
550     *
551     * @since 2.10.2
552     */
553    protected void setCustomProperties(WikiPage page, Properties properties) {
554        Enumeration propertyNames = properties.propertyNames();
555        while (propertyNames.hasMoreElements()) {
556            String key = (String) propertyNames.nextElement();
557            if (!key.equals(WikiPage.AUTHOR) && !key.equals(WikiPage.CHANGENOTE) && !key.equals(WikiPage.VIEWCOUNT)) {
558                page.setAttribute(key, properties.get(key));
559            }
560        }
561    }
562
563    /**
564     * Get custom properties using {@link #addCustomProperties(WikiPage, Properties)}, validate them using {@link #validateCustomPageProperties(Properties)}
565     * and add them to default properties provided
566     *
567     * @since 2.10.2
568     */
569    protected void getCustomProperties(WikiPage page, Properties defaultProperties) throws IOException {
570        Properties customPageProperties = addCustomProperties(page,defaultProperties);
571        validateCustomPageProperties(customPageProperties);
572        defaultProperties.putAll(customPageProperties);
573    }
574
575    /**
576     * By default all page attributes that start with "@" are returned as custom properties.
577     * This can be overwritten by custom FileSystemProviders to save additional properties.
578     * CustomPageProperties are validated by {@link #validateCustomPageProperties(Properties)}
579     *
580     * @since 2.10.2
581     * @param page the current page
582     * @param props the default properties of this page
583     * @return default implementation returns empty Properties.
584     */
585    protected Properties addCustomProperties(WikiPage page, Properties props) {
586        Properties customProperties = new Properties();
587        if (page != null) {
588            Map<String,Object> atts = page.getAttributes();
589            for (String key : atts.keySet()) {
590                Object value = atts.get(key);
591                if (key.startsWith("@") && value != null) {
592                    customProperties.put(key,value.toString());
593                }
594            }
595
596        }
597        return customProperties;
598    }
599
600    /**
601     * Default validation, validates that key and value is ASCII <code>StringUtils.isAsciiPrintable()</code> and within lengths set up in jspwiki-custom.properties.
602     * This can be overwritten by custom FileSystemProviders to validate additional properties
603     * See https://issues.apache.org/jira/browse/JSPWIKI-856
604     * @since 2.10.2
605     * @param customProperties the custom page properties being added
606     */
607    protected void validateCustomPageProperties(Properties customProperties) throws IOException {
608        // Default validation rules
609        if (customProperties != null && !customProperties.isEmpty()) {
610            if (customProperties.size()>MAX_PROPLIMIT) {
611                throw new IOException("Too many custom properties. You are adding "+customProperties.size()+", but max limit is "+MAX_PROPLIMIT);
612            }
613            Enumeration propertyNames = customProperties.propertyNames();
614            while (propertyNames.hasMoreElements()) {
615                String key = (String) propertyNames.nextElement();
616                String value = (String)customProperties.get(key);
617                if (key != null) {
618                    if (key.length()>MAX_PROPKEYLENGTH) {
619                        throw new IOException("Custom property key "+key+" is too long. Max allowed length is "+MAX_PROPKEYLENGTH);
620                    }
621                    if (!StringUtils.isAsciiPrintable(key)) {
622                        throw new IOException("Custom property key "+key+" is not simple ASCII!");
623                    }
624                }
625                if (value != null) {
626                    if (value.length()>MAX_PROPVALUELENGTH) {
627                        throw new IOException("Custom property key "+key+" has value that is too long. Value="+value+". Max allowed length is "+MAX_PROPVALUELENGTH);
628                    }
629                    if (!StringUtils.isAsciiPrintable(value)) {
630                        throw new IOException("Custom property key "+key+" has value that is not simple ASCII! Value="+value);
631                    }
632                }
633            }
634        }
635    }
636
637    /**
638     *  A simple filter which filters only those filenames which correspond to the
639     *  file extension used.
640     */
641    public static class WikiFileFilter
642        implements FilenameFilter
643    {
644        /**
645         *  {@inheritDoc}
646         */
647        public boolean accept( File dir, String name )
648        {
649            return name.endsWith( FILE_EXT );
650        }
651    }
652}