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