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