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