1<?php
2/**
3 * DokuWiki Plugin xml
4 *
5 * @author   Patrick Bueker <Patrick@patrickbueker.de>
6 * @author   Danny Lin <danny0838@gmail.com>
7 * @license  GPLv2 or later (http://www.gnu.org/licenses/gpl.html)
8 *
9 */
10
11// must be run within Dokuwiki
12if(!defined('DOKU_INC')) die();
13
14if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
15if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
16require_once DOKU_INC . 'inc/parser/renderer.php';
17
18class renderer_plugin_xml extends Doku_Renderer {
19
20    function __construct() {
21        $this->reset();
22    }
23
24    /**
25     * Allows renderer to be used again. Clean out any per-use values.
26     */
27    function reset() {
28        $this->info = array(
29            'cache' => true, // may the rendered result cached?
30            'toc'   => false, // render the TOC?
31        );
32        $this->precedinglevel = array();
33        $this->nextHeader     = "";
34        $this->helper         = &plugin_load('helper','xml');
35        $this->doc            = '';
36        $this->tagStack       = array();
37    }
38
39    /**
40     * Returns the format produced by this renderer.
41     *
42     * @return string
43     */
44    function getFormat(){return 'xml';}
45
46    /**
47     * handle plugin rendering
48     */
49    function plugin($name,$data){
50        $plugin =& plugin_load('syntax',$name);
51        if ($plugin == null) return;
52        if ($this->helper->_xml_extension($this,$name,$data)) return;
53        $plugin->render($this->getFormat(),$this,$data);
54    }
55
56    /**
57     * Handles instructions
58     *
59     * @Stack:         add post-start linefeed and post-end linefeed
60     * @Stack content: add post-start tab and post-end linefeed
61     * @Block:         add post-end linefeed
62     *
63     * ~~SOMETHING>params~~  treated as <macro type="something" params />
64     */
65    function document_start() {
66        global $ID;
67        global $INFO;
68        $this->doc  = '<?xml version="1.0" encoding="UTF-8"?>'.DOKU_LF;
69        $this->doc .= '<document domain="' . DOKU_URL .'" id="' . cleanID($ID) . '" revision="' . $INFO['rev'] . '" lastmod="' . $INFO['lastmod'] . '">'.DOKU_LF;
70        // store the content type headers in metadata
71        $output_filename = str_replace(':','-',$ID).".xml";
72        $headers = array(
73            'Content-Type' => 'text/xml; charset=utf-8;',
74            'Content-Disposition' => 'attachment; filename="'.$output_filename.'";',
75        );
76        p_set_metadata($ID,array('format' => array('xml' => $headers) ));
77    }
78
79    function document_end() {
80        while(count($this->precedinglevel)>0) {
81            $this->doc .= '</section>'.'<!--' . array_pop($this->precedinglevel) .  '-->'.DOKU_LF;
82        }
83        $this->doc .= '</document>'.DOKU_LF;
84    }
85
86    function header($text, $level, $pos) {
87        if (!$text) return; //skip empty headlines
88        $this->nextHeader  = '<header level="' . $level . '" pos="' . $pos . '">'.
89        $this->nextHeader .= $this->_xmlEntities($text);
90        $this->nextHeader .= '</header>'.DOKU_LF;
91    }
92
93    function section_open($level) {
94        while(end($this->precedinglevel) >= $level)
95        {
96            $this->doc .= '</section>'.'<!--' . array_pop($this->precedinglevel) .  '-->'.DOKU_LF;
97        }
98
99        $this->doc .= '<section level="' . $level . '">'.DOKU_LF;
100        $this->doc .= $this->nextHeader;
101        $this->nextHeader = "";
102        array_push($this->precedinglevel,$level);
103    }
104
105    function section_close() {
106        #$this->doc .= '</section>'.DOKU_LF;
107    }
108
109    function nocache() {
110        $this->info['cache'] = false;
111        $this->doc .= '<macro name="nocache" />'.DOKU_LF;
112    }
113
114    function notoc() {
115        $this->info['toc'] = false;
116        $this->doc .= '<macro name="notoc" />'.DOKU_LF;
117    }
118
119    function cdata($text) {
120        $this->doc .= $this->_xmlEntities($text);
121    }
122
123    function p_open() {
124        $this->doc .= '<p>';
125        $this->_openTag($this, 'p_close', array());
126    }
127
128    function p_close() {
129        $this->_closeTags($this, __FUNCTION__);
130        $this->doc .= '</p>'.DOKU_LF;
131    }
132
133    function linebreak() {
134        $this->doc .= '<linebreak/>';
135    }
136
137    function hr() {
138        $this->doc .= '<hr/>'.DOKU_LF;
139    }
140
141    function strong_open() {
142        $this->doc .= '<strong>';
143        $this->_openTag($this, 'strong_close', array());
144    }
145
146    function strong_close() {
147        $this->_closeTags($this, __FUNCTION__);
148        $this->doc .= '</strong>';
149    }
150
151    function emphasis_open() {
152        $this->doc .= '<emphasis>';
153        $this->_openTag($this, 'emphasis_close', array());
154    }
155
156    function emphasis_close() {
157        $this->_closeTags($this, __FUNCTION__);
158        $this->doc .= '</emphasis>';
159    }
160
161    function underline_open() {
162        $this->doc .= '<underline>';
163        $this->_openTag($this, 'underline_close', array());
164    }
165
166    function underline_close() {
167        $this->_closeTags($this, __FUNCTION__);
168        $this->doc .= '</underline>';
169    }
170
171    function monospace_open() {
172        $this->doc .= '<monospace>';
173        $this->_openTag($this, 'monospace_close', array());
174    }
175
176    function monospace_close() {
177        $this->_closeTags($this, __FUNCTION__);
178        $this->doc .= '</monospace>';
179    }
180
181    function subscript_open() {
182        $this->doc .= '<subscript>';
183        $this->_openTag($this, 'subscript_close', array());
184    }
185
186    function subscript_close() {
187        $this->_closeTags($this, __FUNCTION__);
188        $this->doc .= '</subscript>';
189    }
190
191    function superscript_open() {
192        $this->doc .= '<superscript>';
193        $this->_openTag($this, 'superscript_close', array());
194    }
195
196    function superscript_close() {
197        $this->_closeTags($this, __FUNCTION__);
198        $this->doc .= '</superscript>';
199    }
200
201    function deleted_open() {
202        $this->doc .= '<delete>';
203        $this->_openTag($this, 'deleted_close', array());
204    }
205
206    function deleted_close() {
207        $this->_closeTags($this, __FUNCTION__);
208        $this->doc .= '</delete>';
209    }
210
211    function footnote_open() {
212        $this->doc .= '<footnote>';
213        $this->_openTag($this, 'footnote_close', array());
214    }
215
216    function footnote_close() {
217        $this->_closeTags($this, __FUNCTION__);
218        $this->doc .= '</footnote>';
219    }
220
221    function listu_open() {
222        $this->doc .= '<listu>'.DOKU_LF;
223        $this->_openTag($this, 'listu_close', array());
224    }
225
226    function listu_close() {
227        $this->_closeTags($this, __FUNCTION__);
228        $this->doc .= '</listu>'.DOKU_LF;
229    }
230
231    function listo_open() {
232        $this->doc .= '<listo>'.DOKU_LF;
233        $this->_openTag($this, 'listo_close', array());
234    }
235
236    function listo_close() {
237        $this->_closeTags($this, __FUNCTION__);
238        $this->doc .= '</listo>'.DOKU_LF;
239    }
240
241    function listitem_open($level) {
242        $this->doc .= DOKU_TAB.'<listitem level="' . $level . '">';
243        $this->_openTag($this, 'listitem_close', array());
244    }
245
246    function listitem_close() {
247        $this->_closeTags($this, __FUNCTION__);
248        $this->doc .= '</listitem>'.DOKU_LF;
249    }
250
251    function listcontent_open() {
252        $this->doc .= '<listcontent>';
253        $this->_openTag($this, 'listcontent_close', array());
254    }
255
256    function listcontent_close() {
257        $this->_closeTags($this, __FUNCTION__);
258        $this->doc .= '</listcontent>';
259    }
260
261    function unformatted($text) {
262        $this->doc .= '<unformatted>';
263        $this->doc .= $this->_xmlEntities($text);
264        $this->doc .= '</unformatted>';
265    }
266
267    function php($text) {
268        $this->doc .= '<php>';
269        $this->doc .= $this->_xmlEntities($text);
270        $this->doc .= '</php>';
271    }
272
273    function phpblock($text) {
274        $this->doc .= '<phpblock>';
275        $this->doc .= $this->_xmlEntities($text);
276        $this->doc .= '</phpblock>'.DOKU_LF;
277    }
278
279    function html($text) {
280        $this->doc .= '<html>';
281        $this->doc .= $this->_xmlEntities($text);
282        $this->doc .= '</html>';
283    }
284
285    function htmlblock($text) {
286        $this->doc .= '<htmlblock>';
287        $this->doc .= $this->_xmlEntities($text);
288        $this->doc .= '</htmlblock>'.DOKU_LF;
289    }
290
291    function preformatted($text) {
292        $this->doc .= '<preformatted>';
293        $this->doc .= $this->_xmlEntities($text);
294        $this->doc .= '</preformatted>'.DOKU_LF;
295    }
296
297    function quote_open() {
298        $this->doc .= '<quote>';
299        $this->_openTag($this, 'quote_close', array());
300    }
301
302    function quote_close() {
303        $this->_closeTags($this, __FUNCTION__);
304        $this->doc .= '</quote>'.DOKU_LF;
305    }
306
307    function code($text, $lang = null, $file = null) {
308        $this->doc .= '<code lang="' . $lang . '" file="' . $file . '">';
309        $this->doc .= $this->_xmlEntities($text);
310        $this->doc .= '</code>'.DOKU_LF;
311    }
312
313    function file($text, $lang = null, $file = null) {
314        $this->doc .= '<file lang="' . $lang . '" file="' . $file . '">';
315        $this->doc .= $this->_xmlEntities($text);
316        $this->doc .= '</file>'.DOKU_LF;
317    }
318
319    function acronym($acronym) {
320        $this->doc .= '<acronym data="' . $this->_xmlEntities($this->acronyms[$acronym]) . '">';
321        $this->doc .= $this->_xmlEntities($acronym);
322        $this->doc .= '</acronym>';
323    }
324
325    function smiley($smiley) {
326        $this->doc .= '<smiley>';
327        $this->doc .= $this->_xmlEntities($smiley);
328        $this->doc .= '</smiley>';
329    }
330
331    function entity($entity) {
332        $this->doc .= '<entity data="' . $this->_xmlEntities($this->entities[$entity]) . '">';
333        $this->doc .= $this->_xmlEntities($entity);
334        $this->doc .= '</entity>';
335    }
336
337    /**
338     * Multiply entities are of the form: 640x480 where $x=640 and $y=480
339     *
340     * @param string $x The left hand operand
341     * @param string $y The rigth hand operand
342     */
343    function multiplyentity($x, $y) {
344        $this->doc .= '<multiplyentity>';
345        $this->doc .= '<x>'.$this->_xmlEntities($x).'</x>';
346        $this->doc .= '<y>'.$this->_xmlEntities($y).'</y>';
347        $this->doc .= '</multiplyentity>';
348    }
349
350    function singlequoteopening() {
351        global $lang;
352        $this->doc .= '<singlequote open="' . $this->_xmlEntities($lang['singlequoteopening']) . '" close="' . $this->_xmlEntities($lang['singlequoteclosing']) . '">';
353        $this->_openTag($this, 'singlequoteclosing', array());
354    }
355
356    function singlequoteclosing() {
357        $this->_closeTags($this, __FUNCTION__);
358        $this->doc .= '</singlequote>';
359    }
360
361    function apostrophe() {
362        global $lang;
363        $this->doc .= '<apostrophe data="' . $this->_xmlEntities($lang['apostrophe']) . '"/>';
364    }
365
366    function doublequoteopening() {
367        global $lang;
368        $this->doc .= '<doublequote open="' . $this->_xmlEntities($lang['doublequoteopening']) . '" close="' . $this->_xmlEntities($lang['doublequoteclosing']) . '">';
369        $this->_openTag($this, 'doublequoteclosing', array());
370    }
371
372    function doublequoteclosing() {
373        $this->_closeTags($this, __FUNCTION__);
374        $this->doc .= '</doublequote>';
375    }
376
377    /**
378     * Links in CamelCase format.
379     *
380     * @param string $link Link text
381     */
382    function camelcaselink($link) {
383        $this->internallink($link, $link, 'camelcase');
384    }
385
386    function locallink($hash, $name = null) {
387        $this->doc .= '<link type="locallink" link="'.$this->_xmlEntities($hash).'" href="'.$this->_xmlEntities($hash).'">';
388        $this->doc .= $this->_getLinkTitle($name, $hash, $isImage);
389        $this->doc .= '</link>';
390    }
391
392    /**
393     * Links of the form 'wiki:syntax', where $title is either a string or (for
394     * media links) an array.
395     *
396     * @param string $link The link text
397     * @param mixed $title Title text (array for media links)
398     * @param string $type overwrite the type (for camelcaselink)
399     */
400    function internallink($link, $title = null, $type='internal') {
401        global $ID;
402        $id = $link;
403        $name = $title;
404        list($id, $hash) = explode('#', $id, 2);
405        list($id, $search) = explode('?', $id, 2);
406        if ($id === '') $id = $ID;
407        $default = $this->_simpleTitle($id);
408        resolve_pageid(getNS($ID), $id, $exists);
409        $name = $this->_getLinkTitle($name, $default, $isImage, $id, 'content');
410        $this->doc .= '<link type="'.$type.'" link="'.$this->_xmlEntities($link).'" id="'.$id.'" search="'.$this->_xmlEntities($search).'" hash="'.$this->_xmlEntities($hash).'">';
411        $this->doc .= $name;
412        $this->doc .= '</link>';
413    }
414
415    /**
416     * Full URL links with scheme. $title could be an array, for media links.
417     *
418     * @param string $link The link text
419     * @param mixed $title Title text (array for media links)
420     */
421    function externallink($link, $title = null) {
422        $this->doc .= '<link type="external" link="'.$this->_xmlEntities($link).'" href="'.$this->_xmlEntities($link).'">';
423        $this->doc .= $this->_getLinkTitle($title, $link, $isImage);
424        $this->doc .= '</link>';
425    }
426
427    /**
428     * @param string $link the original link - probably not much use
429     * @param string $title
430     * @param string $wikiName an indentifier for the wiki
431     * @param string $wikiUri the URL fragment to append to some known URL
432     */
433    function interwikilink($link, $title = null, $wikiName, $wikiUri) {
434        $name = $this->_getLinkTitle($title, $wikiUri, $isImage);
435        $url = $this->_resolveInterWiki($wikiName, $wikiUri);
436        $this->doc .= '<link type="interwiki" link="'.$this->_xmlEntities($link).'" href="'.$this->_xmlEntities($url).'">';
437        $this->doc .= $name;
438        $this->doc .= '</link>';
439    }
440
441    /**
442     * Link to a Windows share, $title could be an array (media)
443     *
444     * @param string $link
445     * @param mixed $title
446     */
447    function windowssharelink($link, $title = null) {
448        $name = $this->_getLinkTitle($title, $link, $isImage);
449        $url = str_replace('\\','/',$link);
450        $url = 'file:///'.$url;
451        $this->doc .= '<link type="windowssharelink" link="'.$this->_xmlEntities($link).'" href="'.$this->_xmlEntities($url).'">';
452        $this->doc .= $name;
453        $this->doc .= '</link>';
454    }
455
456    function emaillink($address, $name = null) {
457        $name = $this->_getLinkTitle($name, '', $isImage);
458        $url = $this->_xmlEntities($address);
459        $url = obfuscate($url);
460        $url   = 'mailto:'.$url;
461        $this->doc .= '<link type="emaillink" link="'.$this->_xmlEntities($address).'" href="'.$url.'">';
462        $this->doc .= $name;
463        $this->doc .= '</link>';
464    }
465
466    /**
467     * Render media that is internal to the wiki.
468     *
469     * @param string $src
470     * @param string $title
471     * @param string $align
472     * @param string $width
473     * @param string $height
474     * @param string $cache
475     * @param string $linking
476     */
477    function internalmedia ($src, $title=null, $align=null, $width=null, $height=null, $cache=null, $linking=null) {
478        $this->doc .= $this->_media('internalmedia', $src, $title, $align, $width, $height, $cache, $linking);
479    }
480
481    /**
482     * Render media that is external to the wiki.
483     *
484     * @param string $src
485     * @param string $title
486     * @param string $align
487     * @param string $width
488     * @param string $height
489     * @param string $cache
490     * @param string $linking
491     */
492    function externalmedia ($src, $title=null, $align=null, $width=null, $height=null, $cache=null, $linking=null) {
493        $this->doc .= $this->_media('externalmedia', $src, $title, $align, $width, $height, $cache, $linking);
494    }
495
496    function table_open($maxcols = null, $numrows = null){
497        $this->doc .= '<table maxcols="' . $maxcols . '" numrows="' . $numrows . '">'.DOKU_LF;
498        $this->_openTag($this, 'table_close', array());
499    }
500
501    function table_close(){
502        $this->_closeTags($this, __FUNCTION__);
503        $this->doc .= '</table>'.DOKU_LF;
504    }
505
506    function tablerow_open(){
507        $this->doc .= DOKU_TAB.'<tablerow>';
508        $this->_openTag($this, 'tablerow_close', array());
509    }
510
511    function tablerow_close(){
512        $this->_closeTags($this, __FUNCTION__);
513        $this->doc .= '</tablerow>'.DOKU_LF;
514    }
515
516    function tableheader_open($colspan = 1, $align = null, $rowspan = 1){
517        $this->doc .= '<tableheader';
518        if ($colspan>1) $this->doc .= ' colspan="' . $colspan . '"';
519        if ($rowspan>1) $this->doc .= ' rowspan="' . $rowspan . '"';
520        if ($align) $this->doc .= ' align="' . $align . '"';
521        $this->doc .= '>';
522        $this->_openTag($this, 'tableheader_close', array());
523    }
524
525    function tableheader_close(){
526        $this->_closeTags($this, __FUNCTION__);
527        $this->doc .= '</tableheader>';
528    }
529
530    function tablecell_open($colspan = 1, $align = null, $rowspan = 1) {
531        $this->doc .= '<tablecell';
532        if ($colspan>1) $this->doc .= ' colspan="' . $colspan . '"';
533        if ($rowspan>1) $this->doc .= ' rowspan="' . $rowspan . '"';
534        if ($align) $this->doc .= ' align="' . $align . '"';
535        $this->doc .= '>';
536        $this->_openTag($this, 'tablecell_close', array());
537    }
538
539    function tablecell_close(){
540        $this->_closeTags($this, __FUNCTION__);
541        $this->doc .= '</tablecell>';
542    }
543
544    /**
545     * Private functions for internal handling
546     */
547    function _xmlEntities($text){
548        return htmlspecialchars($text,ENT_COMPAT,'UTF-8');
549    }
550
551    /**
552     * Render media elements.
553     * @see Doku_Renderer_xhtml::internalmedia()
554     *
555     * @param string $type Either 'internalmedia' or 'externalmedia'
556     * @param string $src
557     * @param string $title
558     * @param string $align
559     * @param string $width
560     * @param string $height
561     * @param string $cache
562     * @param string $linking
563     */
564    function _media($type, $src, $title=null, $align=null, $width=null, $height=null, $cache=null, $linking = null) {
565        global $ID;
566        $link = $src;
567        list($src, $hash) = explode('#', $src, 2);
568        if ($type == 'internalmedia') {
569            resolve_mediaid(getNS($ID), $src, $exists);
570        }
571        $name = $title ? $this->_xmlEntities($title) : $this->_xmlEntities(utf8_basename(noNS($src)));
572        if ($type == 'internalmedia') {
573            $src = ' id="'.$this->_xmlEntities($src).'" hash="'.$this->_xmlEntities($hash).'"';
574        }
575        else {
576            $src = ' src="'.$this->_xmlEntities($src).'"';
577        }
578        $out .= '<media type="'.$type.'" link="'.$this->_xmlEntities($link).'"'.($src).' align="'.$align.'" width="'.$width.'" height="'.$height.'" cache="'.$cache.'" linking="'.$linking.'">';
579        $out .= $name;
580        $out .= '</media>';
581        return $out;
582    }
583
584    function _getLinkTitle($title, $default, & $isImage, $id=null, $linktype='content'){
585        $isImage = false;
586        if ( is_array($title) ) {
587            $isImage = true;
588            return $this->_imageTitle($title);
589        } elseif ( is_null($title) || trim($title)=='') {
590            if (useHeading($linktype) && $id) {
591                $heading = p_get_first_heading($id);
592                if ($heading) {
593                    return $this->_xmlEntities($heading);
594                }
595            }
596            return $this->_xmlEntities($default);
597        } else {
598            return $this->_xmlEntities($title);
599        }
600    }
601
602    function _imageTitle($img) {
603        global $ID;
604
605        // some fixes on $img['src']
606        // see internalmedia() and externalmedia()
607        list($img['src'], $hash) = explode('#', $img['src'], 2);
608        if ($img['type'] == 'internalmedia') {
609            resolve_mediaid(getNS($ID), $img['src'], $exists);
610        }
611
612        return $this->_media($img['type'],
613                              $img['src'],
614                              $img['title'],
615                              $img['align'],
616                              $img['width'],
617                              $img['height'],
618                              $img['cache']);
619    }
620
621    function _openTag($class, $func, $data=null) {
622        $this->tagStack[] = array($class, $func, $data);
623    }
624
625    function _closeTags($class=null, $func=null) {
626        if ($this->tagClosing==true) return;  // skip nested calls
627        $this->tagClosing = true;
628        while(count($this->tagStack)>0) {
629            list($lastclass, $lastfunc, $lastdata) = array_pop($this->tagStack);
630            if (!($lastclass===$class && $lastfunc==$func)) call_user_func_array( array($lastclass, $lastfunc), $lastdata );
631            else break;
632        }
633        $this->tagClosing = false;
634    }
635}
636