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.log4j.Logger; 023import org.apache.wiki.api.core.Engine; 024import org.apache.wiki.api.core.Page; 025import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 026import org.apache.wiki.api.exceptions.ProviderException; 027import org.apache.wiki.api.providers.PageProvider; 028import org.apache.wiki.api.providers.WikiProvider; 029import org.apache.wiki.api.search.QueryItem; 030import org.apache.wiki.api.search.SearchResult; 031import org.apache.wiki.api.spi.Wiki; 032import org.apache.wiki.search.SearchMatcher; 033import org.apache.wiki.search.SearchResultComparator; 034import org.apache.wiki.util.FileUtil; 035import org.apache.wiki.util.TextUtil; 036 037import java.io.File; 038import java.io.FileInputStream; 039import java.io.FileNotFoundException; 040import java.io.FileOutputStream; 041import java.io.FilenameFilter; 042import java.io.IOException; 043import java.io.InputStream; 044import java.io.OutputStreamWriter; 045import java.io.PrintWriter; 046import java.nio.charset.StandardCharsets; 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 = Logger.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 = false; 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 for( final String windowsDeviceName : WINDOWS_DEVICE_NAMES ) { 180 if( windowsDeviceName.equals( pn ) ) { 181 pagename = "$$$" + pagename; 182 } 183 } 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 = new FileInputStream( pagedata ) ) { 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( new FileOutputStream( file ), 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 FileInputStream input = new FileInputStream( wikipage ) ) { 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}