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 org.apache.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.api.core.Attachment; 024import org.apache.wiki.api.core.Engine; 025import org.apache.wiki.api.core.Page; 026import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 027import org.apache.wiki.api.exceptions.ProviderException; 028import org.apache.wiki.api.providers.AttachmentProvider; 029import org.apache.wiki.api.providers.WikiProvider; 030import org.apache.wiki.api.search.QueryItem; 031import org.apache.wiki.api.spi.Wiki; 032import org.apache.wiki.pages.PageTimeComparator; 033import org.apache.wiki.util.FileUtil; 034import org.apache.wiki.util.TextUtil; 035 036import java.io.File; 037import java.io.FileNotFoundException; 038import java.io.FilenameFilter; 039import java.io.IOException; 040import java.io.InputStream; 041import java.io.OutputStream; 042import java.nio.file.Files; 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.Date; 046import java.util.List; 047import java.util.Properties; 048import java.util.regex.Matcher; 049import java.util.regex.Pattern; 050 051/** 052 * Provides basic, versioning attachments. 053 * 054 * <PRE> 055 * Structure is as follows: 056 * attachment_dir/ 057 * ThisPage/ 058 * attachment.doc/ 059 * attachment.properties 060 * 1.doc 061 * 2.doc 062 * 3.doc 063 * picture.png/ 064 * attachment.properties 065 * 1.png 066 * 2.png 067 * ThatPage/ 068 * picture.png/ 069 * attachment.properties 070 * 1.png 071 * 072 * </PRE> 073 * 074 * The names of the directories will be URLencoded. 075 * <p> 076 * "attachment.properties" consists of the following items: 077 * <UL> 078 * <LI>1.author = author name for version 1 (etc) 079 * </UL> 080 */ 081public class BasicAttachmentProvider implements AttachmentProvider { 082 083 private Engine m_engine; 084 private String m_storageDir; 085 086 /* 087 * Disable client cache for files with patterns 088 * since 2.5.96 089 */ 090 private Pattern m_disableCache; 091 092 /** The property name for specifying which attachments are not cached. Value is <tt>{@value}</tt>. */ 093 public static final String PROP_DISABLECACHE = "jspwiki.basicAttachmentProvider.disableCache"; 094 095 /** The name of the property file. */ 096 public static final String PROPERTY_FILE = "attachment.properties"; 097 098 /** The default extension for the page attachment directory name. */ 099 public static final String DIR_EXTENSION = "-att"; 100 101 /** The default extension for the attachment directory. */ 102 public static final String ATTDIR_EXTENSION = "-dir"; 103 104 private static final Logger LOG = LogManager.getLogger( BasicAttachmentProvider.class ); 105 106 /** 107 * {@inheritDoc} 108 */ 109 @Override 110 public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException { 111 m_engine = engine; 112 m_storageDir = TextUtil.getCanonicalFilePathProperty( properties, PROP_STORAGEDIR, 113 System.getProperty("user.home") + File.separator + "jspwiki-files"); 114 115 final String patternString = engine.getWikiProperties().getProperty( PROP_DISABLECACHE ); 116 if ( patternString != null ) { 117 m_disableCache = Pattern.compile(patternString); 118 } 119 120 // Check if the directory exists - if it doesn't, create it. 121 final File f = new File( m_storageDir ); 122 if( !f.exists() ) { 123 f.mkdirs(); 124 } 125 126 // Some sanity checks 127 if( !f.exists() ) { 128 throw new IOException( "Could not find or create attachment storage directory '" + m_storageDir + "'" ); 129 } 130 131 if( !f.canWrite() ) { 132 throw new IOException( "Cannot write to the attachment storage directory '" + m_storageDir + "'" ); 133 } 134 135 if( !f.isDirectory() ) { 136 throw new IOException( "Your attachment storage points to a file, not a directory: '" + m_storageDir + "'" ); 137 } 138 } 139 140 /** 141 * Finds storage dir, and if it exists, makes sure that it is valid. 142 * 143 * @param wikipage Page to which this attachment is attached. 144 */ 145 private File findPageDir( String wikipage ) throws ProviderException { 146 wikipage = mangleName( wikipage ); 147 148 final File f = new File( m_storageDir, wikipage + DIR_EXTENSION ); 149 if( f.exists() && !f.isDirectory() ) { 150 throw new ProviderException( "Storage dir '" + f.getAbsolutePath() + "' is not a directory!" ); 151 } 152 153 return f; 154 } 155 156 private static String mangleName( final String wikiname ) { 157 return TextUtil.urlEncodeUTF8( wikiname ); 158 } 159 160 private static String unmangleName( final String filename ) 161 { 162 return TextUtil.urlDecodeUTF8( filename ); 163 } 164 165 /** 166 * Finds the dir in which the attachment lives. 167 */ 168 private File findAttachmentDir( final Attachment att ) throws ProviderException { 169 File f = new File( findPageDir( att.getParentName() ), mangleName( att.getFileName() + ATTDIR_EXTENSION ) ); 170 171 // Migration code for earlier versions of JSPWiki. Originally, we used plain filename. Then we realized we need 172 // to urlencode it. Then we realized that we have to use a postfix to make sure illegal file names are never formed. 173 if( !f.exists() ) { 174 File oldf = new File( findPageDir( att.getParentName() ), mangleName( att.getFileName() ) ); 175 if( oldf.exists() ) { 176 f = oldf; 177 } else { 178 oldf = new File( findPageDir( att.getParentName() ), att.getFileName() ); 179 if( oldf.exists() ) { 180 f = oldf; 181 } 182 } 183 } 184 185 return f; 186 } 187 188 /** 189 * Goes through the repository and decides which version is the newest one in that directory. 190 * 191 * @return Latest version number in the repository, or 0, if there is no page in the repository. 192 */ 193 private int findLatestVersion( final Attachment att ) throws ProviderException { 194 final File attDir = findAttachmentDir( att ); 195 final String[] pages = attDir.list( new AttachmentVersionFilter() ); 196 if( pages == null ) { 197 return 0; // No such thing found. 198 } 199 200 int version = 0; 201 for( final String page : pages ) { 202 final int cutpoint = page.indexOf( '.' ); 203 final String pageNum = ( cutpoint > 0 ) ? page.substring( 0, cutpoint ) : page; 204 205 try { 206 final int res = Integer.parseInt( pageNum ); 207 208 if( res > version ) { 209 version = res; 210 } 211 } catch( final NumberFormatException e ) { 212 } // It's okay to skip these. 213 } 214 215 return version; 216 } 217 218 /** 219 * Returns the file extension. For example "test.png" returns "png". 220 * <p> 221 * If file has no extension, will return "bin" 222 * 223 * @param filename The file name to check 224 * @return The extension. If no extension is found, returns "bin". 225 */ 226 protected static String getFileExtension( final String filename ) { 227 String fileExt = "bin"; 228 229 final int dot = filename.lastIndexOf('.'); 230 if( dot >= 0 && dot < filename.length()-1 ) { 231 fileExt = mangleName( filename.substring( dot+1 ) ); 232 } 233 234 return fileExt; 235 } 236 237 /** 238 * Writes the page properties back to the file system. 239 * Note that it WILL overwrite any previous properties. 240 */ 241 private void putPageProperties( final Attachment att, final Properties properties ) throws IOException, ProviderException { 242 final File attDir = findAttachmentDir( att ); 243 final File propertyFile = new File( attDir, PROPERTY_FILE ); 244 try( final OutputStream out = Files.newOutputStream( propertyFile.toPath() ) ) { 245 properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" ); 246 } 247 } 248 249 /** 250 * Reads page properties from the file system. 251 */ 252 private Properties getPageProperties( final Attachment att ) throws IOException, ProviderException { 253 final Properties props = new Properties(); 254 final File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE ); 255 if( propertyFile.exists() ) { 256 try( final InputStream in = Files.newInputStream( propertyFile.toPath() ) ) { 257 props.load( in ); 258 } catch( final IOException ioe ) { 259 LOG.error( ioe.getMessage() ); 260 } 261 } 262 263 return props; 264 } 265 266 /** 267 * {@inheritDoc} 268 */ 269 @Override 270 public void putAttachmentData( final Attachment att, final InputStream data ) throws ProviderException, IOException { 271 final File attDir = findAttachmentDir( att ); 272 273 if( !attDir.exists() ) { 274 attDir.mkdirs(); 275 } 276 final int latestVersion = findLatestVersion( att ); 277 final int versionNumber = latestVersion + 1; 278 279 final File newfile = new File( attDir, versionNumber + "." + getFileExtension( att.getFileName() ) ); 280 try( final OutputStream out = Files.newOutputStream( newfile.toPath() ) ) { 281 LOG.info( "Uploading attachment " + att.getFileName() + " to page " + att.getParentName() ); 282 LOG.info( "Saving attachment contents to " + newfile.getAbsolutePath() ); 283 FileUtil.copyContents( data, out ); 284 285 final Properties props = getPageProperties( att ); 286 287 String author = att.getAuthor(); 288 if( author == null ) { 289 author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext 290 } 291 props.setProperty( versionNumber + ".author", author ); 292 293 final String changeNote = att.getAttribute( Page.CHANGENOTE ); 294 if( changeNote != null ) { 295 props.setProperty( versionNumber + ".changenote", changeNote ); 296 } 297 298 putPageProperties( att, props ); 299 } catch( final IOException e ) { 300 LOG.error( "Could not save attachment data: ", e ); 301 throw (IOException) e.fillInStackTrace(); 302 } 303 } 304 305 /** 306 * {@inheritDoc} 307 */ 308 @Override 309 public String getProviderInfo() { 310 return ""; 311 } 312 313 private File findFile( final File dir, final Attachment att ) throws FileNotFoundException, ProviderException { 314 int version = att.getVersion(); 315 if( version == WikiProvider.LATEST_VERSION ) { 316 version = findLatestVersion( att ); 317 } 318 319 final String ext = getFileExtension( att.getFileName() ); 320 File f = new File( dir, version + "." + ext ); 321 322 if( !f.exists() ) { 323 if( "bin".equals( ext ) ) { 324 final File fOld = new File( dir, version + "." ); 325 if( fOld.exists() ) { 326 f = fOld; 327 } 328 } 329 if( !f.exists() ) { 330 throw new FileNotFoundException( "No such file: " + f.getAbsolutePath() + " exists." ); 331 } 332 } 333 334 return f; 335 } 336 337 /** 338 * {@inheritDoc} 339 */ 340 @Override 341 public InputStream getAttachmentData( final Attachment att ) throws IOException, ProviderException { 342 final File attDir = findAttachmentDir( att ); 343 try { 344 final File f = findFile( attDir, att ); 345 return Files.newInputStream( f.toPath() ); 346 } catch( final FileNotFoundException e ) { 347 LOG.error( "File not found: " + e.getMessage() ); 348 throw new ProviderException( "No such page was found." ); 349 } 350 } 351 352 /** 353 * {@inheritDoc} 354 */ 355 @Override 356 public List< Attachment > listAttachments( final Page page ) throws ProviderException { 357 final List< Attachment > result = new ArrayList<>(); 358 final File dir = findPageDir( page.getName() ); 359 final String[] attachments = dir.list(); 360 if( attachments != null ) { 361 // We now have a list of all potential attachments in the directory. 362 for( final String attachment : attachments ) { 363 final File f = new File( dir, attachment ); 364 if( f.isDirectory() ) { 365 String attachmentName = unmangleName( attachment ); 366 367 // Is it a new-stylea attachment directory? If yes, we'll just deduce the name. If not, however, 368 // we'll check if there's a suitable property file in the directory. 369 if( attachmentName.endsWith( ATTDIR_EXTENSION ) ) { 370 attachmentName = attachmentName.substring( 0, attachmentName.length() - ATTDIR_EXTENSION.length() ); 371 } else { 372 final File propFile = new File( f, PROPERTY_FILE ); 373 if( !propFile.exists() ) { 374 // This is not obviously a JSPWiki attachment, so let's just skip it. 375 continue; 376 } 377 } 378 379 final Attachment att = getAttachmentInfo( page, attachmentName, WikiProvider.LATEST_VERSION ); 380 // Sanity check - shouldn't really be happening, unless you mess with the repository directly. 381 if( att == null ) { 382 LOG.error( "Attachment disappeared while reading information:" 383 + " if you did not touch the repository, there is a serious bug somewhere or perhaps it" 384 + " was deleted by antivirus software, etc. " + "Attachment = " + attachment 385 + ", decoded = " + attachmentName ); 386 } else { 387 result.add( att ); 388 } 389 } 390 } 391 } 392 393 return result; 394 } 395 396 /** 397 * {@inheritDoc} 398 */ 399 @Override 400 public Collection< Attachment > findAttachments( final QueryItem[] query ) { 401 return new ArrayList<>(); 402 } 403 404 /** 405 * {@inheritDoc} 406 */ 407 // FIXME: Very unoptimized. 408 @Override 409 public List< Attachment > listAllChanged( final Date timestamp ) throws ProviderException { 410 final File attDir = new File( m_storageDir ); 411 if( !attDir.exists() ) { 412 if (!attDir.mkdirs()) { 413 throw new ProviderException( "Specified attachment directory " + m_storageDir + " does not exist!" ); 414 } 415 } 416 417 final ArrayList< Attachment > list = new ArrayList<>(); 418 final String[] pagesWithAttachments = attDir.list( new AttachmentFilter() ); 419 420 if( pagesWithAttachments != null ) { 421 for( final String pagesWithAttachment : pagesWithAttachments ) { 422 String pageId = unmangleName( pagesWithAttachment ); 423 pageId = pageId.substring( 0, pageId.length() - DIR_EXTENSION.length() ); 424 425 final Collection< Attachment > c = listAttachments( Wiki.contents().page( m_engine, pageId ) ); 426 for( final Attachment att : c ) { 427 if( att.getLastModified().after( timestamp ) ) { 428 list.add( att ); 429 } 430 } 431 } 432 } 433 434 list.sort( new PageTimeComparator() ); 435 436 return list; 437 } 438 439 /** 440 * {@inheritDoc} 441 */ 442 @Override 443 public Attachment getAttachmentInfo( final Page page, final String name, int version ) throws ProviderException { 444 final Attachment att = new org.apache.wiki.attachment.Attachment( m_engine, page.getName(), name ); 445 final File dir = findAttachmentDir( att ); 446 if( !dir.exists() ) { 447 // LOG.debug("Attachment dir not found - thus no attachment can exist."); 448 return null; 449 } 450 451 if( version == WikiProvider.LATEST_VERSION ) { 452 version = findLatestVersion(att); 453 } 454 455 att.setVersion( version ); 456 457 // Should attachment be cachable by the client (browser)? 458 if( m_disableCache != null ) { 459 final Matcher matcher = m_disableCache.matcher( name ); 460 if( matcher.matches() ) { 461 att.setCacheable( false ); 462 } 463 } 464 465 // System.out.println("Fetching info on version "+version); 466 try { 467 final Properties props = getPageProperties( att ); 468 att.setAuthor( props.getProperty( version+".author" ) ); 469 final String changeNote = props.getProperty( version+".changenote" ); 470 if( changeNote != null ) { 471 att.setAttribute( Page.CHANGENOTE, changeNote ); 472 } 473 474 final File f = findFile( dir, att ); 475 att.setSize( f.length() ); 476 att.setLastModified( new Date( f.lastModified() ) ); 477 } catch( final FileNotFoundException e ) { 478 LOG.error( "Can't get attachment properties for " + att, e ); 479 return null; 480 } catch( final IOException e ) { 481 LOG.error("Can't read page properties", e ); 482 throw new ProviderException("Cannot read page properties: "+e.getMessage()); 483 } 484 // FIXME: Check for existence of this particular version. 485 486 return att; 487 } 488 489 /** 490 * {@inheritDoc} 491 */ 492 @Override 493 public List< Attachment > getVersionHistory( final Attachment att ) { 494 final ArrayList< Attachment > list = new ArrayList<>(); 495 try { 496 final int latest = findLatestVersion( att ); 497 for( int i = latest; i >= 1; i-- ) { 498 final Attachment a = getAttachmentInfo( Wiki.contents().page( m_engine, att.getParentName() ), att.getFileName(), i ); 499 if( a != null ) { 500 list.add( a ); 501 } 502 } 503 } catch( final ProviderException e ) { 504 LOG.error( "Getting version history failed for page: " + att, e ); 505 // FIXME: Should this fail? 506 } 507 508 return list; 509 } 510 511 /** 512 * {@inheritDoc} 513 */ 514 @Override 515 public void deleteVersion( final Attachment att ) throws ProviderException { 516 // FIXME: Does nothing yet. 517 } 518 519 /** 520 * {@inheritDoc} 521 */ 522 @Override 523 public void deleteAttachment( final Attachment att ) throws ProviderException { 524 final File dir = findAttachmentDir( att ); 525 final String[] files = dir.list(); 526 for( final String s : files ) { 527 final File file = new File( dir.getAbsolutePath() + "/" + s ); 528 file.delete(); 529 } 530 dir.delete(); 531 } 532 533 /** 534 * Returns only those directories that contain attachments. 535 */ 536 public static class AttachmentFilter implements FilenameFilter { 537 /** 538 * {@inheritDoc} 539 */ 540 @Override 541 public boolean accept( final File dir, final String name ) 542 { 543 return name.endsWith( DIR_EXTENSION ); 544 } 545 } 546 547 /** 548 * Accepts only files that are actual versions, no control files. 549 */ 550 public static class AttachmentVersionFilter implements FilenameFilter { 551 /** 552 * {@inheritDoc} 553 */ 554 @Override 555 public boolean accept( final File dir, final String name ) 556 { 557 return !name.equals( PROPERTY_FILE ); 558 } 559 } 560 561 /** 562 * {@inheritDoc} 563 */ 564 @Override 565 public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException { 566 final File srcDir = findPageDir( oldParent ); 567 final File destDir = findPageDir( newParent ); 568 569 LOG.debug( "Trying to move all attachments from " + srcDir + " to " + destDir ); 570 571 // If it exists, we're overwriting an old page (this has already been confirmed at a higher level), so delete any existing attachments. 572 if( destDir.exists() ) { 573 LOG.error( "Page rename failed because target directory " + destDir + " exists" ); 574 } else { 575 // destDir.getParentFile().mkdir(); 576 srcDir.renameTo( destDir ); 577 } 578 } 579 580} 581