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.content; 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.Context; 025import org.apache.wiki.api.core.Engine; 026import org.apache.wiki.api.core.Page; 027import org.apache.wiki.api.exceptions.ProviderException; 028import org.apache.wiki.api.exceptions.WikiException; 029import org.apache.wiki.attachment.AttachmentManager; 030import org.apache.wiki.event.WikiEventManager; 031import org.apache.wiki.event.WikiPageRenameEvent; 032import org.apache.wiki.pages.PageManager; 033import org.apache.wiki.parser.MarkupParser; 034import org.apache.wiki.references.ReferenceManager; 035import org.apache.wiki.search.SearchManager; 036import org.apache.wiki.util.TextUtil; 037 038import java.util.Collection; 039import java.util.List; 040import java.util.Set; 041import java.util.TreeSet; 042import java.util.regex.Matcher; 043import java.util.regex.Pattern; 044import org.apache.wiki.event.WikiPageRenameEvent; 045import org.apache.wiki.security.EventUtil; 046 047 048/** 049 * Provides page renaming functionality. Note that there used to be a similarly named class in 2.6, but due to unclear copyright, the 050 * class was completely rewritten from scratch for 2.8. 051 * 052 * @since 2.8 053 */ 054public class DefaultPageRenamer implements PageRenamer { 055 056 private static final Logger LOG = LogManager.getLogger( DefaultPageRenamer.class ); 057 058 private boolean m_camelCase; 059 060 /** 061 * Renames a page. 062 * 063 * @param context The current context. 064 * @param renameFrom The name from which to rename. 065 * @param renameTo The new name. 066 * @param changeReferrers If true, also changes all the referrers. 067 * @return The final new name (in case it had to be modified) 068 * @throws WikiException If the page cannot be renamed. 069 */ 070 @Override 071 public String renamePage( final Context context, final String renameFrom, final String renameTo, final boolean changeReferrers ) throws WikiException { 072 // Sanity checks first 073 if( renameFrom == null || renameFrom.isEmpty() ) { 074 throw new WikiException( "From name may not be null or empty" ); 075 } 076 if( renameTo == null || renameTo.isEmpty() ) { 077 throw new WikiException( "To name may not be null or empty" ); 078 } 079 080 // Clean up the "to" -name so that it does not contain anything illegal 081 final String renameToClean = MarkupParser.cleanLink( renameTo.trim() ); 082 if( renameToClean.equals( renameFrom ) ) { 083 throw new WikiException( "You cannot rename the page to itself" ); 084 } 085 086 // Preconditions: "from" page must exist, and "to" page must not yet exist. 087 final Engine engine = context.getEngine(); 088 final Page fromPage = engine.getManager( PageManager.class ).getPage( renameFrom ); 089 if( fromPage == null ) { 090 throw new WikiException("No such page "+renameFrom); 091 } 092 Page toPage = engine.getManager( PageManager.class ).getPage( renameToClean ); 093 if( toPage != null ) { 094 throw new WikiException( "Page already exists " + renameToClean ); 095 } 096 097 final Set< String > referrers = getReferencesToChange( fromPage, engine ); 098 099 // Do the actual rename by changing from the frompage to the topage, including all the attachments 100 // Remove references to attachments under old name 101 final List< Attachment > attachmentsOldName = engine.getManager( AttachmentManager.class ).listAttachments( fromPage ); 102 for( final Attachment att: attachmentsOldName ) { 103 final Page fromAttPage = engine.getManager( PageManager.class ).getPage( att.getName() ); 104 engine.getManager( ReferenceManager.class ).pageRemoved( fromAttPage ); 105 } 106 107 engine.getManager( PageManager.class ).getProvider().movePage( renameFrom, renameToClean ); 108 if( engine.getManager( AttachmentManager.class ).attachmentsEnabled() ) { 109 engine.getManager( AttachmentManager.class ).getCurrentProvider().moveAttachmentsForPage( renameFrom, renameToClean ); 110 } 111 112 // Add a comment to the page notifying what changed. This adds a new revision to the repo with no actual change. 113 toPage = engine.getManager( PageManager.class ).getPage( renameToClean ); 114 if( toPage == null ) { 115 throw new ProviderException( "Rename seems to have failed for some strange reason - please check logs!" ); 116 } 117 toPage.setAttribute( Page.CHANGENOTE, fromPage.getName() + " ==> " + toPage.getName() ); 118 toPage.setAuthor( context.getCurrentUser().getName() ); 119 engine.getManager( PageManager.class ).putPageText( toPage, engine.getManager( PageManager.class ).getPureText( toPage ) ); 120 121 // Update the references 122 engine.getManager( ReferenceManager.class ).pageRemoved( fromPage ); 123 engine.getManager( ReferenceManager.class ).updateReferences( toPage ); 124 125 // Update referrers 126 if( changeReferrers ) { 127 updateReferrers( context, fromPage, toPage, referrers ); 128 } 129 130 // re-index the page including its attachments 131 engine.getManager( SearchManager.class ).reindexPage( toPage ); 132 133 final Collection< Attachment > attachmentsNewName = engine.getManager( AttachmentManager.class ).listAttachments( toPage ); 134 for( final Attachment att:attachmentsNewName ) { 135 final Page toAttPage = engine.getManager( PageManager.class ).getPage( att.getName() ); 136 // add reference to attachment under new page name 137 engine.getManager( ReferenceManager.class ).updateReferences( toAttPage ); 138 engine.getManager( SearchManager.class ).reindexPage( att ); 139 } 140 141 firePageRenameEvent( renameFrom, renameToClean, context ); 142 143 // Done, return the new name. 144 return renameToClean; 145 } 146 147 /** 148 * Fires a WikiPageRenameEvent to all registered listeners. Currently not used internally by JSPWiki itself, but you can use it for 149 * something else. 150 * 151 * @param oldName the former page name 152 * @param newName the new page name 153 */ 154 @Override 155 public void firePageRenameEvent( final String oldName, final String newName, final Context context) { 156 if( WikiEventManager.isListening(this) ) { 157 WikiEventManager.fireEvent(this, EventUtil.applyFrom(new WikiPageRenameEvent(this, oldName, newName ), context) ); 158 } 159 } 160 161 /** 162 * This method finds all the pages which have anything to do with the fromPage and 163 * change any referrers it can figure out in that page. 164 * 165 * @param context WikiContext in which we operate 166 * @param fromPage The old page 167 * @param toPage The new page 168 */ 169 private void updateReferrers( final Context context, final Page fromPage, final Page toPage, final Set< String > referrers ) { 170 if( referrers.isEmpty() ) { // No referrers 171 return; 172 } 173 174 final Engine engine = context.getEngine(); 175 for( String pageName : referrers ) { 176 // In case the page was just changed from under us, let's do this small kludge. 177 if( pageName.equals( fromPage.getName() ) ) { 178 pageName = toPage.getName(); 179 } 180 181 final Page p = engine.getManager( PageManager.class ).getPage( pageName ); 182 183 final String sourceText = engine.getManager( PageManager.class ).getPureText( p ); 184 String newText = replaceReferrerString(sourceText, fromPage.getName(), toPage.getName() ); 185 186 m_camelCase = TextUtil.getBooleanProperty( engine.getWikiProperties(), MarkupParser.PROP_CAMELCASELINKS, m_camelCase ); 187 if( m_camelCase ) { 188 newText = replaceCCReferrerString(newText, fromPage.getName(), toPage.getName() ); 189 } 190 191 if( !sourceText.equals( newText ) ) { 192 p.setAttribute( Page.CHANGENOTE, fromPage.getName()+" ==> "+toPage.getName() ); 193 p.setAuthor( context.getCurrentUser().getName() ); 194 195 try { 196 engine.getManager( PageManager.class ).putPageText( p, newText ); 197 engine.getManager( ReferenceManager.class ).updateReferences( p ); 198 } catch( final ProviderException e ) { 199 // We fail with an error, but we will try to continue to rename other referrers as well. 200 LOG.error("Unable to perform rename.",e); 201 } 202 } 203 } 204 } 205 206 private Set<String> getReferencesToChange( final Page fromPage, final Engine engine ) { 207 final Set< String > referrers = new TreeSet<>(); 208 final Collection< String > r = engine.getManager( ReferenceManager.class ).findReferrers( fromPage.getName() ); 209 if( r != null ) { 210 referrers.addAll( r ); 211 } 212 213 try { 214 final List< Attachment > attachments = engine.getManager( AttachmentManager.class ).listAttachments( fromPage ); 215 for( final Attachment att : attachments ) { 216 final Collection< String > c = engine.getManager( ReferenceManager.class ).findReferrers( att.getName() ); 217 if( c != null ) { 218 referrers.addAll( c ); 219 } 220 } 221 } catch( final ProviderException e ) { 222 // We will continue despite this error 223 LOG.error( "Provider error while fetching attachments for rename", e ); 224 } 225 return referrers; 226 } 227 228 /** 229 * Replaces camelcase links. 230 */ 231 private String replaceCCReferrerString( final String sourceText, final String from, final String to ) { 232 final StringBuilder sb = new StringBuilder( sourceText.length()+32 ); 233 final Pattern linkPattern = Pattern.compile( "\\p{Lu}+\\p{Ll}+\\p{Lu}+[\\p{L}\\p{Digit}]*" ); 234 final Matcher matcher = linkPattern.matcher( sourceText ); 235 int start = 0; 236 237 while( matcher.find( start ) ) { 238 final String match = matcher.group(); 239 sb.append( sourceText, start, matcher.start() ); 240 final int lastOpenBrace = sourceText.lastIndexOf( '[', matcher.start() ); 241 final int lastCloseBrace = sourceText.lastIndexOf( ']', matcher.start() ); 242 243 if( match.equals( from ) && lastCloseBrace >= lastOpenBrace ) { 244 sb.append( to ); 245 } else { 246 sb.append( match ); 247 } 248 249 start = matcher.end(); 250 } 251 252 sb.append( sourceText.substring( start ) ); 253 254 return sb.toString(); 255 } 256 257 private String replaceReferrerString(final String sourceText, final String from, final String to ) { 258 final StringBuilder sb = new StringBuilder( sourceText.length()+32 ); 259 260 // This monstrosity just looks for a JSPWiki link pattern. But it is pretty cool for a regexp, isn't it? If you can 261 // understand this in a single reading, you have way too much time in your hands. 262 final Pattern linkPattern = Pattern.compile( "([\\[~]?)\\[([^|\\]]*)(\\|)?([^|\\]]*)(\\|)?([^|\\]]*)]" ); 263 final Matcher matcher = linkPattern.matcher( sourceText ); 264 int start = 0; 265 266 while( matcher.find( start ) ) { 267 char charBefore = (char)-1; 268 269 if( matcher.start() > 0 ) { 270 charBefore = sourceText.charAt( matcher.start() - 1 ); 271 } 272 273 if( !matcher.group(1).isEmpty() || charBefore == '~' || charBefore == '[' ) { 274 // Found an escape character, so I am escaping. 275 sb.append( sourceText, start, matcher.end() ); 276 start = matcher.end(); 277 continue; 278 } 279 280 String text = matcher.group(2); 281 String link = matcher.group(4); 282 final String attr = matcher.group(6); 283 284 if( link.isEmpty() ) { 285 text = replaceSingleLink(text, from, to ); 286 } else { 287 link = replaceSingleLink(link, from, to ); 288 289 // A very simple substitution, but should work for quite a few cases. 290 text = TextUtil.replaceString( text, from, to ); 291 } 292 293 // 294 // Construct the new string 295 // 296 sb.append( sourceText, start, matcher.start() ); 297 sb.append( "[" ).append( text ); 298 if( !link.isEmpty() ) { 299 sb.append( "|" ).append( link ); 300 } 301 if( !attr.isEmpty() ) { 302 sb.append( "|" ).append( attr ); 303 } 304 sb.append( "]" ); 305 306 start = matcher.end(); 307 } 308 309 sb.append( sourceText.substring( start ) ); 310 311 return sb.toString(); 312 } 313 314 /** 315 * This method does a correct replacement of a single link, taking into account anchors and attachments. 316 */ 317 private String replaceSingleLink(final String original, final String from, final String newlink ) { 318 final int hash = original.indexOf( '#' ); 319 final int slash = original.indexOf( '/' ); 320 String realLink = original; 321 322 if( hash != -1 ) { 323 realLink = original.substring( 0, hash ); 324 } 325 if( slash != -1 ) { 326 realLink = original.substring( 0,slash ); 327 } 328 329 realLink = MarkupParser.cleanLink( realLink ); 330 final String oldStyleRealLink = MarkupParser.wikifyLink( realLink ); 331 332 //WikiPage realPage = context.getEngine().getPage( reallink ); 333 // WikiPage p2 = context.getEngine().getPage( from ); 334 335 // System.out.println(" "+reallink+" :: "+ from); 336 // System.out.println(" "+p+" :: "+p2); 337 338 // 339 // Yes, these point to the same page. 340 // 341 if( realLink.equals( from ) || original.equals( from ) || oldStyleRealLink.equals( from ) ) { 342 // 343 // if the original contains blanks, then we should introduce a link, for example: [My Page] => [My Page|My Renamed Page] 344 final int blank = realLink.indexOf( " "); 345 346 if( blank != -1 ) { 347 return original + "|" + newlink; 348 } 349 350 return newlink + ( ( hash > 0 ) ? original.substring( hash ) : "" ) + ( ( slash > 0 ) ? original.substring( slash ) : "" ) ; 351 } 352 353 return original; 354 } 355 356}