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.pages;
020
021import java.io.IOException;
022import java.security.Permission;
023import java.security.Principal;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Date;
027import java.util.Enumeration;
028import java.util.Iterator;
029import java.util.List;
030import java.util.Properties;
031import java.util.concurrent.ConcurrentHashMap;
032
033import org.apache.commons.lang.ArrayUtils;
034import org.apache.log4j.Logger;
035import org.apache.wiki.WikiBackgroundThread;
036import org.apache.wiki.WikiEngine;
037import org.apache.wiki.WikiPage;
038import org.apache.wiki.WikiProvider;
039import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
040import org.apache.wiki.api.exceptions.ProviderException;
041import org.apache.wiki.api.exceptions.WikiException;
042import org.apache.wiki.auth.WikiPrincipal;
043import org.apache.wiki.auth.WikiSecurityException;
044import org.apache.wiki.auth.acl.Acl;
045import org.apache.wiki.auth.acl.AclEntry;
046import org.apache.wiki.auth.acl.AclEntryImpl;
047import org.apache.wiki.auth.user.UserProfile;
048import org.apache.wiki.event.WikiEvent;
049import org.apache.wiki.event.WikiEventManager;
050import org.apache.wiki.event.WikiPageEvent;
051import org.apache.wiki.event.WikiSecurityEvent;
052import org.apache.wiki.modules.ModuleManager;
053import org.apache.wiki.modules.WikiModuleInfo;
054import org.apache.wiki.providers.RepositoryModifiedException;
055import org.apache.wiki.providers.WikiPageProvider;
056import org.apache.wiki.util.ClassUtil;
057import org.apache.wiki.util.TextUtil;
058
059
060/**
061 * Manages the WikiPages.  This class functions as an unified interface towards
062 * the page providers.  It handles initialization and management of the providers,
063 * and provides utility methods for accessing the contents.
064 * <p/>
065 * Saving a page is a two-stage Task; first the pre-save operations and then the
066 * actual save.  See the descriptions of the tasks for further information.
067 *
068 * @since 2.0
069 */
070// FIXME: This class currently only functions just as an extra layer over providers,
071//        complicating things.  We need to move more provider-specific functionality
072//        from WikiEngine (which is too big now) into this class.
073public class DefaultPageManager extends ModuleManager implements PageManager {
074
075    private static final Logger LOG = Logger.getLogger(DefaultPageManager.class);
076
077    private WikiPageProvider m_provider;
078
079    protected ConcurrentHashMap<String, PageLock> m_pageLocks = new ConcurrentHashMap<>();
080
081    private WikiEngine m_engine;
082
083    private int m_expiryTime = 60;
084
085    private LockReaper m_reaper = null;
086
087    private PageSorter pageSorter = new PageSorter();
088
089    /**
090     * Creates a new PageManager.
091     *
092     * @param engine WikiEngine instance
093     * @param props  Properties to use for initialization
094     * @throws WikiException If anything goes wrong, you get this.
095     */
096    public DefaultPageManager(WikiEngine engine, Properties props) throws WikiException {
097        super(engine);
098        String classname;
099        m_engine = engine;
100        boolean useCache = "true".equals(props.getProperty(PROP_USECACHE));
101
102        m_expiryTime = TextUtil.parseIntParameter(props.getProperty(PROP_LOCKEXPIRY), 60);
103
104        //
105        //  If user wants to use a cache, then we'll use the CachingProvider.
106        //
107        if (useCache) {
108            classname = "org.apache.wiki.providers.CachingProvider";
109        } else {
110            classname = m_engine.getRequiredProperty(props, PROP_PAGEPROVIDER);
111        }
112
113        pageSorter.initialize( props );
114
115        try {
116            LOG.debug("Page provider class: '" + classname + "'");
117            Class<?> providerclass = ClassUtil.findClass("org.apache.wiki.providers", classname);
118            m_provider = (WikiPageProvider) providerclass.newInstance();
119
120            LOG.debug("Initializing page provider class " + m_provider);
121            m_provider.initialize(m_engine, props);
122        } catch (ClassNotFoundException e) {
123            LOG.error("Unable to locate provider class '" + classname + "' (" + e.getMessage() + ")", e);
124            throw new WikiException("No provider class. (" + e.getMessage() + ")", e);
125        } catch (InstantiationException e) {
126            LOG.error("Unable to create provider class '" + classname + "' (" + e.getMessage() + ")", e);
127            throw new WikiException("Faulty provider class. (" + e.getMessage() + ")", e);
128        } catch (IllegalAccessException e) {
129            LOG.error("Illegal access to provider class '" + classname + "' (" + e.getMessage() + ")", e);
130            throw new WikiException("Illegal provider class. (" + e.getMessage() + ")", e);
131        } catch (NoRequiredPropertyException e) {
132            LOG.error("Provider did not found a property it was looking for: " + e.getMessage(), e);
133            throw e;  // Same exception works.
134        } catch (IOException e) {
135            LOG.error("An I/O exception occurred while trying to create a new page provider: " + classname, e);
136            throw new WikiException("Unable to start page provider: " + e.getMessage(), e);
137        }
138
139    }
140
141    /* (non-Javadoc)
142     * @see org.apache.wiki.pages.PageManager#getProvider()
143     */
144    @Override
145    public WikiPageProvider getProvider() {
146        return m_provider;
147    }
148
149    /* (non-Javadoc)
150     * @see org.apache.wiki.pages.PageManager#getAllPages()
151     */
152    @Override
153    public Collection< WikiPage > getAllPages() throws ProviderException {
154        return m_provider.getAllPages();
155    }
156
157    /* (non-Javadoc)
158     * @see org.apache.wiki.pages.PageManager#getPageText(java.lang.String, int)
159     */
160    @Override
161    public String getPageText(String pageName, int version) throws ProviderException {
162        if (pageName == null || pageName.length() == 0) {
163            throw new ProviderException("Illegal page name");
164        }
165        String text = null;
166
167        try {
168            text = m_provider.getPageText(pageName, version);
169        } catch (RepositoryModifiedException e) {
170            //
171            //  This only occurs with the latest version.
172            //
173            LOG.info("Repository has been modified externally while fetching page " + pageName);
174
175            //
176            //  Empty the references and yay, it shall be recalculated
177            //
178            //WikiPage p = new WikiPage( pageName );
179            WikiPage p = m_provider.getPageInfo(pageName, version);
180
181            m_engine.updateReferences(p);
182
183            if (p != null) {
184                m_engine.getSearchManager().reindexPage(p);
185                text = m_provider.getPageText(pageName, version);
186            } else {
187                //
188                //  Make sure that it no longer exists in internal data structures either.
189                //
190                WikiPage dummy = new WikiPage(m_engine, pageName);
191                m_engine.getSearchManager().pageRemoved(dummy);
192                m_engine.getReferenceManager().pageRemoved(dummy);
193            }
194        }
195
196        return text;
197    }
198
199    /* (non-Javadoc)
200     * @see org.apache.wiki.pages.PageManager#getEngine()
201     */
202    @Override
203    public WikiEngine getEngine() {
204        return m_engine;
205    }
206
207    /* (non-Javadoc)
208     * @see org.apache.wiki.pages.PageManager#putPageText(org.apache.wiki.WikiPage, java.lang.String)
209     */
210    @Override
211    public void putPageText(WikiPage page, String content) throws ProviderException {
212        if (page == null || page.getName() == null || page.getName().length() == 0) {
213            throw new ProviderException("Illegal page name");
214        }
215
216        m_provider.putPageText(page, content);
217    }
218
219    /* (non-Javadoc)
220     * @see org.apache.wiki.pages.PageManager#lockPage(org.apache.wiki.WikiPage, java.lang.String)
221     */
222    @Override
223    public PageLock lockPage(WikiPage page, String user) {
224        if (m_reaper == null) {
225            //
226            //  Start the lock reaper lazily.  We don't want to start it in
227            //  the constructor, because starting threads in constructors
228            //  is a bad idea when it comes to inheritance.  Besides,
229            //  laziness is a virtue.
230            //
231            m_reaper = new LockReaper(m_engine);
232            m_reaper.start();
233        }
234
235        fireEvent(WikiPageEvent.PAGE_LOCK, page.getName()); // prior to or after actual lock?
236        PageLock lock = m_pageLocks.get(page.getName());
237
238        if (lock == null) {
239            //
240            //  Lock is available, so make a lock.
241            //
242            Date d = new Date();
243            lock = new PageLock(page, user, d, new Date(d.getTime() + m_expiryTime * 60 * 1000L));
244            m_pageLocks.put(page.getName(), lock);
245            LOG.debug("Locked page " + page.getName() + " for " + user);
246        } else {
247            LOG.debug("Page " + page.getName() + " already locked by " + lock.getLocker());
248            lock = null; // Nothing to return
249        }
250
251        return lock;
252    }
253
254    /* (non-Javadoc)
255     * @see org.apache.wiki.pages.PageManager#unlockPage(org.apache.wiki.pages.PageLock)
256     */
257    @Override
258    public void unlockPage(PageLock lock) {
259        if (lock == null) {
260            return;
261        }
262
263        m_pageLocks.remove(lock.getPage());
264        LOG.debug("Unlocked page " + lock.getPage());
265
266        fireEvent(WikiPageEvent.PAGE_UNLOCK, lock.getPage());
267    }
268
269    /* (non-Javadoc)
270     * @see org.apache.wiki.pages.PageManager#getCurrentLock(org.apache.wiki.WikiPage)
271     */
272    @Override
273    public PageLock getCurrentLock(WikiPage page) {
274        return m_pageLocks.get(page.getName());
275    }
276
277    /* (non-Javadoc)
278     * @see org.apache.wiki.pages.PageManager#getActiveLocks()
279     */
280    @Override
281    public List<PageLock> getActiveLocks() {
282        ArrayList<PageLock> result = new ArrayList<>();
283
284        for (PageLock lock : m_pageLocks.values()) {
285            result.add(lock);
286        }
287
288        return result;
289    }
290
291    /* (non-Javadoc)
292     * @see org.apache.wiki.pages.PageManager#getPageInfo(java.lang.String, int)
293     */
294    @Override
295    public WikiPage getPageInfo(String pageName, int version) throws ProviderException {
296        if (pageName == null || pageName.length() == 0) {
297            throw new ProviderException("Illegal page name '" + pageName + "'");
298        }
299
300        WikiPage page = null;
301
302        try {
303            page = m_provider.getPageInfo(pageName, version);
304        } catch (RepositoryModifiedException e) {
305            //
306            //  This only occurs with the latest version.
307            //
308            LOG.info("Repository has been modified externally while fetching info for " + pageName);
309            page = m_provider.getPageInfo(pageName, version);
310            if (page != null) {
311                m_engine.updateReferences(page);
312            } else {
313                m_engine.getReferenceManager().pageRemoved(new WikiPage(m_engine, pageName));
314            }
315        }
316
317        //
318        //  Should update the metadata.
319        //
320        /*
321        if( page != null && !page.hasMetadata() )
322        {
323            WikiContext ctx = new WikiContext(m_engine,page);
324            m_engine.textToHTML( ctx, getPageText(pageName,version) );
325        }
326        */
327        return page;
328    }
329
330    /* (non-Javadoc)
331     * @see org.apache.wiki.pages.PageManager#getVersionHistory(java.lang.String)
332     */
333    @Override
334    public List< WikiPage > getVersionHistory(String pageName) throws ProviderException {
335        if (pageExists(pageName)) {
336            return m_provider.getVersionHistory(pageName);
337        }
338
339        return null;
340    }
341
342    /* (non-Javadoc)
343     * @see org.apache.wiki.pages.PageManager#getProviderDescription()
344     */
345    @Override
346    public String getProviderDescription() {
347        return m_provider.getProviderInfo();
348    }
349
350    /* (non-Javadoc)
351     * @see org.apache.wiki.pages.PageManager#getTotalPageCount()
352     */
353    @Override
354    public int getTotalPageCount() {
355        try {
356            return m_provider.getAllPages().size();
357        } catch (ProviderException e) {
358            LOG.error("Unable to count pages: ", e);
359            return -1;
360        }
361    }
362
363    /* (non-Javadoc)
364     * @see org.apache.wiki.pages.PageManager#pageExists(java.lang.String)
365     */
366    @Override
367    public boolean pageExists(String pageName) throws ProviderException {
368        if (pageName == null || pageName.length() == 0) {
369            throw new ProviderException("Illegal page name");
370        }
371
372        return m_provider.pageExists(pageName);
373    }
374
375    /* (non-Javadoc)
376     * @see org.apache.wiki.pages.PageManager#pageExists(java.lang.String, int)
377     */
378    @Override
379    public boolean pageExists(String pageName, int version) throws ProviderException {
380        if (pageName == null || pageName.length() == 0) {
381            throw new ProviderException("Illegal page name");
382        }
383
384        if (version == WikiProvider.LATEST_VERSION) {
385            return pageExists(pageName);
386        }
387
388        return m_provider.pageExists(pageName, version);
389    }
390
391    /* (non-Javadoc)
392     * @see org.apache.wiki.pages.PageManager#deleteVersion(org.apache.wiki.WikiPage)
393     */
394    @Override
395    public void deleteVersion(WikiPage page) throws ProviderException {
396        m_provider.deleteVersion(page.getName(), page.getVersion());
397
398        // FIXME: If this was the latest, reindex Lucene
399        // FIXME: Update RefMgr
400    }
401
402    /* (non-Javadoc)
403     * @see org.apache.wiki.pages.PageManager#deletePage(org.apache.wiki.WikiPage)
404     */
405    @Override
406    public void deletePage(WikiPage page) throws ProviderException {
407        fireEvent(WikiPageEvent.PAGE_DELETE_REQUEST, page.getName());
408        m_provider.deletePage(page.getName());
409        fireEvent(WikiPageEvent.PAGE_DELETED, page.getName());
410    }
411
412    /**
413     * This is a simple reaper thread that runs roughly every minute
414     * or so (it's not really that important, as long as it runs),
415     * and removes all locks that have expired.
416     */
417    private class LockReaper extends WikiBackgroundThread {
418        /**
419         * Create a LockReaper for a given engine.
420         *
421         * @param engine WikiEngine to own this thread.
422         */
423        public LockReaper(WikiEngine engine) {
424            super(engine, 60);
425            setName("JSPWiki Lock Reaper");
426        }
427
428        @Override
429        public void backgroundTask() throws Exception {
430            Collection<PageLock> entries = m_pageLocks.values();
431            for (Iterator<PageLock> i = entries.iterator(); i.hasNext(); ) {
432                PageLock p = i.next();
433
434                if ( p.isExpired() ) {
435                    i.remove();
436
437                    LOG.debug("Reaped lock: " + p.getPage() +
438                              " by " + p.getLocker() +
439                              ", acquired " + p.getAcquisitionTime() +
440                              ", and expired " + p.getExpiryTime());
441                }
442            }
443        }
444    }
445
446    // events processing .......................................................
447
448    /**
449     * Fires a WikiPageEvent of the provided type and page name
450     * to all registered listeners.
451     *
452     * @param type     the event type to be fired
453     * @param pagename the wiki page name as a String
454     * @see org.apache.wiki.event.WikiPageEvent
455     */
456    protected final void fireEvent(int type, String pagename) {
457        if (WikiEventManager.isListening(this)) {
458            WikiEventManager.fireEvent(this, new WikiPageEvent(m_engine, type, pagename));
459        }
460    }
461
462    /**
463     * {@inheritDoc}
464     */
465    @Override
466    public Collection< WikiModuleInfo > modules() {
467        return new ArrayList<>();
468    }
469
470    /**
471     * Returns null!
472     *  {@inheritDoc}
473     */
474    @Override
475    public WikiModuleInfo getModuleInfo(String moduleName) {
476        return null;
477    }
478
479    /* (non-Javadoc)
480     * @see org.apache.wiki.pages.PageManager#actionPerformed(org.apache.wiki.event.WikiEvent)
481     */
482    @Override
483    public void actionPerformed(WikiEvent event) {
484        if (!(event instanceof WikiSecurityEvent)) {
485            return;
486        }
487
488        WikiSecurityEvent se = (WikiSecurityEvent) event;
489        if (se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED) {
490            UserProfile[] profiles = (UserProfile[]) se.getTarget();
491            Principal[] oldPrincipals = new Principal[]
492                    {new WikiPrincipal(profiles[0].getLoginName()),
493                            new WikiPrincipal(profiles[0].getFullname()),
494                            new WikiPrincipal(profiles[0].getWikiName())};
495            Principal newPrincipal = new WikiPrincipal(profiles[1].getFullname());
496
497            // Examine each page ACL
498            try {
499                int pagesChanged = 0;
500                Collection< WikiPage > pages = getAllPages();
501                for (Iterator< WikiPage > it = pages.iterator(); it.hasNext(); ) {
502                    WikiPage page = it.next();
503                    boolean aclChanged = changeAcl(page, oldPrincipals, newPrincipal);
504                    if (aclChanged) {
505                        // If the Acl needed changing, change it now
506                        try {
507                            m_engine.getAclManager().setPermissions(page, page.getAcl());
508                        } catch (WikiSecurityException e) {
509                            LOG.error("Could not change page ACL for page " + page.getName() + ": " + e.getMessage(), e);
510                        }
511                        pagesChanged++;
512                    }
513                }
514                LOG.info("Profile name change for '" + newPrincipal.toString() +
515                        "' caused " + pagesChanged + " page ACLs to change also.");
516            } catch (ProviderException e) {
517                // Oooo! This is really bad...
518                LOG.error("Could not change user name in Page ACLs because of Provider error:" + e.getMessage(), e);
519            }
520        }
521    }
522
523    /**
524     * For a single wiki page, replaces all Acl entries matching a supplied array of Principals
525     * with a new Principal.
526     *
527     * @param page          the wiki page whose Acl is to be modified
528     * @param oldPrincipals an array of Principals to replace; all AclEntry objects whose
529     *                      {@link AclEntry#getPrincipal()} method returns one of these Principals will be replaced
530     * @param newPrincipal  the Principal that should receive the old Principals' permissions
531     * @return <code>true</code> if the Acl was actually changed; <code>false</code> otherwise
532     */
533    protected boolean changeAcl(WikiPage page, Principal[] oldPrincipals, Principal newPrincipal) {
534        Acl acl = page.getAcl();
535        boolean pageChanged = false;
536        if (acl != null) {
537            Enumeration<AclEntry> entries = acl.entries();
538            Collection<AclEntry> entriesToAdd = new ArrayList<>();
539            Collection<AclEntry> entriesToRemove = new ArrayList<>();
540            while (entries.hasMoreElements()) {
541                AclEntry entry = entries.nextElement();
542                if (ArrayUtils.contains(oldPrincipals, entry.getPrincipal())) {
543                    // Create new entry
544                    AclEntry newEntry = new AclEntryImpl();
545                    newEntry.setPrincipal(newPrincipal);
546                    Enumeration<Permission> permissions = entry.permissions();
547                    while (permissions.hasMoreElements()) {
548                        Permission permission = permissions.nextElement();
549                        newEntry.addPermission(permission);
550                    }
551                    pageChanged = true;
552                    entriesToRemove.add(entry);
553                    entriesToAdd.add(newEntry);
554                }
555            }
556            for (Iterator<AclEntry> ix = entriesToRemove.iterator(); ix.hasNext(); ) {
557                AclEntry entry = ix.next();
558                acl.removeEntry(entry);
559            }
560            for (Iterator<AclEntry> ix = entriesToAdd.iterator(); ix.hasNext(); ) {
561                AclEntry entry = ix.next();
562                acl.addEntry(entry);
563            }
564        }
565        return pageChanged;
566    }
567
568    /* (non-Javadoc)
569     * @see org.apache.wiki.pages.PageManager#getPageSorter()
570     */
571    @Override
572    public PageSorter getPageSorter() {
573        return pageSorter;
574    }
575
576}