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 LOG.debug(e.getMessage(), e); 213 } // It's okay to skip these. 214 } 215 216 return version; 217 } 218 219 /** 220 * Returns the file extension. For example "test.png" returns "png". 221 * <p> 222 * If file has no extension, will return "bin" 223 * 224 * @param filename The file name to check 225 * @return The extension. If no extension is found, returns "bin". 226 */ 227 protected static String getFileExtension( final String filename ) { 228 String fileExt = "bin"; 229 230 final int dot = filename.lastIndexOf('.'); 231 if( dot >= 0 && dot < filename.length()-1 ) { 232 fileExt = mangleName( filename.substring( dot+1 ) ); 233 } 234 235 return fileExt; 236 } 237 238 /** 239 * Writes the page properties back to the file system. 240 * Note that it WILL overwrite any previous properties. 241 */ 242 private void putPageProperties( final Attachment att, final Properties properties ) throws IOException, ProviderException { 243 final File attDir = findAttachmentDir( att ); 244 final File propertyFile = new File( attDir, PROPERTY_FILE ); 245 try( final OutputStream out = Files.newOutputStream( propertyFile.toPath() ) ) { 246 properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" ); 247 } 248 } 249 250 /** 251 * Reads page properties from the file system. 252 */ 253 private Properties getPageProperties( final Attachment att ) throws IOException, ProviderException { 254 final Properties props = new Properties(); 255 final File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE ); 256 if( propertyFile.exists() ) { 257 try( final InputStream in = Files.newInputStream( propertyFile.toPath() ) ) { 258 props.load( in ); 259 } catch( final IOException ioe ) { 260 LOG.error( ioe.getMessage() ); 261 } 262 } 263 264 return props; 265 } 266 267 /** 268 * {@inheritDoc} 269 */ 270 @Override 271 public void putAttachmentData( final Attachment att, final InputStream data ) throws ProviderException, IOException { 272 final File attDir = findAttachmentDir( att ); 273 274 if( !attDir.exists() ) { 275 attDir.mkdirs(); 276 } 277 final int latestVersion = findLatestVersion( att ); 278 final int versionNumber = latestVersion + 1; 279 280 final File newfile = new File( attDir, versionNumber + "." + getFileExtension( att.getFileName() ) ); 281 try( final OutputStream out = Files.newOutputStream( newfile.toPath() ) ) { 282 LOG.info( "Uploading attachment " + att.getFileName() + " to page " + att.getParentName() ); 283 LOG.info( "Saving attachment contents to " + newfile.getAbsolutePath() ); 284 FileUtil.copyContents( data, out ); 285 286 final Properties props = getPageProperties( att ); 287 288 String author = att.getAuthor(); 289 if( author == null ) { 290 author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext 291 } 292 props.setProperty( versionNumber + ".author", author ); 293 294 final String changeNote = att.getAttribute( Page.CHANGENOTE ); 295 if( changeNote != null ) { 296 props.setProperty( versionNumber + ".changenote", changeNote ); 297 } 298 299 putPageProperties( att, props ); 300 } catch( final IOException e ) { 301 LOG.error( "Could not save attachment data: ", e ); 302 throw (IOException) e.fillInStackTrace(); 303 } 304 } 305 306 /** 307 * {@inheritDoc} 308 */ 309 @Override 310 public String getProviderInfo() { 311 return ""; 312 } 313 314 private File findFile( final File dir, final Attachment att ) throws FileNotFoundException, ProviderException { 315 int version = att.getVersion(); 316 if( version == WikiProvider.LATEST_VERSION ) { 317 version = findLatestVersion( att ); 318 } 319 320 final String ext = getFileExtension( att.getFileName() ); 321 File f = new File( dir, version + "." + ext ); 322 323 if( !f.exists() ) { 324 if( "bin".equals( ext ) ) { 325 final File fOld = new File( dir, version + "." ); 326 if( fOld.exists() ) { 327 f = fOld; 328 } 329 } 330 if( !f.exists() ) { 331 throw new FileNotFoundException( "No such file: " + f.getAbsolutePath() + " exists." ); 332 } 333 } 334 335 return f; 336 } 337 338 /** 339 * {@inheritDoc} 340 */ 341 @Override 342 public InputStream getAttachmentData( final Attachment att ) throws IOException, ProviderException { 343 final File attDir = findAttachmentDir( att ); 344 try { 345 final File f = findFile( attDir, att ); 346 return Files.newInputStream( f.toPath() ); 347 } catch( final FileNotFoundException e ) { 348 LOG.error( "File not found: " + e.getMessage() ); 349 throw new ProviderException( "No such page was found." ); 350 } 351 } 352 353 /** 354 * {@inheritDoc} 355 */ 356 @Override 357 public List< Attachment > listAttachments( final Page page ) throws ProviderException { 358 final List< Attachment > result = new ArrayList<>(); 359 final File dir = findPageDir( page.getName() ); 360 final String[] attachments = dir.list(); 361 if( attachments != null ) { 362 // We now have a list of all potential attachments in the directory. 363 for( final String attachment : attachments ) { 364 final File f = new File( dir, attachment ); 365 if( f.isDirectory() ) { 366 String attachmentName = unmangleName( attachment ); 367 368 // Is it a new-stylea attachment directory? If yes, we'll just deduce the name. If not, however, 369 // we'll check if there's a suitable property file in the directory. 370 if( attachmentName.endsWith( ATTDIR_EXTENSION ) ) { 371 attachmentName = attachmentName.substring( 0, attachmentName.length() - ATTDIR_EXTENSION.length() ); 372 } else { 373 final File propFile = new File( f, PROPERTY_FILE ); 374 if( !propFile.exists() ) { 375 // This is not obviously a JSPWiki attachment, so let's just skip it. 376 continue; 377 } 378 } 379 380 final Attachment att = getAttachmentInfo( page, attachmentName, WikiProvider.LATEST_VERSION ); 381 // Sanity check - shouldn't really be happening, unless you mess with the repository directly. 382 if( att == null ) { 383 LOG.error( "Attachment disappeared while reading information:" 384 + " if you did not touch the repository, there is a serious bug somewhere or perhaps it" 385 + " was deleted by antivirus software, etc. " + "Attachment = " + attachment 386 + ", decoded = " + attachmentName ); 387 } else { 388 result.add( att ); 389 } 390 } 391 } 392 } 393 394 return result; 395 } 396 397 /** 398 * {@inheritDoc} 399 */ 400 @Override 401 public Collection< Attachment > findAttachments( final QueryItem[] query ) { 402 return new ArrayList<>(); 403 } 404 405 /** 406 * {@inheritDoc} 407 */ 408 // FIXME: Very unoptimized. 409 @Override 410 public List< Attachment > listAllChanged( final Date timestamp ) throws ProviderException { 411 final File attDir = new File( m_storageDir ); 412 if( !attDir.exists() ) { 413 if (!attDir.mkdirs()) { 414 throw new ProviderException( "Specified attachment directory " + m_storageDir + " does not exist!" ); 415 } 416 } 417 418 final ArrayList< Attachment > list = new ArrayList<>(); 419 final String[] pagesWithAttachments = attDir.list( new AttachmentFilter() ); 420 421 if( pagesWithAttachments != null ) { 422 for( final String pagesWithAttachment : pagesWithAttachments ) { 423 String pageId = unmangleName( pagesWithAttachment ); 424 pageId = pageId.substring( 0, pageId.length() - DIR_EXTENSION.length() ); 425 426 final Collection< Attachment > c = listAttachments( Wiki.contents().page( m_engine, pageId ) ); 427 for( final Attachment att : c ) { 428 if( att.getLastModified().after( timestamp ) ) { 429 list.add( att ); 430 } 431 } 432 } 433 } 434 435 list.sort( new PageTimeComparator() ); 436 437 return list; 438 } 439 440 /** 441 * {@inheritDoc} 442 */ 443 @Override 444 public Attachment getAttachmentInfo( final Page page, final String name, int version ) throws ProviderException { 445 final Attachment att = new org.apache.wiki.attachment.Attachment( m_engine, page.getName(), name ); 446 final File dir = findAttachmentDir( att ); 447 if( !dir.exists() ) { 448 // LOG.debug("Attachment dir not found - thus no attachment can exist."); 449 return null; 450 } 451 452 if( version == WikiProvider.LATEST_VERSION ) { 453 version = findLatestVersion(att); 454 } 455 456 att.setVersion( version ); 457 458 // Should attachment be cachable by the client (browser)? 459 if( m_disableCache != null ) { 460 final Matcher matcher = m_disableCache.matcher( name ); 461 if( matcher.matches() ) { 462 att.setCacheable( false ); 463 } 464 } 465 466 // System.out.println("Fetching info on version "+version); 467 try { 468 final Properties props = getPageProperties( att ); 469 att.setAuthor( props.getProperty( version+".author" ) ); 470 final String changeNote = props.getProperty( version+".changenote" ); 471 if( changeNote != null ) { 472 att.setAttribute( Page.CHANGENOTE, changeNote ); 473 } 474 475 final File f = findFile( dir, att ); 476 att.setSize( f.length() ); 477 att.setLastModified( new Date( f.lastModified() ) ); 478 } catch( final FileNotFoundException e ) { 479 LOG.error( "Can't get attachment properties for " + att, e ); 480 return null; 481 } catch( final IOException e ) { 482 LOG.error("Can't read page properties", e ); 483 throw new ProviderException("Cannot read page properties: "+e.getMessage()); 484 } 485 // FIXME: Check for existence of this particular version. 486 487 return att; 488 } 489 490 /** 491 * {@inheritDoc} 492 */ 493 @Override 494 public List< Attachment > getVersionHistory( final Attachment att ) { 495 final ArrayList< Attachment > list = new ArrayList<>(); 496 try { 497 final int latest = findLatestVersion( att ); 498 for( int i = latest; i >= 1; i-- ) { 499 final Attachment a = getAttachmentInfo( Wiki.contents().page( m_engine, att.getParentName() ), att.getFileName(), i ); 500 if( a != null ) { 501 list.add( a ); 502 } 503 } 504 } catch( final ProviderException e ) { 505 LOG.error( "Getting version history failed for page: " + att, e ); 506 // FIXME: Should this fail? 507 } 508 509 return list; 510 } 511 512 /** 513 * {@inheritDoc} 514 */ 515 @Override 516 public void deleteVersion( final Attachment att ) throws ProviderException { 517 // FIXME: Does nothing yet. 518 } 519 520 /** 521 * {@inheritDoc} 522 */ 523 @Override 524 public void deleteAttachment( final Attachment att ) throws ProviderException { 525 final File dir = findAttachmentDir( att ); 526 final String[] files = dir.list(); 527 for( final String s : files ) { 528 final File file = new File( dir.getAbsolutePath() + "/" + s ); 529 file.delete(); 530 } 531 dir.delete(); 532 } 533 534 /** 535 * Returns only those directories that contain attachments. 536 */ 537 public static class AttachmentFilter implements FilenameFilter { 538 /** 539 * {@inheritDoc} 540 */ 541 @Override 542 public boolean accept( final File dir, final String name ) 543 { 544 return name.endsWith( DIR_EXTENSION ); 545 } 546 } 547 548 /** 549 * Accepts only files that are actual versions, no control files. 550 */ 551 public static class AttachmentVersionFilter implements FilenameFilter { 552 /** 553 * {@inheritDoc} 554 */ 555 @Override 556 public boolean accept( final File dir, final String name ) 557 { 558 return !name.equals( PROPERTY_FILE ); 559 } 560 } 561 562 /** 563 * {@inheritDoc} 564 */ 565 @Override 566 public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException { 567 final File srcDir = findPageDir( oldParent ); 568 final File destDir = findPageDir( newParent ); 569 570 LOG.debug( "Trying to move all attachments from " + srcDir + " to " + destDir ); 571 572 // If it exists, we're overwriting an old page (this has already been confirmed at a higher level), so delete any existing attachments. 573 if( destDir.exists() ) { 574 LOG.error( "Page rename failed because target directory " + destDir + " exists" ); 575 } else { 576 // destDir.getParentFile().mkdir(); 577 srcDir.renameTo( destDir ); 578 } 579 } 580 581} 582