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