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.xmlrpc;
020
021import org.apache.wiki.LinkCollector;
022import org.apache.wiki.api.core.Attachment;
023import org.apache.wiki.api.core.Context;
024import org.apache.wiki.api.core.ContextEnum;
025import org.apache.wiki.api.core.Page;
026import org.apache.wiki.api.spi.Wiki;
027import org.apache.wiki.auth.permissions.PagePermission;
028import org.apache.wiki.auth.permissions.PermissionFactory;
029import org.apache.wiki.pages.PageManager;
030import org.apache.wiki.render.RenderingManager;
031import org.apache.wiki.util.TextUtil;
032import org.apache.xmlrpc.XmlRpcException;
033
034import java.nio.charset.StandardCharsets;
035import java.util.Calendar;
036import java.util.Collection;
037import java.util.Date;
038import java.util.Hashtable;
039import java.util.Set;
040import java.util.Vector;
041import java.util.stream.Collectors;
042
043/**
044 *  Provides handlers for all RPC routines.
045 *
046 *  @since 1.6.6
047 */
048// We could use Engine directly, but because of introspection it would show just too many methods to be safe.
049public class RPCHandler extends AbstractRPCHandler {
050
051    /**
052     *  Converts Java string into RPC string.
053     */
054    private String toRPCString( final String src )
055    {
056        return TextUtil.urlEncodeUTF8( src );
057    }
058
059    /**
060     *  Converts RPC string (UTF-8, url encoded) into Java string.
061     */
062    private String fromRPCString( final String src )
063    {
064        return TextUtil.urlDecodeUTF8( src );
065    }
066
067    /**
068     *  Transforms a Java string into UTF-8.
069     */
070    private byte[] toRPCBase64( final String src )
071    {
072        return src.getBytes( StandardCharsets.UTF_8 );
073    }
074
075    public String getApplicationName() {
076        checkPermission( PagePermission.VIEW );
077        return toRPCString(m_engine.getApplicationName());
078    }
079
080    public Vector< String > getAllPages() {
081        checkPermission( PagePermission.VIEW );
082        final Collection< Page > pages = m_engine.getManager( PageManager.class ).getRecentChanges();
083
084        return pages.stream().filter(p -> !(p instanceof Attachment)).map(p -> toRPCString(p.getName())).collect(Collectors.toCollection(Vector::new));
085    }
086
087    /**
088     *  Encodes a single wiki page info into a Hashtable.
089     */
090    @Override
091    protected Hashtable<String,Object> encodeWikiPage( final Page page ) {
092        final Hashtable<String, Object> ht = new Hashtable<>();
093        ht.put( "name", toRPCString(page.getName()) );
094
095        final Date d = page.getLastModified();
096
097        //
098        //  Here we reset the DST and TIMEZONE offsets of the
099        //  calendar.  Unfortunately, I haven't thought of a better
100        //  way to ensure that we're getting the proper date
101        //  from the XML-RPC thingy, except to manually adjust the date.
102        //
103
104        final Calendar cal = Calendar.getInstance();
105        cal.setTime( d );
106        cal.add( Calendar.MILLISECOND,
107                 - (cal.get( Calendar.ZONE_OFFSET ) +
108                    (cal.getTimeZone().inDaylightTime( d ) ? cal.get( Calendar.DST_OFFSET ) : 0 ) ) );
109
110        ht.put( "lastModified", cal.getTime() );
111        ht.put( "version", page.getVersion() );
112
113        if( page.getAuthor() != null ) {
114            ht.put( "author", toRPCString( page.getAuthor() ) );
115        }
116
117        return ht;
118    }
119
120    /**
121     * {@inheritDoc}
122     */
123    @Override
124    public Vector< Hashtable< String, Object > > getRecentChanges( Date since ) {
125        checkPermission( PagePermission.VIEW );
126        final Set< Page > pages = m_engine.getManager( PageManager.class ).getRecentChanges();
127        final Vector< Hashtable< String, Object > > result = new Vector<>();
128
129        final Calendar cal = Calendar.getInstance();
130        cal.setTime( since );
131
132        //
133        //  Convert UTC to our time.
134        //
135        cal.add( Calendar.MILLISECOND,
136                 (cal.get( Calendar.ZONE_OFFSET ) +
137                  (cal.getTimeZone().inDaylightTime(since) ? cal.get( Calendar.DST_OFFSET ) : 0 ) ) );
138        since = cal.getTime();
139
140        for( final Page page : pages ) {
141            if( page.getLastModified().after( since ) && !(page instanceof Attachment) ) {
142                result.add( encodeWikiPage( page ) );
143            }
144        }
145
146        return result;
147    }
148
149    /**
150     *  Simple helper method, turns the incoming page name into
151     *  normal Java string, then checks page condition.
152     *
153     *  @param pagename Page Name as an RPC string (URL-encoded UTF-8)
154     *  @return Real page name, as Java string.
155     *  @throws XmlRpcException, if there is something wrong with the page.
156     */
157    private String parsePageCheckCondition( String pagename ) throws XmlRpcException {
158        pagename = fromRPCString( pagename );
159
160        if( !m_engine.getManager( PageManager.class ).wikiPageExists(pagename) ) {
161            throw new XmlRpcException( ERR_NOPAGE, "No such page '"+pagename+"' found, o master." );
162        }
163
164        final Page p = m_engine.getManager( PageManager.class ).getPage( pagename );
165
166        checkPermission( PermissionFactory.getPagePermission( p, PagePermission.VIEW_ACTION ) );
167
168        return pagename;
169    }
170
171    public Hashtable< String, Object > getPageInfo( String pagename ) throws XmlRpcException {
172        pagename = parsePageCheckCondition( pagename );
173        return encodeWikiPage( m_engine.getManager( PageManager.class ).getPage(pagename) );
174    }
175
176    public Hashtable< String, Object > getPageInfoVersion( String pagename, final int version ) throws XmlRpcException {
177        pagename = parsePageCheckCondition( pagename );
178
179        return encodeWikiPage( m_engine.getManager( PageManager.class ).getPage( pagename, version ) );
180    }
181
182    public byte[] getPage( final String pagename ) throws XmlRpcException {
183        final String text = m_engine.getManager( PageManager.class ).getPureText( parsePageCheckCondition( pagename ), -1 );
184        return toRPCBase64( text );
185    }
186
187    public byte[] getPageVersion( String pagename, final int version ) throws XmlRpcException {
188        pagename = parsePageCheckCondition( pagename );
189
190        return toRPCBase64( m_engine.getManager( PageManager.class ).getPureText( pagename, version ) );
191    }
192
193    public byte[] getPageHTML( String pagename ) throws XmlRpcException {
194        pagename = parsePageCheckCondition( pagename );
195
196        return toRPCBase64( m_engine.getManager( RenderingManager.class ).getHTML( pagename ) );
197    }
198
199    public byte[] getPageHTMLVersion( String pagename, final int version ) throws XmlRpcException {
200        pagename = parsePageCheckCondition( pagename );
201
202        return toRPCBase64( m_engine.getManager( RenderingManager.class ).getHTML( pagename, version ) );
203    }
204
205    public Vector< Hashtable< String, String > > listLinks( String pagename ) throws XmlRpcException {
206        pagename = parsePageCheckCondition( pagename );
207
208        final Page page = m_engine.getManager( PageManager.class ).getPage( pagename );
209        final String pagedata = m_engine.getManager( PageManager.class ).getPureText( page );
210
211        final LinkCollector localCollector = new LinkCollector();
212        final LinkCollector extCollector   = new LinkCollector();
213        final LinkCollector attCollector   = new LinkCollector();
214
215        final Context context = Wiki.context().create( m_engine, page );
216        m_engine.getManager( RenderingManager.class ).textToHTML( context, pagedata, localCollector, extCollector, attCollector );
217
218        final Vector< Hashtable< String, String > > result = new Vector<>();
219
220        //
221        //  Add local links.
222        //
223        for( final String link : localCollector.getLinks() ) {
224            final Hashtable< String, String > ht = new Hashtable<>();
225            ht.put( "page", toRPCString( link ) );
226            ht.put( "type", LINK_LOCAL );
227
228            //
229            //  FIXME: This is a kludge.  The link format should really be queried
230            //  from the TranslatorReader itself.  Also, the link format should probably
231            //  have information on whether the page exists or not.
232            //
233
234            //
235            //  FIXME: The current link collector interface is not very good, since it causes this.
236            //
237
238            if( m_engine.getManager( PageManager.class ).wikiPageExists( link ) ) {
239                ht.put( "href", context.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), link ) );
240            } else {
241                ht.put( "href", context.getURL( ContextEnum.PAGE_EDIT.getRequestContext(), link ) );
242            }
243
244            result.add( ht );
245        }
246
247        //
248        // Add links to inline attachments
249        //
250        for( final String link : attCollector.getLinks() ) {
251            final Hashtable<String, String> ht = new Hashtable<>();
252            ht.put( "page", toRPCString( link ) );
253            ht.put( "type", LINK_LOCAL );
254            ht.put( "href", context.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), link ) );
255            result.add( ht );
256        }
257
258        //
259        // External links don't need to be changed into XML-RPC strings, simply because URLs are by definition ASCII.
260        //
261        for( final String link : extCollector.getLinks() ) {
262            final Hashtable<String, String> ht = new Hashtable<>();
263            ht.put( "page", link );
264            ht.put( "type", LINK_EXTERNAL );
265            ht.put( "href", link );
266            result.add( ht );
267        }
268
269        return result;
270    }
271
272}