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