    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}