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