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.attachment; 020 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Comparator; 029import java.util.Date; 030import java.util.List; 031import java.util.Properties; 032 033import org.apache.commons.lang.StringUtils; 034import org.apache.log4j.Logger; 035import org.apache.wiki.WikiContext; 036import org.apache.wiki.WikiEngine; 037import org.apache.wiki.WikiPage; 038import org.apache.wiki.WikiProvider; 039import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 040import org.apache.wiki.api.exceptions.ProviderException; 041import org.apache.wiki.api.exceptions.WikiException; 042import org.apache.wiki.pages.PageManager; 043import org.apache.wiki.parser.MarkupParser; 044import org.apache.wiki.providers.WikiAttachmentProvider; 045import org.apache.wiki.util.ClassUtil; 046 047import net.sf.ehcache.Cache; 048import net.sf.ehcache.CacheManager; 049import net.sf.ehcache.Element; 050 051 052/** 053 * Provides facilities for handling attachments. All attachment handling goes through this class. 054 * <p> 055 * The AttachmentManager provides a facade towards the current WikiAttachmentProvider that is in use. 056 * It is created by the WikiEngine as a singleton object, and can be requested through the WikiEngine. 057 * 058 * @since 1.9.28 059 */ 060public class AttachmentManager 061{ 062 /** 063 * The property name for defining the attachment provider class name. 064 */ 065 public static final String PROP_PROVIDER = "jspwiki.attachmentProvider"; 066 067 /** 068 * The maximum size of attachments that can be uploaded. 069 */ 070 public static final String PROP_MAXSIZE = "jspwiki.attachment.maxsize"; 071 072 /** 073 * A space-separated list of attachment types which can be uploaded 074 */ 075 public static final String PROP_ALLOWEDEXTENSIONS = "jspwiki.attachment.allowed"; 076 077 /** 078 * A space-separated list of attachment types which cannot be uploaded 079 */ 080 public static final String PROP_FORDBIDDENEXTENSIONS = "jspwiki.attachment.forbidden"; 081 082 static Logger log = Logger.getLogger( AttachmentManager.class ); 083 private WikiAttachmentProvider m_provider; 084 private WikiEngine m_engine; 085 private CacheManager m_cacheManager = CacheManager.getInstance(); 086 087 private Cache m_dynamicAttachments; 088 /** Name of the page cache. */ 089 public static final String CACHE_NAME = "jspwiki.dynamicAttachmentCache"; 090 091 /** The capacity of the cache, if you want something else, tweak ehcache.xml. */ 092 public static final int DEFAULT_CACHECAPACITY = 1000; 093 094 /** 095 * Creates a new AttachmentManager. Note that creation will never fail, 096 * but it's quite likely that attachments do not function. 097 * <p> 098 * <b>DO NOT CREATE</b> an AttachmentManager on your own, unless you really 099 * know what you're doing. Just use WikiEngine.getAttachmentManager() if 100 * you're making a module for JSPWiki. 101 * 102 * @param engine The wikiengine that owns this attachment manager. 103 * @param props A list of properties from which the AttachmentManager will seek 104 * its configuration. Typically this is the "jspwiki.properties". 105 */ 106 107 // FIXME: Perhaps this should fail somehow. 108 public AttachmentManager( WikiEngine engine, Properties props ) 109 { 110 String classname; 111 112 m_engine = engine; 113 114 115 // 116 // If user wants to use a cache, then we'll use the CachingProvider. 117 // 118 boolean useCache = "true".equals(props.getProperty( PageManager.PROP_USECACHE )); 119 120 if( useCache ) 121 { 122 classname = "org.apache.wiki.providers.CachingAttachmentProvider"; 123 } 124 else 125 { 126 classname = props.getProperty( PROP_PROVIDER ); 127 } 128 129 // 130 // If no class defined, then will just simply fail. 131 // 132 if( classname == null ) 133 { 134 log.info( "No attachment provider defined - disabling attachment support." ); 135 return; 136 } 137 138 // 139 // Create and initialize the provider. 140 // 141 String cacheName = engine.getApplicationName() + "." + CACHE_NAME; 142 try { 143 if (m_cacheManager.cacheExists(cacheName)) { 144 m_dynamicAttachments = m_cacheManager.getCache(cacheName); 145 } else { 146 log.info("cache with name " + cacheName + " not found in ehcache.xml, creating it with defaults."); 147 m_dynamicAttachments = new Cache(cacheName, DEFAULT_CACHECAPACITY, false, false, 0, 0); 148 m_cacheManager.addCache(m_dynamicAttachments); 149 } 150 151 Class<?> providerclass = ClassUtil.findClass("org.apache.wiki.providers", classname); 152 153 m_provider = (WikiAttachmentProvider) providerclass.newInstance(); 154 155 m_provider.initialize(m_engine, props); 156 } catch( ClassNotFoundException e ) 157 { 158 log.error( "Attachment provider class not found",e); 159 } 160 catch( InstantiationException e ) 161 { 162 log.error( "Attachment provider could not be created", e ); 163 } 164 catch( IllegalAccessException e ) 165 { 166 log.error( "You may not access the attachment provider class", e ); 167 } 168 catch( NoRequiredPropertyException e ) 169 { 170 log.error( "Attachment provider did not find a property that it needed: "+e.getMessage(), e ); 171 m_provider = null; // No, it did not work. 172 } 173 catch( IOException e ) 174 { 175 log.error( "Attachment provider reports IO error", e ); 176 m_provider = null; 177 } 178 } 179 180 /** 181 * Returns true, if attachments are enabled and running. 182 * 183 * @return A boolean value indicating whether attachment functionality is enabled. 184 */ 185 public boolean attachmentsEnabled() 186 { 187 return m_provider != null; 188 } 189 190 /** 191 * Gets info on a particular attachment, latest version. 192 * 193 * @param name A full attachment name. 194 * @return Attachment, or null, if no such attachment exists. 195 * @throws ProviderException If something goes wrong. 196 */ 197 public Attachment getAttachmentInfo( String name ) 198 throws ProviderException 199 { 200 return getAttachmentInfo( name, WikiProvider.LATEST_VERSION ); 201 } 202 203 /** 204 * Gets info on a particular attachment with the given version. 205 * 206 * @param name A full attachment name. 207 * @param version A version number. 208 * @return Attachment, or null, if no such attachment or version exists. 209 * @throws ProviderException If something goes wrong. 210 */ 211 212 public Attachment getAttachmentInfo( String name, int version ) 213 throws ProviderException 214 { 215 if( name == null ) 216 { 217 return null; 218 } 219 220 return getAttachmentInfo( null, name, version ); 221 } 222 223 /** 224 * Figures out the full attachment name from the context and 225 * attachment name. 226 * 227 * @param context The current WikiContext 228 * @param attachmentname The file name of the attachment. 229 * @return Attachment, or null, if no such attachment exists. 230 * @throws ProviderException If something goes wrong. 231 */ 232 public Attachment getAttachmentInfo( WikiContext context, 233 String attachmentname ) 234 throws ProviderException 235 { 236 return getAttachmentInfo( context, attachmentname, WikiProvider.LATEST_VERSION ); 237 } 238 239 /** 240 * Figures out the full attachment name from the context and attachment name. 241 * 242 * @param context The current WikiContext 243 * @param attachmentname The file name of the attachment. 244 * @return Attachment, or null, if no such attachment exists. 245 * @throws ProviderException If something goes wrong. 246 */ 247 public String getAttachmentInfoName( WikiContext context, 248 String attachmentname ) 249 { 250 Attachment att = null; 251 252 try 253 { 254 att = getAttachmentInfo( context, attachmentname ); 255 } 256 catch( ProviderException e ) 257 { 258 log.warn("Finding attachments failed: ",e); 259 return null; 260 } 261 262 if( att != null ) 263 { 264 return att.getName(); 265 } 266 else if( attachmentname.indexOf('/') != -1 ) 267 { 268 return attachmentname; 269 } 270 271 return null; 272 } 273 274 /** 275 * Figures out the full attachment name from the context and 276 * attachment name. 277 * 278 * @param context The current WikiContext 279 * @param attachmentname The file name of the attachment. 280 * @param version A particular version. 281 * @return Attachment, or null, if no such attachment or version exists. 282 * @throws ProviderException If something goes wrong. 283 */ 284 285 public Attachment getAttachmentInfo( WikiContext context, 286 String attachmentname, 287 int version ) 288 throws ProviderException 289 { 290 if( m_provider == null ) 291 { 292 return null; 293 } 294 295 WikiPage currentPage = null; 296 297 if( context != null ) 298 { 299 currentPage = context.getPage(); 300 } 301 302 // 303 // Figure out the parent page of this attachment. If we can't find it, 304 // we'll assume this refers directly to the attachment. 305 // 306 int cutpt = attachmentname.lastIndexOf('/'); 307 308 if( cutpt != -1 ) 309 { 310 String parentPage = attachmentname.substring(0,cutpt); 311 parentPage = MarkupParser.cleanLink( parentPage ); 312 attachmentname = attachmentname.substring(cutpt+1); 313 314 // If we for some reason have an empty parent page name; 315 // this can't be an attachment 316 if(parentPage.length() == 0) return null; 317 318 currentPage = m_engine.getPage( parentPage ); 319 320 // 321 // Go check for legacy name 322 // 323 // FIXME: This should be resolved using CommandResolver, 324 // not this adhoc way. This also assumes that the 325 // legacy charset is a subset of the full allowed set. 326 if( currentPage == null ) 327 { 328 currentPage = m_engine.getPage( MarkupParser.wikifyLink( parentPage ) ); 329 } 330 } 331 332 // 333 // If the page cannot be determined, we cannot possibly find the 334 // attachments. 335 // 336 if( currentPage == null || currentPage.getName().length() == 0 ) 337 { 338 return null; 339 } 340 341 // System.out.println("Seeking info on "+currentPage+"::"+attachmentname); 342 343 // 344 // Finally, figure out whether this is a real attachment or a generated attachment. 345 // 346 Attachment att = getDynamicAttachment( currentPage.getName()+"/"+attachmentname ); 347 348 if( att == null ) 349 { 350 att = m_provider.getAttachmentInfo( currentPage, attachmentname, version ); 351 } 352 353 return att; 354 } 355 356 /** 357 * Returns the list of attachments associated with a given wiki page. 358 * If there are no attachments, returns an empty Collection. 359 * 360 * @param wikipage The wiki page from which you are seeking attachments for. 361 * @return a valid collection of attachments. 362 * @throws ProviderException If there was something wrong in the backend. 363 */ 364 public List< Attachment > listAttachments( WikiPage wikipage ) throws ProviderException { 365 if( m_provider == null ) { 366 return new ArrayList<>(); 367 } 368 369 List< Attachment >atts = new ArrayList<>( m_provider.listAttachments( wikipage ) ); 370 Collections.< Attachment >sort( atts, Comparator.comparing( Attachment::getName, m_engine.getPageManager().getPageSorter() ) ); 371 372 return atts; 373 } 374 375 /** 376 * Returns true, if the page has any attachments at all. This is 377 * a convinience method. 378 * 379 * 380 * @param wikipage The wiki page from which you are seeking attachments for. 381 * @return True, if the page has attachments, else false. 382 */ 383 public boolean hasAttachments( WikiPage wikipage ) 384 { 385 try 386 { 387 return listAttachments( wikipage ).size() > 0; 388 } 389 catch( Exception e ) {} 390 391 return false; 392 } 393 394 /** 395 * Finds a (real) attachment from the repository as a stream. 396 * 397 * @param att Attachment 398 * @return An InputStream to read from. May return null, if 399 * attachments are disabled. 400 * @throws IOException If the stream cannot be opened 401 * @throws ProviderException If the backend fails due to some other reason. 402 */ 403 public InputStream getAttachmentStream( Attachment att ) 404 throws IOException, 405 ProviderException 406 { 407 return getAttachmentStream( null, att ); 408 } 409 410 /** 411 * Returns an attachment stream using the particular WikiContext. This method 412 * should be used instead of getAttachmentStream(Attachment), since it also allows 413 * the DynamicAttachments to function. 414 * 415 * @param ctx The Wiki Context 416 * @param att The Attachment to find 417 * @return An InputStream. May return null, if attachments are disabled. You must 418 * take care of closing it. 419 * @throws ProviderException If the backend fails due to some reason 420 * @throws IOException If the stream cannot be opened 421 */ 422 public InputStream getAttachmentStream( WikiContext ctx, Attachment att ) 423 throws ProviderException, IOException 424 { 425 if( m_provider == null ) 426 { 427 return null; 428 } 429 430 if( att instanceof DynamicAttachment ) 431 { 432 return ((DynamicAttachment)att).getProvider().getAttachmentData( ctx, att ); 433 } 434 435 return m_provider.getAttachmentData( att ); 436 } 437 438 439 440 /** 441 * Stores a dynamic attachment. Unlike storeAttachment(), this just stores 442 * the attachment in the memory. 443 * 444 * @param ctx A WikiContext 445 * @param att An attachment to store 446 */ 447 public void storeDynamicAttachment( WikiContext ctx, DynamicAttachment att ) 448 { 449 m_dynamicAttachments.put(new Element(att.getName(), att)); 450 } 451 452 /** 453 * Finds a DynamicAttachment. Normally, you should just use getAttachmentInfo(), 454 * since that will find also DynamicAttachments. 455 * 456 * @param name The name of the attachment to look for 457 * @return An Attachment, or null. 458 * @see #getAttachmentInfo(String) 459 */ 460 461 public DynamicAttachment getDynamicAttachment(String name) { 462 Element element = m_dynamicAttachments.get(name); 463 if (element != null) { 464 return (DynamicAttachment) element.getObjectValue(); 465 } else { 466 // 467 // Remove from cache, it has expired. 468 // 469 m_dynamicAttachments.put(new Element(name, null)); 470 471 return null; 472 } 473 } 474 475 /** 476 * Stores an attachment that lives in the given file. 477 * If the attachment did not exist previously, this method 478 * will create it. If it did exist, it stores a new version. 479 * 480 * @param att Attachment to store this under. 481 * @param source A file to read from. 482 * 483 * @throws IOException If writing the attachment failed. 484 * @throws ProviderException If something else went wrong. 485 */ 486 public void storeAttachment( Attachment att, File source ) 487 throws IOException, 488 ProviderException 489 { 490 FileInputStream in = null; 491 492 try 493 { 494 in = new FileInputStream( source ); 495 storeAttachment( att, in ); 496 } 497 finally 498 { 499 if( in != null ) in.close(); 500 } 501 } 502 503 /** 504 * Stores an attachment directly from a stream. 505 * If the attachment did not exist previously, this method 506 * will create it. If it did exist, it stores a new version. 507 * 508 * @param att Attachment to store this under. 509 * @param in InputStream from which the attachment contents will be read. 510 * 511 * @throws IOException If writing the attachment failed. 512 * @throws ProviderException If something else went wrong. 513 */ 514 public void storeAttachment( Attachment att, InputStream in ) 515 throws IOException, 516 ProviderException 517 { 518 if( m_provider == null ) 519 { 520 return; 521 } 522 523 // 524 // Checks if the actual, real page exists without any modifications 525 // or aliases. We cannot store an attachment to a non-existent page. 526 // 527 if( !m_engine.getPageManager().pageExists( att.getParentName() ) ) 528 { 529 // the caller should catch the exception and use the exception text as an i18n key 530 throw new ProviderException( "attach.parent.not.exist" ); 531 } 532 533 m_provider.putAttachmentData( att, in ); 534 535 m_engine.getReferenceManager().updateReferences( att.getName(), new ArrayList< String >() ); 536 537 WikiPage parent = new WikiPage( m_engine, att.getParentName() ); 538 m_engine.updateReferences( parent ); 539 540 m_engine.getSearchManager().reindexPage( att ); 541 } 542 543 /** 544 * Returns a list of versions of the attachment. 545 * 546 * @param attachmentName A fully qualified name of the attachment. 547 * 548 * @return A list of Attachments. May return null, if attachments are 549 * disabled. 550 * @throws ProviderException If the provider fails for some reason. 551 */ 552 public List<Attachment> getVersionHistory( String attachmentName ) 553 throws ProviderException 554 { 555 if( m_provider == null ) 556 { 557 return null; 558 } 559 560 Attachment att = getAttachmentInfo( (WikiContext)null, attachmentName ); 561 562 if( att != null ) 563 { 564 return m_provider.getVersionHistory( att ); 565 } 566 567 return null; 568 } 569 570 /** 571 * Returns a collection of Attachments, containing each and every attachment 572 * that is in this Wiki. 573 * 574 * @return A collection of attachments. If attachments are disabled, will 575 * return an empty collection. 576 * @throws ProviderException If something went wrong with the backend 577 */ 578 public Collection<Attachment> getAllAttachments() throws ProviderException { 579 if( attachmentsEnabled() ) { 580 return m_provider.listAllChanged( new Date(0L) ); 581 } 582 583 return new ArrayList<>(); 584 } 585 586 /** 587 * Returns the current attachment provider. 588 * 589 * @return The current provider. May be null, if attachments are disabled. 590 */ 591 public WikiAttachmentProvider getCurrentProvider() 592 { 593 return m_provider; 594 } 595 596 /** 597 * Deletes the given attachment version. 598 * 599 * @param att The attachment to delete 600 * @throws ProviderException If something goes wrong with the backend. 601 */ 602 public void deleteVersion( Attachment att ) 603 throws ProviderException 604 { 605 if( m_provider == null ) return; 606 607 m_provider.deleteVersion( att ); 608 } 609 610 /** 611 * Deletes all versions of the given attachment. 612 * @param att The Attachment to delete. 613 * @throws ProviderException if something goes wrong with the backend. 614 */ 615 // FIXME: Should also use events! 616 public void deleteAttachment( Attachment att ) 617 throws ProviderException 618 { 619 if( m_provider == null ) return; 620 621 m_provider.deleteAttachment( att ); 622 623 m_engine.getSearchManager().pageRemoved( att ); 624 625 m_engine.getReferenceManager().clearPageEntries( att.getName() ); 626 627 } 628 629 /** 630 * Validates the filename and makes sure it is legal. It trims and splits 631 * and replaces bad characters. 632 * 633 * @param filename 634 * @return A validated name with annoying characters replaced. 635 * @throws WikiException If the filename is not legal (e.g. empty) 636 */ 637 static String validateFileName( String filename ) 638 throws WikiException 639 { 640 if( filename == null || filename.trim().length() == 0 ) 641 { 642 log.error("Empty file name given."); 643 644 // the caller should catch the exception and use the exception text as an i18n key 645 throw new WikiException( "attach.empty.file" ); 646 } 647 648 // 649 // Should help with IE 5.22 on OSX 650 // 651 filename = filename.trim(); 652 653 // If file name ends with .jsp or .jspf, the user is being naughty! 654 if( filename.toLowerCase().endsWith( ".jsp" ) || filename.toLowerCase().endsWith(".jspf") ) 655 { 656 log.info( "Attempt to upload a file with a .jsp/.jspf extension. In certain cases this " + 657 "can trigger unwanted security side effects, so we're preventing it." ); 658 // 659 // the caller should catch the exception and use the exception text as an i18n key 660 throw new WikiException( "attach.unwanted.file" ); 661 } 662 663 // 664 // Some browser send the full path info with the filename, so we need 665 // to remove it here by simply splitting along slashes and then taking the path. 666 // 667 668 String[] splitpath = filename.split( "[/\\\\]" ); 669 filename = splitpath[splitpath.length-1]; 670 671 // 672 // Remove any characters that might be a problem. Most 673 // importantly - characters that might stop processing 674 // of the URL. 675 // 676 filename = StringUtils.replaceChars( filename, "#?\"'", "____" ); 677 678 return filename; 679 } 680}