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 throw new ProviderException( "Attachment disappeared while reading information:" 383 + " if you did not touch the repository, there is a serious bug somewhere. " + "Attachment = " + attachment 384 + ", decoded = " + attachmentName ); 385 } 386 387 result.add( att ); 388 } 389 } 390 } 391 392 return result; 393 } 394 395 /** 396 * {@inheritDoc} 397 */ 398 @Override 399 public Collection< Attachment > findAttachments( final QueryItem[] query ) { 400 return new ArrayList<>(); 401 } 402 403 /** 404 * {@inheritDoc} 405 */ 406 // FIXME: Very unoptimized. 407 @Override 408 public List< Attachment > listAllChanged( final Date timestamp ) throws ProviderException { 409 final File attDir = new File( m_storageDir ); 410 if( !attDir.exists() ) { 411 throw new ProviderException( "Specified attachment directory " + m_storageDir + " does not exist!" ); 412 } 413 414 final ArrayList< Attachment > list = new ArrayList<>(); 415 final String[] pagesWithAttachments = attDir.list( new AttachmentFilter() ); 416 417 if( pagesWithAttachments != null ) { 418 for( final String pagesWithAttachment : pagesWithAttachments ) { 419 String pageId = unmangleName( pagesWithAttachment ); 420 pageId = pageId.substring( 0, pageId.length() - DIR_EXTENSION.length() ); 421 422 final Collection< Attachment > c = listAttachments( Wiki.contents().page( m_engine, pageId ) ); 423 for( final Attachment att : c ) { 424 if( att.getLastModified().after( timestamp ) ) { 425 list.add( att ); 426 } 427 } 428 } 429 } 430 431 list.sort( new PageTimeComparator() ); 432 433 return list; 434 } 435 436 /** 437 * {@inheritDoc} 438 */ 439 @Override 440 public Attachment getAttachmentInfo( final Page page, final String name, int version ) throws ProviderException { 441 final Attachment att = new org.apache.wiki.attachment.Attachment( m_engine, page.getName(), name ); 442 final File dir = findAttachmentDir( att ); 443 if( !dir.exists() ) { 444 // LOG.debug("Attachment dir not found - thus no attachment can exist."); 445 return null; 446 } 447 448 if( version == WikiProvider.LATEST_VERSION ) { 449 version = findLatestVersion(att); 450 } 451 452 att.setVersion( version ); 453 454 // Should attachment be cachable by the client (browser)? 455 if( m_disableCache != null ) { 456 final Matcher matcher = m_disableCache.matcher( name ); 457 if( matcher.matches() ) { 458 att.setCacheable( false ); 459 } 460 } 461 462 // System.out.println("Fetching info on version "+version); 463 try { 464 final Properties props = getPageProperties( att ); 465 att.setAuthor( props.getProperty( version+".author" ) ); 466 final String changeNote = props.getProperty( version+".changenote" ); 467 if( changeNote != null ) { 468 att.setAttribute( Page.CHANGENOTE, changeNote ); 469 } 470 471 final File f = findFile( dir, att ); 472 att.setSize( f.length() ); 473 att.setLastModified( new Date( f.lastModified() ) ); 474 } catch( final FileNotFoundException e ) { 475 LOG.error( "Can't get attachment properties for " + att, e ); 476 return null; 477 } catch( final IOException e ) { 478 LOG.error("Can't read page properties", e ); 479 throw new ProviderException("Cannot read page properties: "+e.getMessage()); 480 } 481 // FIXME: Check for existence of this particular version. 482 483 return att; 484 } 485 486 /** 487 * {@inheritDoc} 488 */ 489 @Override 490 public List< Attachment > getVersionHistory( final Attachment att ) { 491 final ArrayList< Attachment > list = new ArrayList<>(); 492 try { 493 final int latest = findLatestVersion( att ); 494 for( int i = latest; i >= 1; i-- ) { 495 final Attachment a = getAttachmentInfo( Wiki.contents().page( m_engine, att.getParentName() ), att.getFileName(), i ); 496 if( a != null ) { 497 list.add( a ); 498 } 499 } 500 } catch( final ProviderException e ) { 501 LOG.error( "Getting version history failed for page: " + att, e ); 502 // FIXME: Should this fail? 503 } 504 505 return list; 506 } 507 508 /** 509 * {@inheritDoc} 510 */ 511 @Override 512 public void deleteVersion( final Attachment att ) throws ProviderException { 513 // FIXME: Does nothing yet. 514 } 515 516 /** 517 * {@inheritDoc} 518 */ 519 @Override 520 public void deleteAttachment( final Attachment att ) throws ProviderException { 521 final File dir = findAttachmentDir( att ); 522 final String[] files = dir.list(); 523 for( final String s : files ) { 524 final File file = new File( dir.getAbsolutePath() + "/" + s ); 525 file.delete(); 526 } 527 dir.delete(); 528 } 529 530 /** 531 * Returns only those directories that contain attachments. 532 */ 533 public static class AttachmentFilter implements FilenameFilter { 534 /** 535 * {@inheritDoc} 536 */ 537 @Override 538 public boolean accept( final File dir, final String name ) 539 { 540 return name.endsWith( DIR_EXTENSION ); 541 } 542 } 543 544 /** 545 * Accepts only files that are actual versions, no control files. 546 */ 547 public static class AttachmentVersionFilter implements FilenameFilter { 548 /** 549 * {@inheritDoc} 550 */ 551 @Override 552 public boolean accept( final File dir, final String name ) 553 { 554 return !name.equals( PROPERTY_FILE ); 555 } 556 } 557 558 /** 559 * {@inheritDoc} 560 */ 561 @Override 562 public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException { 563 final File srcDir = findPageDir( oldParent ); 564 final File destDir = findPageDir( newParent ); 565 566 LOG.debug( "Trying to move all attachments from " + srcDir + " to " + destDir ); 567 568 // If it exists, we're overwriting an old page (this has already been confirmed at a higher level), so delete any existing attachments. 569 if( destDir.exists() ) { 570 LOG.error( "Page rename failed because target directory " + destDir + " exists" ); 571 } else { 572 // destDir.getParentFile().mkdir(); 573 srcDir.renameTo( destDir ); 574 } 575 } 576 577} 578