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