1
<?php
2
/**
3
 * ODT Plugin: Exports to ODT
4
 *
5
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6
 * @author Andreas Gohr <andi@splitbrain.org>
7
 * @author Aurelien Bompard <aurelien@bompard.org>
8
 */
9
// must be run within Dokuwiki
10
if(!defined('DOKU_INC')) die();
11
12
require_once DOKU_INC.'inc/parser/renderer.php';
13
14
// ZipLib.class.php
15
$dw_version = preg_replace('/[^\d]/', '', getversion());
16
if (version_compare($dw_version, "20070626") and 
17
    version_compare(PHP_VERSION,'5.0.0','>')) {
18
    // If strictly newer than 2007-06-26 and use PHP5, fixes to ZipLib are
19
    // included in Dokuwiki's ZipLib
20
    require_once DOKU_INC.'inc/ZipLib.class.php';
21
} else { // for DW up to 2007-06-26, we need the patched version
22
    require_once 'ZipLib.class.php';
23
}
24
25
/**
26
 * The Renderer
27
 */
28
class renderer_plugin_odt extends Doku_Renderer {
29
    var $ZIP = null;
30
    var $meta;
31
    var $store = '';
32
    var $footnotes = array();
33
    var $manifest  = array();
34
    var $headers = array();
35
    var $template = "";
36
    var $fields = array();
37
    var $in_list_item = false;
38
    var $in_paragraph = false;
39
    var $highlight_style_num = 1;
40
    // Automatic styles. Will always be added to content.xml and styles.xml
41
    var $autostyles = array(
42
        "pm1"=>'
43
            <style:page-layout style:name="pm1">
44
                <style:page-layout-properties fo:page-width="21cm" fo:page-height="29.7cm" style:num-format="1" style:print-orientation="portrait" fo:margin-top="2cm" fo:margin-bottom="2cm" fo:margin-left="2cm" fo:margin-right="2cm" style:writing-mode="lr-tb" style:footnote-max-height="0cm">
45
                    <style:footnote-sep style:width="0.018cm" style:distance-before-sep="0.1cm" style:distance-after-sep="0.1cm" style:adjustment="left" style:rel-width="25%" style:color="#000000"/>
46
                </style:page-layout-properties>
47
                <style:header-style/>
48
                <style:footer-style/>
49
            </style:page-layout>',
50
        "sub"=>'
51
            <style:style style:name="sub" style:family="text">
52
                <style:text-properties style:text-position="-33% 80%"/>
53
            </style:style>',
54
        "sup"=>'
55
            <style:style style:name="sup" style:family="text">
56
                <style:text-properties style:text-position="33% 80%"/>
57
            </style:style>',
58
        "del"=>'
59
            <style:style style:name="del" style:family="text">
60
                <style:text-properties style:text-line-through-style="solid"/>
61
            </style:style>',
62
        "underline"=>'
63
            <style:style style:name="underline" style:family="text">
64
              <style:text-properties style:text-underline-style="solid"
65
                 style:text-underline-width="auto" style:text-underline-color="font-color"/>
66
            </style:style>',
67
        "media"=>'
68
            <style:style style:name="media" style:family="graphic" style:parent-style-name="Graphics">
69
                <style:graphic-properties style:run-through="foreground" style:wrap="parallel" style:number-wrapped-paragraphs="no-limit"
70
                   style:wrap-contour="false" style:vertical-pos="top" style:vertical-rel="baseline" style:horizontal-pos="left"
71
                   style:horizontal-rel="paragraph"/>
72
            </style:style>',
73
        "medialeft"=>'
74
            <style:style style:name="medialeft" style:family="graphic" style:parent-style-name="Graphics">
75
              <style:graphic-properties style:run-through="foreground" style:wrap="parallel" style:number-wrapped-paragraphs="no-limit"
76
                 style:wrap-contour="false" style:horizontal-pos="left" style:horizontal-rel="paragraph"/>
77
            </style:style>',
78
        "mediaright"=>'
79
            <style:style style:name="mediaright" style:family="graphic" style:parent-style-name="Graphics">
80
              <style:graphic-properties style:run-through="foreground" style:wrap="parallel" style:number-wrapped-paragraphs="no-limit"
81
                 style:wrap-contour="false" style:horizontal-pos="right" style:horizontal-rel="paragraph"/>
82
            </style:style>',
83
        "mediacenter"=>'
84
            <style:style style:name="mediacenter" style:family="graphic" style:parent-style-name="Graphics">
85
               <style:graphic-properties style:run-through="foreground" style:wrap="none" style:horizontal-pos="center"
86
                  style:horizontal-rel="paragraph"/>
87
            </style:style>',
88
        "tablealigncenter"=>'
89
            <style:style style:name="tablealigncenter" style:family="paragraph" style:parent-style-name="Table_20_Contents">
90
                <style:paragraph-properties fo:text-align="center"/>
91
            </style:style>',
92
        "tablealignright"=>'
93
            <style:style style:name="tablealignright" style:family="paragraph" style:parent-style-name="Table_20_Contents">
94
                <style:paragraph-properties fo:text-align="end"/>
95
            </style:style>',
96
        "tablealignleft"=>'
97
            <style:style style:name="tablealignleft" style:family="paragraph" style:parent-style-name="Table_20_Contents">
98
                <style:paragraph-properties fo:text-align="left"/>
99
            </style:style>',
100
        "tableheader"=>'
101
            <style:style style:name="tableheader" style:family="table-cell">
102
                <style:table-cell-properties fo:padding="0.05cm" fo:border-left="0.002cm solid #000000" fo:border-right="0.002cm solid #000000" fo:border-top="0.002cm solid #000000" fo:border-bottom="0.002cm solid #000000"/>
103
            </style:style>',
104
        "tablecell"=>'
105
            <style:style style:name="tablecell" style:family="table-cell">
106
                <style:table-cell-properties fo:padding="0.05cm" fo:border-left="0.002cm solid #000000" fo:border-right="0.002cm solid #000000" fo:border-top="0.002cm solid #000000" fo:border-bottom="0.002cm solid #000000"/>
107
            </style:style>',
108
        "legendcenter"=>'
109
            <style:style style:name="legendcenter" style:family="paragraph" style:parent-style-name="Illustration">
110
                <style:paragraph-properties fo:text-align="center"/>
111
            </style:style>',
112
    );
113
    // Regular styles. May not be present if in template mode, in which case they will be added to styles.xml
114
    var $styles = array(
115
        "Source_20_Text"=>'
116
            <style:style style:name="Source_20_Text" style:display-name="Source Text" style:family="text">
117
                <style:text-properties style:font-name="Bitstream Vera Sans Mono" style:font-name-asian="Bitstream Vera Sans Mono" style:font-name-complex="Bitstream Vera Sans Mono"/>
118
            </style:style>',
119
        "Preformatted_20_Text"=>'
120
            <style:style style:name="Preformatted_20_Text" style:display-name="Preformatted Text" style:family="paragraph" style:parent-style-name="Standard" style:class="html">
121
                <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0.2cm"/>
122
                <style:text-properties style:font-name="Bitstream Vera Sans Mono" style:font-name-asian="Bitstream Vera Sans Mono" style:font-name-complex="Bitstream Vera Sans Mono"/>
123
            </style:style>',
124
        "Source_20_Code"=>'
125
            <style:style style:name="Source_20_Code" style:display-name="Source Code" style:family="paragraph" style:parent-style-name="Preformatted_20_Text">
126
                <style:paragraph-properties fo:padding="0.05cm" style:shadow="none" fo:border="0.002cm solid #8cacbb" fo:background-color="#f7f9fa"/>
127
            </style:style>',
128
        "Source_20_File"=>'
129
            <style:style style:name="Source_20_File" style:display-name="Source File" style:family="paragraph" style:parent-style-name="Preformatted_20_Text">
130
                <style:paragraph-properties fo:padding="0.05cm" style:shadow="none" fo:border="0.002cm solid #8cacbb" fo:background-color="#f1f4f5"/>
131
            </style:style>',
132
        "Horizontal_20_Line"=>'
133
            <style:style style:name="Horizontal_20_Line" style:display-name="Horizontal Line" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="html">
134
                <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0.5cm" style:border-line-width-bottom="0.002cm 0.035cm 0.002cm" fo:padding="0cm" fo:border-left="none" fo:border-right="none" fo:border-top="none" fo:border-bottom="0.04cm double #808080" text:number-lines="false" text:line-number="0" style:join-border="false"/>
135
                <style:text-properties fo:font-size="6pt" style:font-size-asian="6pt" style:font-size-complex="6pt"/>
136
            </style:style>',
137
        "Footnote"=>'
138
            <style:style style:name="Footnote" style:family="paragraph" style:parent-style-name="Standard" style:class="extra">
139
                <style:paragraph-properties fo:margin-left="0.5cm" fo:margin-right="0cm" fo:text-indent="-0.5cm" style:auto-text-indent="false" text:number-lines="false" text:line-number="0"/>
140
                <style:text-properties fo:font-size="10pt" style:font-size-asian="10pt" style:font-size-complex="10pt"/>
141
            </style:style>',
142
        "Emphasis"=>'
143
            <style:style style:name="Emphasis" style:family="text">
144
                <style:text-properties fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"/>
145
            </style:style>',
146
        "Strong_20_Emphasis"=>'
147
            <style:style style:name="Strong_20_Emphasis" style:display-name="Strong Emphasis" style:family="text">
148
                <style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
149
            </style:style>',
150
    );
151
    // Font definitions. May not be present if in template mode, in which case they will be added to styles.xml
152
    var $fonts = array(
153
        "StarSymbol"=>'<style:font-face style:name="StarSymbol" svg:font-family="StarSymbol"/>', // for bullets
154
        "Bitstream Vera Sans Mono"=>'<style:font-face style:name="Bitstream Vera Sans Mono" svg:font-family="\'Bitstream Vera Sans Mono\'" style:font-family-generic="modern" style:font-pitch="fixed"/>', // for source code
155
    );
156
157
    /**
158
     * Return version info
159
     */
160
    function getInfo(){
161
        return confToHash(dirname(__FILE__).'/info.txt');
162
    }
163
164
    /**
165
     * Returns the format produced by this renderer.
166
     */
167
    function getFormat(){
168
        return "odt";
169
    }
170
171
    /**
172
     * Do not make multiple instances of this class
173
     */
174
    function isSingleton(){
175
        return true;
176
    }
177
178
179
    /**
180
     * Initialize the rendering
181
     */
182
    function document_start() {
183
        global $ID;
184
185
        // If older or equal to 2007-06-26, we need to disable caching
186
        $dw_version = preg_replace('/[^\d]/', '', getversion());
187
        if (version_compare($dw_version, "20070626", "<=")) {
188
            $this->info["cache"] = false;
189
        }
190
191
        // prepare the zipper
192
        $this->ZIP = new ZipLib();
193
194
        // prepare meta data
195
        $this->meta             = array(
196
                'meta:generator'            => 'DokuWiki '.getversion(),
197
                'meta:initial-creator'      => 'Generated',
198
                'meta:creation-date'        => date('Y-m-d\\TH::i:s', null), //FIXME
199
                'dc:creator'                => 'Generated',
200
                'dc:date'                   => date('Y-m-d\\TH::i:s', null),
201
                'dc:language'               => 'en-US',
202
                'meta:editing-cycles'       => '1',
203
                'meta:editing-duration'     => 'PT0S',
204
            );
205
206
        //$headers = array('Content-Type'=>'text/plain'); p_set_metadata($ID,array('format' => array('odt' => $headers) )); return ; // DEBUG
207
        // send the content type header, new method after 2007-06-26 (handles caching)
208
        $output_filename = str_replace(':','-',$ID).".odt";
209
        if (version_compare($dw_version, "20070626")) {
210
            // store the content type headers in metadata
211
            $headers = array(
212
                'Content-Type' => 'application/vnd.oasis.opendocument.text',
213
                'Content-Disposition' => 'attachment; filename="'.$output_filename.'";',
214
            );
215
            p_set_metadata($ID,array('format' => array('odt' => $headers) ));
216
        } else { // older method
217
            header('Content-Type: application/vnd.oasis.opendocument.text');
218
            header('Content-Disposition: attachment; filename="'.$output_filename.'";');
219
        }
220
    }
221
222
    /**
223
     * Prepare meta.xml
224
     */
225
    function _odtMeta(){
226
        $value  =   '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n";
227
        $value .=   '<office:document-meta ';
228
        $value .=       'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" ';
229
        $value .=       'xmlns:xlink="http://www.w3.org/1999/xlink" ';
230
        $value .=       'xmlns:dc="http://purl.org/dc/elements/1.1/" ';
231
        $value .=       'xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" ';
232
        $value .=   'office:version="1.0">';
233
        $value .=       '<office:meta>';
234
    # FIXME
235
    #    foreach($meta as $meta_key => $meta_value)
236
    #        $value .=       '<' . $meta_key . '>' . ODUtils::encode($meta_value) . '</' . $meta_key . '>';
237
        $value .=       '</office:meta>';
238
        $value .=   '</office:document-meta>';
239
        $this->ZIP->add_File($value,'meta.xml');
240
    }
241
242
    /**
243
     * Prepare manifest.xml
244
     */
245
    function _odtManifest(){
246
        $value  =   '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n";
247
        $value .=   '<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">';
248
        $value .=   '<manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.text" manifest:full-path="/"/>';
249
        $value .=   '<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="settings.xml"/>';
250
        $value .=   '<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="meta.xml"/>';
251
        $value .=   '<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="content.xml"/>';
252
        $value .=   '<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="styles.xml"/>';
253
        $value .= $this->_odtGetManifest();
254
        $value .=   '</manifest:manifest>';
255
        $this->ZIP->add_File($value,'META-INF/manifest.xml');
256
    }
257
258
    function _odtGetManifest() {
259
        $value = '';
260
        foreach($this->manifest as $path => $type){
261
            $value .= '<manifest:file-entry manifest:media-type="'.$this->_xmlEntities($type).
262
                      '" manifest:full-path="'.$this->_xmlEntities($path).'"/>';
263
        }
264
        return $value;
265
    }
266
267
    /**
268
     * Prepare settings.xml
269
     */
270
    function _odtSettings(){
271
        $value  =   '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n";
272
        $value .=   '<office:document-settings xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" office:version="1.0"><office:settings><config:config-item-set config:name="dummy-settings"><config:config-item config:name="MakeValidatorHappy" config:type="boolean">true</config:config-item></config:config-item-set></office:settings></office:document-settings>';
273
        $this->ZIP->add_File($value,'settings.xml');
274
    }
275
276
277
278
279
    /**
280
     * Closes the document
281
     */
282
    function document_end(){
283
        global $conf;
284
        //$this->doc .= $this->_odtAutoStyles(); return; // DEBUG
285
286
        $this->doc = preg_replace('#<text:p[^>]*>\s*</text:p>#', '', $this->doc);
287
288
        // Template name provided in the URL
289
        if (isset($_GET["odt-template"])) {
290
            $this->template = $_GET["odt-template"];
291
        }
292
293
        // Template provided in the configuration
294
        if (!$this->template and $this->getConf("tpl_default")) {
295
            $this->template = $this->getConf("tpl_default");
296
        }
297
298
        if ($this->template) { // template chosen
299
            if (file_exists($conf['mediadir'].'/'.$this->getConf("tpl_dir")."/".$this->template)) { //template found
300
                $this->document_end_template();
301
            } else { // template chosen but not found : warn the user and use the default template
302
                $this->doc = '<text:p text:style-name="Text_20_body"><text:span text:style-name="Strong_20_Emphasis">'
303
                             .$this->_xmlEntities( sprintf($this->getLang('tpl_not_found'),$this->template,$this->getConf("tpl_dir")) )
304
                             .'</text:span></text:p>'.$this->doc;
305
                $this->document_end_scratch();
306
            }
307
        } else {
308
            $this->document_end_scratch();
309
        }
310
        $this->doc = $this->ZIP->get_file();
311
    }
312
313
314
    /**
315
     * Closes the document when not using a template
316
     */
317
    function document_end_scratch(){
318
        $autostyles = $this->_odtAutoStyles();
319
        $userfields = $this->_odtUserFields();
320
321
        // add defaults
322
        $this->ZIP->add_File('application/vnd.oasis.opendocument.text', 'mimetype', 0);
323
324
        $this->_odtMeta();
325
        $this->_odtSettings();
326
327
        $value  =   '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n";
328
        $value .=   '<office:document-content ';
329
        $value .=       'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" ';
330
        $value .=       'xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" ';
331
        $value .=       'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" ';
332
        $value .=       'xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" ';
333
        $value .=       'xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" ';
334
        $value .=       'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" ';
335
        $value .=       'xmlns:xlink="http://www.w3.org/1999/xlink" ';
336
        $value .=       'xmlns:dc="http://purl.org/dc/elements/1.1/" ';
337
        $value .=       'xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" ';
338
        $value .=       'xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" ';
339
        $value .=       'xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" ';
340
        $value .=       'xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" ';
341
        $value .=       'xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" ';
342
        $value .=       'xmlns:math="http://www.w3.org/1998/Math/MathML" ';
343
        $value .=       'xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" ';
344
        $value .=       'xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" ';
345
        $value .=       'xmlns:dom="http://www.w3.org/2001/xml-events" ';
346
        $value .=       'xmlns:xforms="http://www.w3.org/2002/xforms" ';
347
        $value .=       'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ';
348
        $value .=       'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ';
349
        $value .=   'office:version="1.0">';
350
        $value .=       '<office:scripts/>';
351
        $value .=       '<office:font-face-decls>';
352
        $value .=           '<style:font-face style:name="Tahoma1" svg:font-family="Tahoma"/>';
353
        $value .=           '<style:font-face style:name="Lucida Sans Unicode" svg:font-family="&apos;Lucida Sans Unicode&apos;" style:font-pitch="variable"/>';
354
        $value .=           '<style:font-face style:name="Tahoma" svg:font-family="Tahoma" style:font-pitch="variable"/>';
355
        $value .=           '<style:font-face style:name="Times New Roman" svg:font-family="&apos;Times New Roman&apos;" style:font-family-generic="roman" style:font-pitch="variable"/>';
356
        $value .=       '</office:font-face-decls>';
357
        $value .=       $autostyles;
358
        $value .=       '<office:body>';
359
        $value .=           '<office:text>';
360
        $value .=               '<office:forms form:automatic-focus="false" form:apply-design-mode="false"/>';
361
        $value .=               '<text:sequence-decls>';
362
        $value .=                   '<text:sequence-decl text:display-outline-level="0" text:name="Illustration"/>';
363
        $value .=                   '<text:sequence-decl text:display-outline-level="0" text:name="Table"/>';
364
        $value .=                   '<text:sequence-decl text:display-outline-level="0" text:name="Text"/>';
365
        $value .=                   '<text:sequence-decl text:display-outline-level="0" text:name="Drawing"/>';
366
        $value .=               '</text:sequence-decls>';
367
        $value .=               $userfields;
368
        $value .=   $this->doc;
369
        $value .=           '</office:text>';
370
        $value .=       '</office:body>';
371
        $value .=   '</office:document-content>';
372
373
        $this->ZIP->add_File($value,'content.xml');
374
375
        $value = io_readFile(DOKU_PLUGIN.'odt/styles.xml');
376
        $value = str_replace('<office:automatic-styles/>', $autostyles, $value);
377
        $this->ZIP->add_File($value,'styles.xml');
378
379
        // build final manifest
380
        $this->_odtManifest();
381
    }
382
383
    /**
384
     * Closes the document using a template
385
     */
386
    function document_end_template(){
387
        global $conf, $ID; // for the temp dir
388
389
        // Temp dir
390
        if (is_dir($conf['tmpdir'])) {
391
            $temp_dir = $conf['tmpdir']; // version > 20070626
392
        } else {
393
            $temp_dir = $conf['savedir'].'/cache/tmp'; // version <= 20070626
394
        }
395
        $this->temp_dir = $temp_dir."/odt/".str_replace(':','-',$ID);
396
        if (is_dir($this->temp_dir)) { $this->io_rm_rf($this->temp_dir); }
397
        io_mkdir_p($this->temp_dir);
398
399
        // Extract template
400
        $template_path = $conf['mediadir'].'/'.$this->getConf("tpl_dir")."/".$this->template;
401
        $this->ZIP->Extract($template_path, $this->temp_dir);
402
403
        // Prepare content
404
        $autostyles = $this->_odtAutoStyles();
405
        $missingstyles = $this->_odtStyles();
406
        $missingfonts = $this->_odtFonts();
407
        $userfields = $this->_odtUserFields();
408
409
        // Insert content
410
        $old_content = io_readFile($this->temp_dir.'/content.xml');
411
        if (strpos($old_content, 'DOKUWIKI-ODT-INSERT') !== FALSE) { // Replace the mark
412
            $this->_odtReplaceInFile('/<text:p[^>]*>DOKUWIKI-ODT-INSERT<\/text:p>/', 
413
                $this->doc, $this->temp_dir.'/content.xml', true);
414
        } else { // Append to the template
415
            $this->_odtReplaceInFile('</office:text>', $this->doc.'</office:text>', $this->temp_dir.'/content.xml');
416
        }
417
418
        // Cut off unwanted content
419
        if (strpos($old_content, 'DOKUWIKI-ODT-CUT-START') !== FALSE 
420
                && strpos($old_content, 'DOKUWIKI-ODT-CUT-STOP') !== FALSE) {
421
            $this->_odtReplaceInFile('/DOKUWIKI-ODT-CUT-START.*DOKUWIKI-ODT-CUT-STOP/', 
422
                '', $this->temp_dir.'/content.xml', true);
423
        }
424
425
        // Insert userfields
426
        if (strpos($old_content, "text:user-field-decls") === FALSE) { // no existing userfields
427
            $this->_odtReplaceInFile('/<office:text([^>]*)>/U', '<office:text\1>'.$userfields, $this->temp_dir.'/content.xml', TRUE);
428
        } else {
429
            $this->_odtReplaceInFile('</text:user-field-decls>', substr($userfields,23), $this->temp_dir.'/content.xml');
430
        }
431
432
        // Insert styles & fonts
433
        $this->_odtReplaceInFile('</office:automatic-styles>', substr($autostyles, 25), $this->temp_dir.'/content.xml');
434
        $this->_odtReplaceInFile('</office:automatic-styles>', substr($autostyles, 25), $this->temp_dir.'/styles.xml');
435
        $this->_odtReplaceInFile('</office:styles>', $missingstyles.'</office:styles>', $this->temp_dir.'/styles.xml');
436
        $this->_odtReplaceInFile('</office:font-face-decls>', $missingfonts.'</office:font-face-decls>', $this->temp_dir.'/styles.xml');
437
438
        // Add manifest data
439
        $this->_odtReplaceInFile('</manifest:manifest>', $this->_odtGetManifest() . '</manifest:manifest>', $this->temp_dir . '/META-INF/manifest.xml');
440
441
        // Build the Zip
442
        $this->ZIP->Compress(null, $this->temp_dir, null);
443
        $this->io_rm_rf($this->temp_dir);
444
    }
445
446
    function _odtReplaceInFile($from, $to, $file, $regexp=FALSE) {
447
        $value = io_readFile($file);
448
        if ($regexp) {
449
            $value = preg_replace($from, $to, $value);
450
        } else {
451
            $value = str_replace($from, $to, $value);
452
        }
453
        $file_f = fopen($file, 'w');
454
        fwrite($file_f, $value);
455
        fclose($file_f);
456
    }
457
458
    /**
459
     * Recursively deletes a directory (equivalent to the "rm -rf" command)
460
     * Found in comments on http://www.php.net/rmdir
461
     */
462
    function io_rm_rf($f) {
463
        if (is_dir($f)) {
464
            foreach(glob($f.'/*') as $sf) {
465
                if (is_dir($sf) && !is_link($sf)) {
466
                    $this->io_rm_rf($sf);
467
                } else {
468
                    unlink($sf);
469
                }
470
            }
471
        } else { // avoid nasty consequenses if something wrong is given
472
            die("Error: not a directory - $f");
473
        }
474
        rmdir($f);
475
    }
476
477
    // not supported - use OpenOffice builtin tools instead!
478
    function render_TOC() { return ''; }
479
480
    function toc_additem($id, $text, $level) {}
481
482
    function _odtAutoStyles() {
483
        $value = '<office:automatic-styles>';
484
        foreach ($this->autostyles as $stylename=>$stylexml) {
485
            $value .= $stylexml;
486
        }
487
        $value .= '</office:automatic-styles>';
488
        return $value;
489
    }
490
491
    function _odtUserFields() {
492
        $value = '<text:user-field-decls>';
493
        foreach ($this->fields as $fname=>$fvalue) {
494
            $value .= '<text:user-field-decl office:value-type="string" text:name="'.$fname.'" office:string-value="'.$fvalue.'"/>';
495
        }
496
        $value .= '</text:user-field-decls>';
497
        return $value;
498
    }
499
500
    /* Add missing styles in the template */
501
    function _odtStyles() {
502
        $value = '';
503
        $existing_styles = io_readFile($this->temp_dir.'/styles.xml');
504
        foreach ($this->styles as $stylename=>$stylexml) {
505
            if (strpos($existing_styles, 'style:name="'.$stylename.'"') === FALSE) {
506
                $value .= $stylexml;
507
            }
508
        }
509
        // Loop on bullet/numerotation styles
510
        if (strpos($existing_styles, 'style:name="List_20_1"') === FALSE) {
511
            $value .= '<text:list-style style:name="List_20_1" style:display-name="List 1">';
512
            for ($i=1;$i<=10;$i++) {
513
                $value .= '<text:list-level-style-bullet text:level="'.$i.'" text:style-name="Numbering_20_Symbols" text:bullet-char="•">
514
                               <style:list-level-properties text:space-before="'.(0.4*($i-1)).'cm" text:min-label-width="0.4cm"/>
515
                               <style:text-properties style:font-name="StarSymbol"/>
516
                           </text:list-level-style-bullet>';
517
            }
518
            $value .= '</text:list-style>';
519
        }
520
        if (strpos($existing_styles, 'style:name="Numbering_20_1"') === FALSE) {
521
            $value .= '<text:list-style style:name="Numbering_20_1" style:display-name="Numbering 1">';
522
            for ($i=1;$i<=10;$i++) {
523
                $value .= '<text:list-level-style-number text:level="'.$i.'" text:style-name="Numbering_20_Symbols" style:num-suffix="." style:num-format="1">
524
                               <style:list-level-properties text:space-before="'.(0.5*($i-1)).'cm" text:min-label-width="0.5cm"/>
525
                           </text:list-level-style-number>';
526
            }
527
            $value .= '</text:list-style>';
528
        }
529
        return $value;
530
    }
531
532
    /* Add missing fonts in the template */
533
    function _odtFonts() {
534
        $value = '';
535
        $existing_styles = io_readFile($this->temp_dir.'/styles.xml');
536
        foreach ($this->fonts as $name=>$xml) {
537
            if (strpos($existing_styles, 'style:name="'.$name.'"') === FALSE) {
538
                $value .= $xml;
539
            }
540
        }
541
        return $value;
542
    }
543
544
    function cdata($text) {
545
        $this->doc .= $this->_xmlEntities($text);
546
    }
547
548
    function p_open($style='Text_20_body'){
549
        if (!$this->in_paragraph) { // opening a paragraph inside another paragraph is illegal
550
            $this->in_paragraph = true;
551
            $this->doc .= '<text:p text:style-name="'.$style.'">';
552
        }
553
    }
554
555
    function p_close(){
556
        if ($this->in_paragraph) {
557
            $this->in_paragraph = false;
558
            $this->doc .= '</text:p>';
559
        }
560
    }
561
562
    function header($text, $level, $pos){
563
        $hid = $this->_headerToLink($text,true);
564
        $this->doc .= '<text:h text:style-name="Heading_20_'.$level.'" text:outline-level="'.$level.'">';
565
        $this->doc .= '<text:bookmark-start text:name="'.$hid.'"/>';
566
        $this->doc .= $this->_xmlEntities($text);
567
        $this->doc .= '<text:bookmark-end text:name="'.$hid.'"/>';
568
        $this->doc .= '</text:h>';
569
    }
570
571
    function hr() {
572
        $this->doc .= '<text:p text:style-name="Horizontal_20_Line"/>';
573
    }
574
575
    function linebreak() {
576
        $this->doc .= '<text:line-break/>';
577
    }
578
579
    function strong_open() {
580
        $this->doc .= '<text:span text:style-name="Strong_20_Emphasis">';
581
    }
582
583
    function strong_close() {
584
        $this->doc .= '</text:span>';
585
    }
586
587
    function emphasis_open() {
588
        $this->doc .= '<text:span text:style-name="Emphasis">';
589
    }
590
591
    function emphasis_close() {
592
        $this->doc .= '</text:span>';
593
    }
594
595
    function underline_open() {
596
        $this->doc .= '<text:span text:style-name="underline">';
597
    }
598
599
    function underline_close() {
600
        $this->doc .= '</text:span>';
601
    }
602
603
    function monospace_open() {
604
        $this->doc .= '<text:span text:style-name="Source_20_Text">';
605
    }
606
607
    function monospace_close() {
608
        $this->doc .= '</text:span>';
609
    }
610
611
    function subscript_open() {
612
        $this->doc .= '<text:span text:style-name="sub">';
613
    }
614
615
    function subscript_close() {
616
        $this->doc .= '</text:span>';
617
    }
618
619
    function superscript_open() {
620
        $this->doc .= '<text:span text:style-name="sup">';
621
    }
622
623
    function superscript_close() {
624
        $this->doc .= '</text:span>';
625
    }
626
627
    function deleted_open() {
628
        $this->doc .= '<text:span text:style-name="del">';
629
    }
630
631
    function deleted_close() {
632
        $this->doc .= '</text:span>';
633
    }
634
635
    /*
636
     * Tables
637
     */
638
    function table_open($maxcols = NULL, $numrows = NULL){
639
        $this->doc .= '<table:table>';
640
        for($i=0; $i<$maxcols; $i++){
641
            $this->doc .= '<table:table-column />';
642
        }
643
    }
644
645
    function table_close(){
646
        $this->doc .= '</table:table>';
647
    }
648
649
    function tablerow_open(){
650
        $this->doc .= '<table:table-row>';
651
    }
652
653
    function tablerow_close(){
654
        $this->doc .= '</table:table-row>';
655
    }
656
657
    function tableheader_open($colspan = 1, $align = "left", $rowspan = 1){
658
        $this->doc .= '<table:table-cell office:value-type="string" table:style-name="tableheader" ';
659
        //$this->doc .= ' table:style-name="tablealign'.$align.'"';
660
        if ( $colspan > 1 ) {
661
            $this->doc .= ' table:number-columns-spanned="'.$colspan.'"';
662
        }
663
        if ( $rowspan > 1 ) {
664
            $this->doc .= ' table:number-rows-spanned="'.$rowspan.'"';
665
        }
666
        $this->doc .= '>';
667
        $this->p_open('Table_20_Heading');
668
    }
669
670
    function tableheader_close(){
671
        $this->p_close();
672
        $this->doc .= '</table:table-cell>';
673
    }
674
675
    function tablecell_open($colspan = 1, $align = "left", $rowspan = 1){
676
        $this->doc .= '<table:table-cell office:value-type="string" table:style-name="tablecell" ';
677
        if ( $colspan > 1 ) {
678
            $this->doc .= ' table:number-columns-spanned="'.$colspan.'"';
679
        }
680
        if ( $rowspan > 1 ) {
681
            $this->doc .= ' table:number-rows-spanned="'.$rowspan.'"';
682
        }
683
        $this->doc .= '>';
684
        if (!$align) $align = "left";
685
        $style = "tablealign".$align;
686
        $this->p_open($style);
687
    }
688
689
    function tablecell_close(){
690
        $this->p_close();
691
        $this->doc .= '</table:table-cell>';
692
    }
693
694
    /**
695
     * Callback for footnote start syntax
696
     *
697
     * All following content will go to the footnote instead of
698
     * the document. To achieve this the previous rendered content
699
     * is moved to $store and $doc is cleared
700
     *
701
     * @author Andreas Gohr <andi@splitbrain.org>
702
     */
703
    function footnote_open() {
704
705
        // move current content to store and record footnote
706
        $this->store = $this->doc;
707
        $this->doc   = '';
708
    }
709
710
    /**
711
     * Callback for footnote end syntax
712
     *
713
     * All rendered content is moved to the $footnotes array and the old
714
     * content is restored from $store again
715
     *
716
     * @author Andreas Gohr
717
     */
718
    function footnote_close() {
719
        // recover footnote into the stack and restore old content
720
        $footnote = $this->doc;
721
        $this->doc = $this->store;
722
        $this->store = '';
723
724
        // check to see if this footnote has been seen before
725
        $i = array_search($footnote, $this->footnotes);
726
727
        if ($i === false) {
728
            $i = count($this->footnotes);
729
            // its a new footnote, add it to the $footnotes array
730
            $this->footnotes[$i] = $footnote;
731
732
            $this->doc .= '<text:note text:id="ftn'.$i.'" text:note-class="footnote">';
733
            $this->doc .= '<text:note-citation>'.($i+1).'</text:note-citation>';
734
            $this->doc .= '<text:note-body>';
735
            $this->doc .= '<text:p text:style-name="Footnote">';
736
            $this->doc .= $footnote;
737
            $this->doc .= '</text:p>';
738
            $this->doc .= '</text:note-body>';
739
            $this->doc .= '</text:note>';
740
741
        } else {
742
            // seen this one before - just reference it FIXME: style isn't correct yet
743
            $this->doc .= '<text:note-ref text:note-class="footnote" text:ref-name="ftn'.$i.'">'.($i+1).'</text:note-ref>';
744
        }
745
    }
746
747
    function listu_open() {
748
        $this->p_close();
749
        $this->doc .= '<text:list text:style-name="List_20_1">';
750
    }
751
752
    function listu_close() {
753
        $this->doc .= '</text:list>';
754
    }
755
756
    function listo_open() {
757
        $this->p_close();
758
        $this->doc .= '<text:list text:style-name="Numbering_20_1">';
759
    }
760
761
    function listo_close() {
762
        $this->doc .= '</text:list>';
763
    }
764
765
    function listitem_open($level) {
766
        $this->in_list_item = true;
767
        $this->doc .= '<text:list-item>';
768
    }
769
770
    function listitem_close() {
771
        $this->in_list_item = false;
772
        $this->doc .= '</text:list-item>';
773
    }
774
775
    function listcontent_open() {
776
        $this->doc .= '<text:p text:style-name="Text_20_body">';
777
    }
778
779
    function listcontent_close() {
780
        $this->doc .= '</text:p>';
781
    }
782
783
    function unformatted($text) {
784
        $this->doc .= $this->_xmlEntities($text);
785
    }
786
787
    function acronym($acronym) {
788
        $this->doc .= $this->_xmlEntities($acronym);
789
    }
790
791
    function smiley($smiley) {
792
        if ( array_key_exists($smiley, $this->smileys) ) {
793
            $src = DOKU_INC."lib/images/smileys/".$this->smileys[$smiley];
794
            $this->_odtAddImage($src);
795
        } else {
796
            $this->doc .= $this->_xmlEntities($smiley);
797
        }
798
    }
799
800
    function entity($entity) {
801
        # UTF-8 entity decoding is broken in PHP <5
802
        if (version_compare(phpversion(), "5.0.0") and array_key_exists($entity, $this->entities) ) {
803
            # decoding may fail for missing Multibyte-Support in entity_decode
804
            $dec = @html_entity_decode($this->entities[$entity],ENT_NOQUOTES,'UTF-8');
805
            if($dec){
806
                $this->doc .= $this->_xmlEntities($dec);
807
            }else{
808
                $this->doc .= $this->_xmlEntities($entity);
809
            }
810
        } else {
811
            $this->doc .= $this->_xmlEntities($entity);
812
        }
813
    }
814
815
    function multiplyentity($x, $y) {
816
        $this->doc .= $x.'×'.$y;
817
    }
818
819
    function singlequoteopening() {
820
        global $lang;
821
        $this->doc .= $lang['singlequoteopening'];
822
    }
823
824
    function singlequoteclosing() {
825
        global $lang;
826
        $this->doc .= $lang['singlequoteclosing'];
827
    }
828
829
    function apostrophe() {
830
        global $lang;
831
        $this->doc .= $lang['apostrophe'];
832
    }
833
834
    function doublequoteopening() {
835
        global $lang;
836
        $this->doc .= $lang['doublequoteopening'];
837
    }
838
839
    function doublequoteclosing() {
840
        global $lang;
841
        $this->doc .= $lang['doublequoteclosing'];
842
    }
843
844
    function php($text, $wrapper='dummy') {
845
        $this->monospace_open();
846
        $this->doc .= $this->_xmlEntities($text);
847
        $this->monospace_close();
848
    }
849
    function phpblock($text) {
850
        $this->file($text);
851
    }
852
853
    function html($text, $wrapper='dummy') {
854
        $this->monospace_open();
855
        $this->doc .= $this->_xmlEntities($text);
856
        $this->monospace_close();
857
    }
858
    function htmlblock($text) {
859
        $this->file($text);
860
    }
861
862
    /**
863
     * static call back to replace spaces
864
     */
865
    function _preserveSpace($matches){
866
        $spaces = $matches[1];
867
        $len    = strlen($spaces);
868
        return '<text:s text:c="'.$len.'"/>';
869
    }
870
871
    function preformatted($text) {
872
        $this->_preformatted($text);
873
    }
874
875
    function file($text, $language=null, $filename=null) {
876
        $this->_highlight('file', $text, $language);
877
    }
878
879
    function quote_open() {
880
        if (!$this->in_paragraph) { // only start a new par if we're not already in one
881
            $this->p_open();
882
        }
883
        $this->doc .= "&gt;";
884
    }
885
886
    function quote_close() {
887
        if ($this->in_paragraph) { // only close the paragraph if we're actually in one
888
            $this->p_close();
889
        }
890
    }
891
892
    function code($text, $language=null, $filename=null) {
893
        $this->_highlight('code', $text, $language);
894
    }
895
896
    function _preformatted($text, $style="Preformatted_20_Text", $notescaped=true) {
897
        if ($notescaped) {
898
            $text = $this->_xmlEntities($text);
899
        }
900
        if (strpos($text, "\n") !== FALSE and strpos($text, "\n") == 0) {
901
            // text starts with a newline, remove it
902
            $text = substr($text,1);
903
        }
904
        $text = str_replace("\n",'<text:line-break/>',$text);
905
        $text = str_replace("\t",'<text:tab/>',$text);
906
        $text = preg_replace_callback('/(  +)/',array('renderer_plugin_odt','_preserveSpace'),$text);
907
908
        if ($this->in_list_item) { // if we're in a list item, we must close the <text:p> tag
909
            $this->doc .= '</text:p>';
910
            $this->doc .= '<text:p text:style-name="'.$style.'">';
911
            $this->doc .= $text;
912
            $this->doc .= '</text:p>';
913
            $this->doc .= '<text:p>';
914
        } else {
915
            $this->doc .= '<text:p text:style-name="'.$style.'">';
916
            $this->doc .= $text;
917
            $this->doc .= '</text:p>';
918
        }
919
    }
920
921
    function _highlight($type, $text, $language=null) {
922
        global $conf;
923
        $style_name = "Source_20_Code";
924
        if ($type == "file") $style_name = "Source_20_File";
925
926
        if (is_null($language)) {
927
            $this->_preformatted($text, $style_name);
928
            return;
929
        }
930
931
        // from inc/parserutils.php:p_xhtml_cached_geshi()
932
        require_once(DOKU_INC . 'inc/geshi.php');
933
        $geshi = new GeSHi($text, $language, DOKU_INC . 'inc/geshi');
934
        $geshi->set_encoding('utf-8');
935
        // $geshi->enable_classes(); DO NOT WANT !
936
        $geshi->set_header_type(GESHI_HEADER_PRE);
937
        $geshi->enable_keyword_links(false);
938
939
        // remove GeSHi's wrapper element (we'll replace it with our own later)
940
        // we need to use a GeSHi wrapper to avoid <BR> throughout the highlighted text
941
        $highlighted_code = trim(preg_replace('!^<pre[^>]*>|</pre>$!','',$geshi->parse_code()),"\n\r");
942
        // remove useless leading and trailing whitespace-newlines
943
        $highlighted_code = preg_replace('/^&nbsp;\n/','',$highlighted_code);
944
        $highlighted_code = preg_replace('/\n&nbsp;$/','',$highlighted_code);
945
        // replace styles
946
        $highlighted_code = str_replace("</span>", "</text:span>", $highlighted_code);
947
        $highlighted_code = preg_replace_callback('/<span style="([^"]+)">/', array('renderer_plugin_odt','_convert_css_styles'), $highlighted_code);
948
        // cleanup leftover span tags
949
        $highlighted_code = preg_replace('/<span[^>]*>/', "<text:span>", $highlighted_code);
950
        $highlighted_code = str_replace("&nbsp;", "&#xA0;", $highlighted_code);
951
        $this->_preformatted($highlighted_code, $style_name, false);
952
    }
953
954
    function _convert_css_styles($matches) {
955
        $all_css_styles = $matches[1];
956
        // parse the CSS attribute
957
        $css_styles = array();
958
        foreach(explode(";", $all_css_styles) as $css_style) {
959
            $css_style_array = explode(":", $css_style);
960
            if (!trim($css_style_array[0]) or !trim($css_style_array[1])) {
961
                continue;
962
            }
963
            $css_styles[trim($css_style_array[0])] = trim($css_style_array[1]);
964
        }
965
        // create the ODT xml style
966
        $style_name = "highlight." . $this->highlight_style_num;
967
        $this->highlight_style_num += 1;
968
        $style_content = '
969
            <style:style style:name="'.$style_name.'" style:family="text">
970
                <style:text-properties ';
971
        foreach($css_styles as $style_key=>$style_value) {
972
            // Hats off to those who thought out the OpenDocument spec: styling syntax is similar to CSS !
973
            $style_content .= 'fo:'.$style_key.'="'.$style_value.'" ';
974
        }
975
        $style_content .= '/>
976
            </style:style>';
977
        // add the style to the library
978
        $this->autostyles[$style_name] = $style_content;
979
        // now make use of the new style
980
        return '<text:span text:style-name="'.$style_name.'">';
981
    }
982
983
    function internalmedia ($src, $title=NULL, $align=NULL, $width=NULL,
984
                            $height=NULL, $cache=NULL, $linking=NULL) {
985
        global $conf;
986
        global $ID;
987
        resolve_mediaid(getNS($ID),$src, $exists);
988
        list($ext,$mime) = mimetype($src);
989
990
        if(substr($mime,0,5) == 'image'){
991
            $file = mediaFN($src);
992
            $this->_odtAddImage($file, $width, $height, $align, $title);
993
        }else{
994
            // FIXME build absolute medialink and call externallink()
995
            $this->code('FIXME internalmedia: '.$src);
996
        }
997
    }
998
999
    function externalmedia ($src, $title=NULL, $align=NULL, $width=NULL,
1000
                            $height=NULL, $cache=NULL, $linking=NULL) {
1001
        global $conf;
1002
        global $ID;
1003
        list($ext,$mime) = mimetype($src);
1004
1005
        if(substr($mime,0,5) == 'image'){
1006
            $tmp_dir = $conf['tmpdir']."/odt";
1007
            $tmp_name = $tmp_dir."/".md5($src).'.'.$ext;
1008
            $final_name = 'Pictures/'.md5($tmp_name).'.'.$ext;
1009
            if(!isset($this->manifest[$final_name])){
1010
                $client = new DokuHTTPClient;
1011
                $img = $client->get($src);
1012
                if ($img === FALSE) {
1013
                    $tmp_name = $src; // fallback to a simple link
1014
                } else {
1015
                    if (!is_dir($tmp_dir)) io_mkdir_p($tmp_dir);
1016
                    $tmp_img = fopen($tmp_name, "w") or die("Can't create temp file $tmp_img");
1017
                    fwrite($tmp_img, $img);
1018
                    fclose($tmp_img);
1019
                }
1020
            }
1021
            $this->_odtAddImage($tmp_name, $width, $height, $align, $title);
1022
            if (file_exists($tmp_name)) unlink($tmp_name);
1023
        }else{
1024
            $this->externallink($src,$title);
1025
        }
1026
    }
1027
1028
    function camelcaselink($link) {
1029
        $this->internallink($link,$link);
1030
    }
1031
1032
    function reference($id, $name = NULL) {
1033
        $this->doc .= '<text:a xlink:type="simple" xlink:href="#'.$id.'"';
1034
        if ($name) {
1035
            $this->doc .= '>'.$this->_xmlEntities($name).'</text:a>';
1036
        } else {
1037
            $this->doc .= '/>';
1038
        }
1039
    }
1040
1041
    /**
1042
     * Render an internal Wiki Link
1043
     *
1044
     * @author Andreas Gohr <andi@splitbrain.org>
1045
     */
1046
    function internallink($id, $name = NULL) {
1047
        global $conf;
1048
        global $ID;
1049
        // default name is based on $id as given
1050
        $default = $this->_simpleTitle($id);
1051
        // now first resolve and clean up the $id
1052
        resolve_pageid(getNS($ID),$id,$exists);
1053
        $name = $this->_getLinkTitle($name, $default, $isImage, $id);
1054
1055
        // build the absolute URL (keeping a hash if any)
1056
        list($id,$hash) = explode('#',$id,2);
1057
        $url = wl($id,'',true);
1058
        if($hash) $url .='#'.$hash;
1059
1060
        if ($ID == $id) {
1061
            $this->reference($hash, $name);
1062
        } else {
1063
            $this->_doLink($url,$name);
1064
        }
1065
    }
1066
1067
    /**
1068
     * Add external link
1069
     */
1070
    function externallink($url, $name = NULL) {
1071
        global $conf;
1072
1073
        $name = $this->_getLinkTitle($name, $url, $isImage);
1074
1075
        $this->_doLink($url,$name);
1076
    }
1077
1078
    /**
1079
     * Just print local links
1080
     *
1081
     * @fixme add image handling
1082
     */
1083
    function locallink($hash, $name = NULL){
1084
        $name  = $this->_getLinkTitle($name, $hash, $isImage);
1085
        $this->doc .= $name;
1086
    }
1087
1088
    /**
1089
     * InterWiki links
1090
     */
1091
    function interwikilink($match, $name = NULL, $wikiName, $wikiUri) {
1092
        $name  = $this->_getLinkTitle($name, $wikiUri, $isImage);
1093
        $url = $this-> _resolveInterWiki($wikiName,$wikiUri);
1094
        $this->_doLink($url,$name);
1095
    }
1096
1097
    /**
1098
     * Just print WindowsShare links
1099
     *
1100
     * @fixme add image handling
1101
     */
1102
    function windowssharelink($url, $name = NULL) {
1103
        $name  = $this->_getLinkTitle($name, $url, $isImage);
1104
        $this->doc .= $name;
1105
    }
1106
1107
    /**
1108
     * Just print email links
1109
     *
1110
     * @fixme add image handling
1111
     */
1112
    function emaillink($address, $name = NULL) {
1113
        $name  = $this->_getLinkTitle($name, $address, $isImage);
1114
        $this->_doLink("mailto:".$address,$name);
1115
    }
1116
1117
    /**
1118
     * Add a hyperlink, handling Images correctly
1119
     *
1120
     * @author Andreas Gohr <andi@splitbrain.org>
1121
     */
1122
    function _doLink($url,$name){
1123
        $url = $this->_xmlEntities($url);
1124
        if(is_array($name)){
1125
            // Images
1126
            if($url) $this->doc .= '<draw:a xlink:type="simple" xlink:href="'.$url.'">';
1127
1128
            if($name['type'] == 'internalmedia'){
1129
                $this->internalmedia($name['src'],
1130
                                     $name['title'],
1131
                                     $name['align'],
1132
                                     $name['width'],
1133
                                     $name['height'],
1134
                                     $name['cache'],
1135
                                     $name['linking']);
1136
            }
1137
1138
            if($url) $this->doc .= '</draw:a>';
1139
        }else{
1140
            // Text
1141
            if($url) $this->doc .= '<text:a xlink:type="simple" xlink:href="'.$url.'">';
1142
            $this->doc .= $name; // we get the name already XML encoded
1143
            if($url) $this->doc .= '</text:a>';
1144
        }
1145
    }
1146
1147
    /**
1148
     * Construct a title and handle images in titles
1149
     *
1150
     * @author Harry Fuecks <hfuecks@gmail.com>
1151
     */
1152
    function _getLinkTitle($title, $default, & $isImage, $id=null) {
1153
        global $conf;
1154
1155
        $isImage = false;
1156
        if ( is_null($title) ) {
1157
            if ($conf['useheading'] && $id) {
1158
                $heading = p_get_first_heading($id);
1159
                if ($heading) {
1160
                    return $this->_xmlEntities($heading);
1161
                }
1162
            }
1163
            return $this->_xmlEntities($default);
1164
        } else if ( is_string($title) ) {
1165
            return $this->_xmlEntities($title);
1166
        } else if ( is_array($title) ) {
1167
            $isImage = true;
1168
            return $title;
1169
        }
1170
    }
1171
1172
    /**
1173
     * Creates a linkid from a headline
1174
     *
1175
     * @param string  $title   The headline title
1176
     * @param boolean $create  Create a new unique ID?
1177
     * @author Andreas Gohr <andi@splitbrain.org>
1178
     */
1179
    function _headerToLink($title,$create=false) {
1180
        $title = str_replace(':','',cleanID($title));
1181
        $title = ltrim($title,'0123456789._-');
1182
        if(empty($title)) $title='section';
1183
1184
        if($create){
1185
            // make sure tiles are unique
1186
            $num = '';
1187
            while(in_array($title.$num,$this->headers)){
1188
                ($num) ? $num++ : $num = 1;
1189
            }
1190
            $title = $title.$num;
1191
            $this->headers[] = $title;
1192
        }
1193
1194
        return $title;
1195
    }
1196
1197
1198
    function _xmlEntities($value) {
1199
        return str_replace( array('&','"',"'",'<','>'), array('&#38;','&#34;','&#39;','&#60;','&#62;'), $value);
1200
    }
1201
1202
    function rss ($url,$params){
1203
        global $lang;
1204
        global $conf;
1205
1206
        require_once(DOKU_INC.'inc/FeedParser.php');
1207
        $feed = new FeedParser();
1208
        $feed->feed_url($url);
1209
1210
        //disable warning while fetching
1211
        if (!defined('DOKU_E_LEVEL')) { $elvl = error_reporting(E_ERROR); }
1212
        $rc = $feed->init();
1213
        if (!defined('DOKU_E_LEVEL')) { error_reporting($elvl); }
1214
1215
        //decide on start and end
1216
        if($params['reverse']){
1217
            $mod = -1;
1218
            $start = $feed->get_item_quantity()-1;
1219
            $end   = $start - ($params['max']);
1220
            $end   = ($end < -1) ? -1 : $end;
1221
        }else{
1222
            $mod   = 1;
1223
            $start = 0;
1224
            $end   = $feed->get_item_quantity();
1225
            $end   = ($end > $params['max']) ? $params['max'] : $end;;
1226
        }
1227
1228
        $this->listu_open();
1229
        if($rc){
1230
            for ($x = $start; $x != $end; $x += $mod) {
1231
                $item = $feed->get_item($x);
1232
                $this->listitem_open(0);
1233
                $this->listcontent_open();
1234
                $this->externallink($item->get_permalink(),
1235
                                    $item->get_title());
1236
                if($params['author']){
1237
                    $author = $item->get_author(0);
1238
                    if($author){
1239
                        $name = $author->get_name();
1240
                        if(!$name) $name = $author->get_email();
1241
                        if($name) $this->cdata(' '.$lang['by'].' '.$name);
1242
                    }
1243
                }
1244
                if($params['date']){
1245
                    $this->cdata(' ('.$item->get_date($conf['dformat']).')');
1246
                }
1247
                if($params['details']){
1248
                    $this->cdata(strip_tags($item->get_description()));
1249
                }
1250
                $this->listcontent_close();
1251
                $this->listitem_close();
1252
            }
1253
        }else{
1254
            $this->listitem_open(0);
1255
            $this->listcontent_open();
1256
            $this->emphasis_open();
1257
            $this->cdata($lang['rssfailed']);
1258
            $this->emphasis_close();
1259
            $this->externallink($url);
1260
            $this->listcontent_close();
1261
            $this->listitem_close();
1262
        }
1263
        $this->listu_close();
1264
    }
1265
1266
1267
    function _odtAddImage($src, $width = NULL, $height = NULL, $align = NULL, $title = NULL, $style = NULL){
1268
        if (file_exists($src)) {
1269
            list($ext,$mime) = mimetype($src);
1270
            $name = 'Pictures/'.md5($src).'.'.$ext;
1271
            if(!$this->manifest[$name]){
1272
                $this->manifest[$name] = $mime;
1273
                $this->ZIP->add_File(io_readfile($src,false),$name,0);
1274
            }
1275
        } else {
1276
            $name = $src;
1277
        }
1278
        // make sure width and height are available
1279
        if (!$width || !$height) {
1280
            list($width, $height) = $this->_odtGetImageSize($src, $width, $height);
1281
        }
1282
1283
        if($align){
1284
            $anchor = 'paragraph';
1285
        }else{
1286
            $anchor = 'as-char';
1287
        }
1288
1289
        if (!$style or !array_key_exists($style, $this->autostyles)) {
1290
            $style = 'media'.$align;
1291
        }
1292
1293
        if ($title) {
1294
            $this->doc .= '<draw:frame draw:style-name="'.$style.'" draw:name="'.$this->_xmlEntities($title).' Legend"
1295
                            text:anchor-type="'.$anchor.'" draw:z-index="0" svg:width="'.$width.'">';
1296
            $this->doc .= '<draw:text-box>';
1297
            $this->doc .= '<text:p text:style-name="legendcenter">';
1298
        }
1299
        $this->doc .= '<draw:frame draw:style-name="'.$style.'" draw:name="'.$this->_xmlEntities($title).'"
1300
                        text:anchor-type="'.$anchor.'" draw:z-index="0"
1301
                        svg:width="'.$width.'" svg:height="'.$height.'" >';
1302
        $this->doc .= '<draw:image xlink:href="'.$this->_xmlEntities($name).'"
1303
                        xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>';
1304
        $this->doc .= '</draw:frame>';
1305
        if ($title) {
1306
            $this->doc .= $this->_xmlEntities($title).'</text:p></draw:text-box></draw:frame>';
1307
        }
1308
    }
1309
1310
    function _odtGetImageSize($src, $width = NULL, $height = NULL){
1311
        if (file_exists($src)) {
1312
            $info  = getimagesize($src);
1313
            if(!$width){
1314
                $width  = $info[0];
1315
                $height = $info[1];
1316
            }else{
1317
                $height = round(($width * $info[1]) / $info[0]);
1318
            }
1319
        }
1320
1321
        // convert from pixel to centimeters
1322
        if ($width) $width = (($width/96.0)*2.54);
1323
        if ($height) $height = (($height/96.0)*2.54);
1324
1325
        if ($width && $height) {
1326
            // Don't be wider than the page
1327
            if ($width >= 17){ // FIXME : this assumes A4 page format with 2cm margins
1328
                $width = $width.'cm"  style:rel-width="100%';
1329
                $height = $height.'cm"  style:rel-height="scale';
1330
            } else {
1331
                $width = $width.'cm';
1332
                $height = $height.'cm';
1333
            }
1334
        } else {
1335
            // external image and unable to download, fallback
1336
            if ($width) {
1337
                $width = $width."cm";
1338
            } else {
1339
                $width = '" svg:rel-width="100%';
1340
            }
1341
            if ($height) {
1342
                $height = $height."cm";
1343
            } else {
1344
                $height = '" svg:rel-height="100%';
1345
            }
1346
        }
1347
        return array($width, $height);
1348
    }
1349
1350
}
1351
1352
// vim: set et ts=4 sw=4 fileencoding=utf-8 :