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