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