1<?php
2/**
3 * DokuWiki Plugin struct (Helper Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Szymon Olewniczak <it@rid.pl>
7 */
8
9// must be run within Dokuwiki
10use dokuwiki\Parsing\Parser;
11use dokuwiki\plugin\struct\meta\AccessTable;
12use dokuwiki\plugin\struct\meta\Schema;
13use dokuwiki\plugin\struct\meta\Search;
14use dokuwiki\plugin\struct\meta\StructException;
15use dokuwiki\plugin\struct\meta\Value;
16use dokuwiki\plugin\struct\types\Wiki;
17use splitbrain\PHPArchive\FileInfo;
18use splitbrain\PHPArchive\Zip;
19
20if(!defined('DOKU_INC')) die();
21
22class helper_plugin_structodt extends DokuWiki_Plugin {
23    /**
24     * Generate temporary file name with full path in temporary directory
25     *
26     * @param string $ext
27     * @return string
28     */
29    public function tmpFileName($ext='') {
30        global $conf;
31        $name = $conf['tmpdir'] . '/structodt/' . uniqid();
32        if ($ext) {
33            $name .= ".$ext";
34        }
35        return $name;
36    }
37
38    /**
39     * Render ODT file from template
40     *
41     * @param $template
42     * @param $schemas
43     * @param $pid
44     *
45     * @return string|bool
46     * @throws \splitbrain\PHPArchive\ArchiveIOException
47     * @throws \splitbrain\PHPArchive\FileInfoException
48     */
49    public function renderODT($template, $row) {
50        $template_file = mediaFN($template);
51        $tmp_dir = $this->tmpFileName() . '/';
52        if (!mkdir($tmp_dir, 0777, true)) {
53            msg("could not create tmp dir - bad permissions?", -1);
54            return false;
55        }
56
57        $template_zip = new Zip();
58        $template_zip->open($template_file);
59        $template_zip->extract($tmp_dir);
60
61        //do replacements
62        $files = array('content.xml', 'styles.xml');
63        foreach ($files as $file) {
64            $content_file = $tmp_dir . $file;
65            $content = file_get_contents($content_file);
66            if ($content === false) {
67                msg("Cannot open: $content_file", -1);
68                $this->rmdir_recursive($tmp_dir);
69                return false;
70            }
71
72            $content = $this->replace($content, $row);
73            file_put_contents($content_file, $content);
74        }
75
76
77        $tmp_file = $this->tmpFileName('odt');
78
79        $tmp_zip = new Zip();
80        $tmp_zip->create($tmp_file);
81        foreach($this->readdir_recursive($tmp_dir) as $file) {
82            $fileInfo = FileInfo::fromPath($file);
83            $fileInfo->strip(substr($tmp_dir, 1));
84            $tmp_zip->addFile($file, $fileInfo);
85        }
86        $tmp_zip->close();
87
88        //remove temp dir
89        $this->rmdir_recursive($tmp_dir);
90
91        return $tmp_file;
92    }
93
94    /**
95     * Render PDF file from template
96     *
97     * @param $template
98     * @param $schemas
99     * @param $pid
100     *
101     * @return string|bool
102     * @throws \splitbrain\PHPArchive\ArchiveIOException
103     * @throws \splitbrain\PHPArchive\FileInfoException
104     */
105    public function renderPDF($template, $row) {
106        $tmp_file = $this->renderODT($template, $row);
107        if (!$tmp_file) return false;
108
109        $wd = dirname($tmp_file);
110        $bn = basename($tmp_file);
111        $cmd = "cd $wd && HOME=$wd unoconv -f pdf $bn 2>&1";
112        exec($cmd, $output, $return_var);
113        unlink($tmp_file);
114        if ($return_var != 0) {
115            msg("PDF conversion error($return_var): " . implode('<br>', $output), -1);
116            return false;
117        }
118        //change extension to pdf
119        $tmp_file = substr($tmp_file, 0, -3) . 'pdf';
120
121        return $tmp_file;
122    }
123
124    /**
125     * Send ODT file using range request
126     *
127     * @param $tmp_file string path of sending file
128     * @param $filename string name of sending file
129     * $param $ext odt or pdf
130     */
131    public function sendFile($tmp_file, $filename, $ext='odt') {
132        $mime = "application/$ext";
133        header("Content-Type: $mime");
134        header("Content-Disposition: attachment; filename=\"$filename.$ext\";");
135
136        http_sendfile($tmp_file);
137
138        $fp = @fopen($tmp_file, "rb");
139        if($fp) {
140            //we have to remove file before exit
141            define('SIMPLE_TEST', true);
142            http_rangeRequest($fp, filesize($tmp_file), $mime);
143        } else {
144            header("HTTP/1.0 500 Internal Server Error");
145            print "Could not read file - bad permissions?";
146        }
147    }
148
149    /**
150     * Read directory recursively
151     *
152     * @param string $path
153     * @return array of file full paths
154     */
155    public function readdir_recursive($path) {
156        $directory = new \RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
157        $iterator = new \RecursiveIteratorIterator($directory);
158        $files = array();
159        foreach ($iterator as $info) {
160            if ($info->isFile()) {
161                $files[] = $info->getPathname();
162            }
163        }
164
165        return $files;
166    }
167
168    /**
169     * Remove director recursively
170     *
171     * @param $path
172     */
173    public function rmdir_recursive($path) {
174        $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
175        $iterator = new RecursiveIteratorIterator($directory,
176            RecursiveIteratorIterator::CHILD_FIRST);
177        foreach($iterator as $file) {
178            if ($file->isDir()){
179                rmdir($file->getRealPath());
180            } else {
181                unlink($file->getRealPath());
182            }
183        }
184        rmdir($path);
185    }
186
187    /**
188     * @param $schemas
189     * @param Schema $first_schema
190     * @return Search
191     */
192    public function getSearch($schemas, &$first_schema) {
193        $search = new Search();
194        if (!empty($schemas)) foreach ($schemas as $schema) {
195            $search->addSchema($schema[0], $schema[1]);
196        }
197        $search->addColumn('*');
198        $first_schema = $search->getSchemas()[0];
199
200        if ($first_schema->isLookup()) {
201            $search->addColumn('%rowid%');
202        } else {
203            $search->addColumn('%pageid%');
204            $search->addColumn('%title%');
205            $search->addColumn('%lastupdate%');
206            $search->addColumn('%lasteditor%');
207        }
208
209        return $search;
210    }
211
212    /**
213     * Get rows data, optionally filtered by pid
214     *
215     * @param string|array $schemas
216     * @param Schema $first_schema
217     * @return Value[][]
218     */
219    public function getRows($schemas, &$first_schema, $filters=array())
220    {
221        $search = $this->getSearch($schemas, $first_schema);
222        foreach ($filters as $filter) {
223            $colname = $filter[0];
224            $value = $filter[2];
225            $comp = $filter[1];
226            $op = $filter[3];
227            $search->addFilter($colname, $value, $comp, $op);
228        }
229        $result = $search->execute();
230        $pids = $search->getPids();
231        return array_combine($pids, $result);
232    }
233
234    /**
235     * Get single row by pid
236     *
237     * @param $schemas
238     * @param $pid
239     * @return Value[]|null
240     */
241    public function getRow($table, $pid, $rev, $rid) {
242        try {
243            if (AccessTable::isTypePage($pid, $rev)) {
244                $schemadata = AccessTable::getPageAccess($table, $pid);
245            } elseif (AccessTable::isTypeSerial($pid, $rev)) {
246                $schemadata = AccessTable::getSerialAccess($table, $pid, $rid);
247            } else {
248                $schemadata = AccessTable::getGlobalAccess($table, $rid);
249            }
250            return $schemadata->getData();
251        } catch (StructException $ignore) {
252            return null;
253        }
254    }
255
256    /**
257     * Perform $content replacements basing on $row Values
258     *
259     * @param string $content
260     * @param Value[] $row
261     * @return string
262     */
263    public function replace($content, $row) {
264        /** @var Value $value */
265        foreach ($row as $value) {
266            $label = $value->getColumn()->getLabel();
267            $pattern = '/@@' . preg_quote($label) . '(?:\[(\d+)\])?@@/';
268            $content = preg_replace_callback($pattern, function($matches) use ($value) {
269                $dvalue = $value->getDisplayValue();
270                if (isset($matches[1])) {
271                    $index = (int)$matches[1];
272                    if (!is_array($dvalue)) {
273                        $dvalue = array_map('trim', explode('|', $dvalue));
274                    }
275                    if (isset($dvalue[$index])) {
276                        return $dvalue[$index];
277                    }
278                    return 'Array: index out of bound';
279                } elseif (is_array($dvalue)) {
280                    return implode(',', $dvalue);
281                } elseif ($value->getColumn()->getType() instanceof Wiki) {
282                    //import parser classes and mode definitions
283                    require_once DOKU_INC . 'inc/parser/parser.php';
284
285                    // Create the parser and handler
286                    $Parser = new Parser(new Doku_Handler());
287
288                    // add default modes
289                    $std_modes = array('linebreak', 'eol');
290
291                    foreach($std_modes as $m){
292                        $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m);
293                        $obj   = new $class();
294                        $modes[] = array(
295                            'sort' => $obj->getSort(),
296                            'mode' => $m,
297                            'obj'  => $obj
298                        );
299                    }
300
301                    //add modes to parser
302                    foreach($modes as $mode){
303                        $Parser->addMode($mode['mode'],$mode['obj']);
304                    }
305                    // Do the parsing
306                    $instructions = $Parser->parse($dvalue);
307                    $Renderer = p_get_renderer('structodt');
308
309                    // Loop through the instructions
310                    foreach ($instructions as $instruction) {
311                        // Execute the callback against the Renderer
312                        if(method_exists($Renderer, $instruction[0])){
313                            call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array());
314                        }
315                    }
316
317                    return $Renderer->doc;
318                }
319                return $dvalue;
320            }, $content);
321        }
322
323        return $content;
324    }
325
326    /**
327     * @param $row
328     * @param string $template
329     * @return string
330     */
331    public function rowTemplate($row, $template) {
332        global $ID;
333
334        //do media file substitutions
335        $media = preg_replace_callback('/\$(.*?)\$/', function ($matches) use ($row) {
336            $possibleValueTypes = array('getValue', 'getCompareValue', 'getDisplayValue', 'getRawValue');
337            list($label, $valueType) = explode('.', $matches[1], 2);
338            if (!$valueType || !in_array($valueType, $possibleValueTypes)) {
339                $valueType = 'getDisplayValue';
340            }
341            foreach ($row as $value) {
342                $column = $value->getColumn();
343                if ($column->getLabel() == $label) {
344                    return call_user_func(array($value, $valueType));
345                }
346            }
347            return '';
348        }, $template);
349
350        resolve_mediaid(getNS($ID), $media, $exists);
351        if (!$exists) {
352            msg("<strong>structodt</strong>: template file($media) doesn't exist", -1);
353        }
354        return $media;
355    }
356}