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