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