| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| package org.apache.wiki.providers; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.log4j.Logger; |
| import org.apache.wiki.WikiPage; |
| import org.apache.wiki.api.core.Engine; |
| import org.apache.wiki.api.exceptions.NoRequiredPropertyException; |
| import org.apache.wiki.api.exceptions.ProviderException; |
| import org.apache.wiki.api.providers.WikiProvider; |
| import org.apache.wiki.search.QueryItem; |
| import org.apache.wiki.search.SearchMatcher; |
| import org.apache.wiki.search.SearchResult; |
| import org.apache.wiki.search.SearchResultComparator; |
| import org.apache.wiki.util.FileUtil; |
| import org.apache.wiki.util.TextUtil; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.FilenameFilter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.Enumeration; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.TreeSet; |
| |
| |
| /** |
| * Provides a simple directory based repository for Wiki pages. |
| * <P> |
| * All files have ".txt" appended to make life easier for those who insist on using Windows or other software which makes assumptions |
| * on the files contents based on its name. |
| * <p> |
| * This class functions as a superclass to all file based providers. |
| * |
| * @since 2.1.21. |
| */ |
| public abstract class AbstractFileProvider implements WikiPageProvider { |
| |
| private static final Logger log = Logger.getLogger(AbstractFileProvider.class); |
| private String m_pageDirectory = "/tmp/"; |
| protected String m_encoding; |
| |
| protected Engine m_engine; |
| |
| public static final String PROP_CUSTOMPROP_MAXLIMIT = "custom.pageproperty.max.allowed"; |
| public static final String PROP_CUSTOMPROP_MAXKEYLENGTH = "custom.pageproperty.key.length"; |
| public static final String PROP_CUSTOMPROP_MAXVALUELENGTH = "custom.pageproperty.value.length"; |
| |
| public static final int DEFAULT_MAX_PROPLIMIT = 200; |
| public static final int DEFAULT_MAX_PROPKEYLENGTH = 255; |
| public static final int DEFAULT_MAX_PROPVALUELENGTH = 4096; |
| |
| /** |
| * This parameter limits the number of custom page properties allowed on a page |
| */ |
| public static int MAX_PROPLIMIT = DEFAULT_MAX_PROPLIMIT; |
| |
| /** |
| * This number limits the length of a custom page property key length. The default value here designed with future JDBC providers in mind. |
| */ |
| public static int MAX_PROPKEYLENGTH = DEFAULT_MAX_PROPKEYLENGTH; |
| |
| /** |
| * This number limits the length of a custom page property value length. The default value here designed with future JDBC providers in mind. |
| */ |
| public static int MAX_PROPVALUELENGTH = DEFAULT_MAX_PROPVALUELENGTH; |
| |
| /** |
| * Name of the property that defines where page directories are. |
| */ |
| public static final String PROP_PAGEDIR = "jspwiki.fileSystemProvider.pageDir"; |
| |
| /** |
| * All files should have this extension to be recognized as JSPWiki files. |
| * We default to .txt, because that is probably easiest for Windows users, |
| * and guarantees correct handling. |
| */ |
| public static final String FILE_EXT = ".txt"; |
| |
| /** The default encoding. */ |
| public static final String DEFAULT_ENCODING = StandardCharsets.ISO_8859_1.toString(); |
| |
| private boolean m_windowsHackNeeded = false; |
| |
| /** |
| * {@inheritDoc} |
| * @throws FileNotFoundException If the specified page directory does not exist. |
| * @throws IOException In case the specified page directory is a file, not a directory. |
| */ |
| @Override |
| public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException, FileNotFoundException { |
| log.debug( "Initing FileSystemProvider" ); |
| m_pageDirectory = TextUtil.getCanonicalFilePathProperty( properties, PROP_PAGEDIR, |
| System.getProperty( "user.home" ) + File.separator + "jspwiki-files" ); |
| |
| final File f = new File( m_pageDirectory ); |
| |
| if( !f.exists() ) { |
| if( !f.mkdirs() ) { |
| throw new IOException( |
| "Failed to create page directory " + f.getAbsolutePath() + " , please check property " + PROP_PAGEDIR ); |
| } |
| } else { |
| if( !f.isDirectory() ) { |
| throw new IOException( "Page directory is not a directory: " + f.getAbsolutePath() ); |
| } |
| if( !f.canWrite() ) { |
| throw new IOException( "Page directory is not writable: " + f.getAbsolutePath() ); |
| } |
| } |
| |
| m_engine = engine; |
| m_encoding = properties.getProperty( Engine.PROP_ENCODING, DEFAULT_ENCODING ); |
| final String os = System.getProperty( "os.name" ).toLowerCase(); |
| if( os.startsWith( "windows" ) || os.equals( "nt" ) ) { |
| m_windowsHackNeeded = true; |
| } |
| |
| MAX_PROPLIMIT = TextUtil.getIntegerProperty( properties, PROP_CUSTOMPROP_MAXLIMIT, DEFAULT_MAX_PROPLIMIT ); |
| MAX_PROPKEYLENGTH = TextUtil.getIntegerProperty( properties, PROP_CUSTOMPROP_MAXKEYLENGTH, DEFAULT_MAX_PROPKEYLENGTH ); |
| MAX_PROPVALUELENGTH = TextUtil.getIntegerProperty( properties, PROP_CUSTOMPROP_MAXVALUELENGTH, DEFAULT_MAX_PROPVALUELENGTH ); |
| |
| log.info( "Wikipages are read from '" + m_pageDirectory + "'" ); |
| } |
| |
| |
| String getPageDirectory() |
| { |
| return m_pageDirectory; |
| } |
| |
| private static final String[] WINDOWS_DEVICE_NAMES = { |
| "con", "prn", "nul", "aux", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", |
| "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9" |
| }; |
| |
| /** |
| * This makes sure that the queried page name is still readable by the file system. For example, all XML entities |
| * and slashes are encoded with the percent notation. |
| * |
| * @param pagename The name to mangle |
| * @return The mangled name. |
| */ |
| protected String mangleName( String pagename ) { |
| pagename = TextUtil.urlEncode( pagename, m_encoding ); |
| pagename = TextUtil.replaceString( pagename, "/", "%2F" ); |
| |
| // Names which start with a dot must be escaped to prevent problems. Since we use URL encoding, this is invisible in our unescaping. |
| if( pagename.startsWith( "." ) ) { |
| pagename = "%2E" + pagename.substring( 1 ); |
| } |
| |
| if( m_windowsHackNeeded ) { |
| final String pn = pagename.toLowerCase(); |
| for( final String windowsDeviceName : WINDOWS_DEVICE_NAMES ) { |
| if( windowsDeviceName.equals( pn ) ) { |
| pagename = "$$$" + pagename; |
| } |
| } |
| } |
| |
| return pagename; |
| } |
| |
| /** |
| * This makes the reverse of mangleName. |
| * |
| * @param filename The filename to unmangle |
| * @return The unmangled name. |
| */ |
| protected String unmangleName( String filename ) { |
| // The exception should never happen. |
| if( m_windowsHackNeeded && filename.startsWith( "$$$" ) && filename.length() > 3 ) { |
| filename = filename.substring( 3 ); |
| } |
| |
| return TextUtil.urlDecode( filename, m_encoding ); |
| } |
| |
| /** |
| * Finds a Wiki page from the page repository. |
| * |
| * @param page The name of the page. |
| * @return A File to the page. May be null. |
| */ |
| protected File findPage( final String page ) |
| { |
| return new File( m_pageDirectory, mangleName(page)+FILE_EXT ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean pageExists( final String page ) { |
| return findPage( page ).exists(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean pageExists( final String page, final int version ) { |
| return pageExists( page ); |
| } |
| |
| /** |
| * This implementation just returns the current version, as filesystem does not provide versioning information for now. |
| * |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getPageText( final String page, final int version ) throws ProviderException { |
| return getPageText( page ); |
| } |
| |
| /** |
| * Read the text directly from the correct file. |
| */ |
| private String getPageText( final String page ) { |
| String result = null; |
| final File pagedata = findPage( page ); |
| if( pagedata.exists() ) { |
| if( pagedata.canRead() ) { |
| try( final InputStream in = new FileInputStream( pagedata ) ) { |
| result = FileUtil.readContents( in, m_encoding ); |
| } catch( final IOException e ) { |
| log.error( "Failed to read", e ); |
| } |
| } else { |
| log.warn( "Failed to read page '" + page + "' from '" + pagedata.getAbsolutePath() + "', possibly a permissions problem" ); |
| } |
| } else { |
| // This is okay. |
| log.info( "New page '" + page + "'" ); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void putPageText( final WikiPage page, final String text ) throws ProviderException { |
| final File file = findPage( page.getName() ); |
| try( final PrintWriter out = new PrintWriter( new OutputStreamWriter( new FileOutputStream( file ), m_encoding ) ) ) { |
| out.print( text ); |
| } catch( final IOException e ) { |
| log.error( "Saving failed", e ); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Collection< WikiPage > getAllPages() throws ProviderException { |
| log.debug("Getting all pages..."); |
| final ArrayList< WikiPage > set = new ArrayList<>(); |
| final File wikipagedir = new File( m_pageDirectory ); |
| final File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); |
| |
| if( wikipages == null ) { |
| log.error("Wikipages directory '" + m_pageDirectory + "' does not exist! Please check " + PROP_PAGEDIR + " in jspwiki.properties."); |
| throw new ProviderException( "Page directory does not exist" ); |
| } |
| |
| for( final File wikipage : wikipages ) { |
| final String wikiname = wikipage.getName(); |
| final int cutpoint = wikiname.lastIndexOf( FILE_EXT ); |
| |
| final WikiPage page = getPageInfo( unmangleName( wikiname.substring( 0, cutpoint ) ), WikiPageProvider.LATEST_VERSION ); |
| if( page == null ) { |
| // This should not really happen. |
| // FIXME: Should we throw an exception here? |
| log.error( "Page " + wikiname + " was found in directory listing, but could not be located individually." ); |
| continue; |
| } |
| |
| set.add( page ); |
| } |
| |
| return set; |
| } |
| |
| /** |
| * Does not work. |
| * |
| * @param date {@inheritDoc} |
| * @return {@inheritDoc} |
| */ |
| @Override |
| public Collection< WikiPage > getAllChangedSince( final Date date ) |
| { |
| return new ArrayList<>(); // FIXME |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int getPageCount() { |
| final File wikipagedir = new File( m_pageDirectory ); |
| final File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); |
| return wikipages != null ? wikipages.length : 0; |
| } |
| |
| /** |
| * Iterates through all WikiPages, matches them against the given query, and returns a Collection of SearchResult objects. |
| * |
| * {@inheritDoc} |
| */ |
| @Override |
| public Collection< SearchResult > findPages( final QueryItem[] query ) { |
| final File wikipagedir = new File( m_pageDirectory ); |
| final TreeSet< SearchResult > res = new TreeSet<>( new SearchResultComparator() ); |
| final SearchMatcher matcher = new SearchMatcher( m_engine, query ); |
| final File[] wikipages = wikipagedir.listFiles( new WikiFileFilter() ); |
| |
| if( wikipages != null ) { |
| for( final File wikipage : wikipages ) { |
| final String filename = wikipage.getName(); |
| final int cutpoint = filename.lastIndexOf( FILE_EXT ); |
| final String wikiname = unmangleName( filename.substring( 0, cutpoint ) ); |
| try( final FileInputStream input = new FileInputStream( wikipage ) ) { |
| final String pagetext = FileUtil.readContents( input, m_encoding ); |
| final SearchResult comparison = matcher.matchPageContent( wikiname, pagetext ); |
| if( comparison != null ) { |
| res.add( comparison ); |
| } |
| } catch( final IOException e ) { |
| log.error( "Failed to read " + filename, e ); |
| } |
| } |
| } |
| |
| return res; |
| } |
| |
| /** |
| * Always returns the latest version, since FileSystemProvider |
| * does not support versioning. |
| * |
| * {@inheritDoc} |
| */ |
| @Override |
| public WikiPage getPageInfo( final String page, final int version ) throws ProviderException { |
| final File file = findPage( page ); |
| if( !file.exists() ) { |
| return null; |
| } |
| |
| final WikiPage p = new WikiPage( m_engine, page ); |
| p.setLastModified( new Date( file.lastModified() ) ); |
| |
| return p; |
| } |
| |
| /** |
| * The FileSystemProvider provides only one version. |
| * |
| * {@inheritDoc} |
| */ |
| @Override |
| public List< WikiPage > getVersionHistory( final String page ) throws ProviderException { |
| final ArrayList< WikiPage > list = new ArrayList<>(); |
| list.add( getPageInfo( page, WikiPageProvider.LATEST_VERSION ) ); |
| |
| return list; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getProviderInfo() |
| { |
| return ""; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void deleteVersion( final String pageName, final int version ) throws ProviderException { |
| if( version == WikiProvider.LATEST_VERSION ) { |
| final File f = findPage( pageName ); |
| f.delete(); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void deletePage( final String pageName ) throws ProviderException { |
| final File f = findPage( pageName ); |
| f.delete(); |
| } |
| |
| /** |
| * Set the custom properties provided into the given page. |
| * |
| * @since 2.10.2 |
| */ |
| protected void setCustomProperties( final WikiPage page, final Properties properties ) { |
| final Enumeration< ? > propertyNames = properties.propertyNames(); |
| while( propertyNames.hasMoreElements() ) { |
| final String key = ( String )propertyNames.nextElement(); |
| if( !key.equals( WikiPage.AUTHOR ) && !key.equals( WikiPage.CHANGENOTE ) && !key.equals( WikiPage.VIEWCOUNT ) ) { |
| page.setAttribute( key, properties.get( key ) ); |
| } |
| } |
| } |
| |
| /** |
| * Get custom properties using {@link #addCustomProperties(WikiPage, Properties)}, validate them using {@link #validateCustomPageProperties(Properties)} |
| * and add them to default properties provided |
| * |
| * @since 2.10.2 |
| */ |
| protected void getCustomProperties( final WikiPage page, final Properties defaultProperties ) throws IOException { |
| final Properties customPageProperties = addCustomProperties( page, defaultProperties ); |
| validateCustomPageProperties( customPageProperties ); |
| defaultProperties.putAll( customPageProperties ); |
| } |
| |
| /** |
| * By default all page attributes that start with "@" are returned as custom properties. |
| * This can be overwritten by custom FileSystemProviders to save additional properties. |
| * CustomPageProperties are validated by {@link #validateCustomPageProperties(Properties)} |
| * |
| * @since 2.10.2 |
| * @param page the current page |
| * @param props the default properties of this page |
| * @return default implementation returns empty Properties. |
| */ |
| protected Properties addCustomProperties( final WikiPage page, final Properties props ) { |
| final Properties customProperties = new Properties(); |
| if( page != null ) { |
| final Map< String, Object > atts = page.getAttributes(); |
| for( final String key : atts.keySet() ) { |
| final Object value = atts.get( key ); |
| if( key.startsWith( "@" ) && value != null ) { |
| customProperties.put( key, value.toString() ); |
| } |
| } |
| |
| } |
| return customProperties; |
| } |
| |
| /** |
| * Default validation, validates that key and value is ASCII <code>StringUtils.isAsciiPrintable()</code> and within lengths set up in jspwiki-custom.properties. |
| * This can be overwritten by custom FileSystemProviders to validate additional properties |
| * See https://issues.apache.org/jira/browse/JSPWIKI-856 |
| * @since 2.10.2 |
| * @param customProperties the custom page properties being added |
| */ |
| protected void validateCustomPageProperties( final Properties customProperties ) throws IOException { |
| // Default validation rules |
| if( customProperties != null && !customProperties.isEmpty() ) { |
| if( customProperties.size() > MAX_PROPLIMIT ) { |
| throw new IOException( "Too many custom properties. You are adding " + customProperties.size() + ", but max limit is " + MAX_PROPLIMIT ); |
| } |
| final Enumeration< ? > propertyNames = customProperties.propertyNames(); |
| while( propertyNames.hasMoreElements() ) { |
| final String key = ( String )propertyNames.nextElement(); |
| final String value = ( String )customProperties.get( key ); |
| if( key != null ) { |
| if( key.length() > MAX_PROPKEYLENGTH ) { |
| throw new IOException( "Custom property key " + key + " is too long. Max allowed length is " + MAX_PROPKEYLENGTH ); |
| } |
| if( !StringUtils.isAsciiPrintable( key ) ) { |
| throw new IOException( "Custom property key " + key + " is not simple ASCII!" ); |
| } |
| } |
| if( value != null ) { |
| if( value.length() > MAX_PROPVALUELENGTH ) { |
| throw new IOException( "Custom property key " + key + " has value that is too long. Value=" + value + ". Max allowed length is " + MAX_PROPVALUELENGTH ); |
| } |
| if( !StringUtils.isAsciiPrintable( value ) ) { |
| throw new IOException( "Custom property key " + key + " has value that is not simple ASCII! Value=" + value ); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * A simple filter which filters only those filenames which correspond to the |
| * file extension used. |
| */ |
| public static class WikiFileFilter implements FilenameFilter { |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean accept( final File dir, final String name ) { |
| return name.endsWith( FILE_EXT ); |
| } |
| } |
| |
| } |