1<?php
2
3use dokuwiki\File\PageResolver;
4
5/**
6 * Simple template replacement action for the bureaucracy plugin
7 *
8 * @author Michael Klier <chi@chimeric.de>
9 */
10
11class helper_plugin_bureaucracy_actiontemplate extends helper_plugin_bureaucracy_action {
12
13    var $targetpages;
14    var $pagename;
15
16    /**
17     * Performs template action
18     *
19     * @param helper_plugin_bureaucracy_field[] $fields  array with form fields
20     * @param string $thanks  thanks message
21     * @param array  $argv    array with entries: template, pagename, separator
22     * @return array|mixed
23     *
24     * @throws Exception
25     */
26    public function run($fields, $thanks, $argv) {
27        global $conf;
28
29        [$tpl, $this->pagename] = $argv;
30        $sep = $argv[2] ?? $conf['sepchar'];
31
32        $this->patterns = array();
33        $this->values   = array();
34        $this->targetpages = array();
35
36        $this->prepareNamespacetemplateReplacements();
37        $this->prepareDateTimereplacements();
38        $this->prepareLanguagePlaceholder();
39        $this->prepareNoincludeReplacement();
40        $this->prepareFieldReplacements($fields);
41
42        $evdata = array(
43            'patterns' => &$this->patterns,
44            'values' => &$this->values,
45            'fields' => $fields,
46            'action' => $this
47        );
48
49        $event = new Doku_Event('PLUGIN_BUREAUCRACY_PAGENAME', $evdata);
50        if ($event->advise_before()) {
51            $this->buildTargetPagename($fields, $sep);
52        }
53        $event->advise_after();
54
55        //target&template(s) from addpage fields
56        $this->getAdditionalTargetpages($fields);
57        //target&template(s) from action field
58        $tpl = $this->getActionTargetpages($tpl);
59
60        if(empty($this->targetpages)) {
61            throw new Exception(sprintf($this->getLang('e_template'), $tpl));
62        }
63
64        $this->checkTargetPageNames();
65
66        $this->processUploads($fields);
67        $this->replaceAndSavePages($fields);
68
69        $ret = $this->buildThankYouPage($thanks);
70
71        return $ret;
72    }
73
74    /**
75     * Prepare and resolve target page
76     *
77     * @param helper_plugin_bureaucracy_field[]  $fields  List of field objects
78     * @param string                             $sep     Separator between fields for page id
79     * @throws Exception missing pagename
80     */
81    protected function buildTargetPagename($fields, $sep) {
82        global $ID;
83
84        foreach ($fields as $field) {
85            $pname = $field->getParam('pagename');
86            if (!is_null($pname)) {
87                if (is_array($pname)) $pname = implode($sep, $pname);
88                $this->pagename .= $sep . $pname;
89            }
90        }
91
92        $resolver = new PageResolver(getNS($ID));
93        $this->pagename = $resolver->resolveId($this->replace($this->pagename));
94
95        if ($this->pagename === '') {
96            throw new Exception($this->getLang('e_pagename'));
97        }
98    }
99
100    /**
101     * Handle templates from addpage field
102     *
103     * @param helper_plugin_bureaucracy_field[]  $fields  List of field objects
104     * @return array
105     */
106    function getAdditionalTargetpages($fields) {
107        global $ID;
108        $ns = getNS($ID);
109
110        foreach ($fields as $field) {
111            if (!is_null($field->getParam('page_tpl')) && !is_null($field->getParam('page_tgt')) ) {
112                $resolver = new PageResolver($ns);
113
114                //template
115                $templatepage = $this->replace($field->getParam('page_tpl'));
116                $templatepage = $resolver->resolveId($templatepage);
117
118                //target
119                $relativetargetpage = $resolver->resolveId($field->getParam('page_tgt'));
120                $targetpage = "$this->pagename:$relativetargetpage";
121
122                $auth = $this->aclcheck($templatepage); // runas
123                if ($auth >= AUTH_READ ) {
124                    $this->addParsedTargetpage($targetpage, $templatepage);
125                }
126            }
127        }
128    }
129
130    /**
131     * Returns raw pagetemplate contents for the ID's namespace
132     *
133     * @param string $id the id of the page to be created
134     * @return string raw pagetemplate content
135     */
136    protected function rawPageTemplate($id) {
137        global $conf;
138
139        $path = dirname(wikiFN($id));
140        if(file_exists($path.'/_template.txt')) {
141            $tplfile = $path.'/_template.txt';
142        } else {
143            // search upper namespaces for templates
144            $len = strlen(rtrim($conf['datadir'], '/'));
145            while(strlen($path) >= $len) {
146                if(file_exists($path.'/__template.txt')) {
147                    $tplfile = $path.'/__template.txt';
148                    break;
149                }
150                $path = substr($path, 0, strrpos($path, '/'));
151            }
152        }
153
154        $tpl = io_readFile($tplfile);
155        return $tpl;
156    }
157
158    /**
159     * Load template(s) for targetpage as given via action field
160     *
161     * @param string $tpl    template name as given in form
162     * @return string parsed templatename
163     */
164    protected function getActionTargetpages($tpl) {
165        global $USERINFO;
166        global $conf;
167        global $ID;
168        $runas = $this->getConf('runas');
169
170        if ($tpl == '_') {
171            // use namespace template
172            if (!isset($this->targetpages[$this->pagename])) {
173                $raw = $this->rawPageTemplate($this->pagename);
174                $this->noreplace_save($raw);
175                $this->targetpages[$this->pagename] = pageTemplate(array($this->pagename));
176            }
177        } elseif ($tpl !== '!') {
178            $tpl = $this->replace($tpl);
179
180            // resolve templates, but keep references to whole namespaces intact (ending in a colon)
181            $resolver = new PageResolver(getNS($ID));
182            if(substr($tpl, -1) == ':') {
183                $tpl = $tpl.'xxx'; // append a fake page name
184                $tpl = $resolver->resolveId($tpl);
185                $tpl = substr($tpl, 0, -3); // cut off fake page name again
186            } else {
187                $tpl = $resolver->resolveId($tpl);
188            }
189
190            $backup = array();
191            if ($runas) {
192                // Hack user credentials.
193                $backup = array($_SERVER['REMOTE_USER'], $USERINFO['grps']);
194                $_SERVER['REMOTE_USER'] = $runas;
195                $USERINFO['grps'] = array();
196            }
197
198            $template_pages = array();
199            //search checks acl (as runas)
200            $opts = array(
201                'depth' => 0,
202                'listfiles' => true,
203                'showhidden' => true
204            );
205            search($template_pages, $conf['datadir'], 'search_universal', $opts, str_replace(':', '/', getNS($tpl)));
206
207            foreach ($template_pages as $template_page) {
208                $templatepageid = cleanID($template_page['id']);
209                // try to replace $tpl path with $this->pagename path in the founded $templatepageid
210                // - a single-page template will only match on itself and will be replaced,
211                //   other newtargets are pages in same namespace, so aren't changed
212                // - a namespace as template will match at the namespaces-part of the path of pages in this namespace
213                //   so these newtargets are changed
214                // if there exist a single-page and a namespace with name $tpl, both are selected
215                $newTargetpageid = preg_replace('/^' . preg_quote_cb(cleanID($tpl)) . '($|:)/', $this->pagename . '$1', $templatepageid);
216
217                if ($newTargetpageid === $templatepageid) {
218                    // only a single-page template or page in the namespace template
219                    // which matches the $tpl path are changed
220                    continue;
221                }
222
223                if (!isset($this->targetpages[$newTargetpageid])) {
224                    $this->addParsedTargetpage($newTargetpageid, $templatepageid);
225                }
226            }
227
228            if ($runas) {
229                /* Restore user credentials. */
230                list($_SERVER['REMOTE_USER'], $USERINFO['grps']) = $backup;
231            }
232        }
233        return $tpl;
234    }
235
236    /**
237     * Checks for existance and access of target pages
238     *
239     * @return mixed
240     * @throws Exception
241     */
242    protected function checkTargetPageNames() {
243        foreach (array_keys($this->targetpages) as $pname) {
244            // prevent overriding already existing pages
245            if (page_exists($pname)) {
246                throw new Exception(sprintf($this->getLang('e_pageexists'), html_wikilink($pname)));
247            }
248
249            $auth = $this->aclcheck($pname);
250            if ($auth < AUTH_CREATE) {
251                throw new Exception($this->getLang('e_denied'));
252            }
253        }
254    }
255
256    /**
257     * Perform replacements on the collected templates, and save the pages.
258     *
259     * Note: wrt runas, for changelog are used:
260     *  - $INFO['userinfo']['name']
261     *  - $INPUT->server->str('REMOTE_USER')
262     */
263    protected function replaceAndSavePages($fields) {
264        global $ID;
265        foreach ($this->targetpages as $pageName => $template) {
266            // set NSBASE var to make certain dataplugin constructs easier
267            $this->patterns['__nsbase__'] = '/@NSBASE@/';
268            $this->values['__nsbase__'] = noNS(getNS($pageName));
269
270            $evdata = array(
271                'patterns' => &$this->patterns,
272                'values' => &$this->values,
273                'id' => $pageName,
274                'template' => $template,
275                'form' => $ID,
276                'fields' => $fields
277            );
278
279            $event = new Doku_Event('PLUGIN_BUREAUCRACY_TEMPLATE_SAVE', $evdata);
280            if($event->advise_before()) {
281                // save page
282                saveWikiText(
283                    $evdata['id'],
284                    cleanText($this->replace($evdata['template'], false)),
285                    sprintf($this->getLang('summary'), $ID)
286                );
287            }
288            $event->advise_after();
289        }
290    }
291
292    /**
293     * (Callback) Sorts first by namespace depth, next by page ids
294     *
295     * @param string $a
296     * @param string $b
297     * @return int positive if $b is in deeper namespace than $a, negative higher.
298     *             further sorted by pageids
299     *
300     *  return an integer less than, equal to, or
301     * greater than zero if the first argument is considered to be
302     * respectively less than, equal to, or greater than the second.
303     */
304    public function _sorttargetpages($a, $b) {
305        $ns_diff = substr_count($a, ':') - substr_count($b, ':');
306        return ($ns_diff === 0) ? strcmp($a, $b) : ($ns_diff > 0 ? -1 : 1);
307    }
308
309    /**
310     * (Callback) Build content of item
311     *
312     * @param array $item
313     * @return string
314     */
315    public function html_list_index($item){
316        $ret = '';
317        if($item['type']=='f'){
318            $ret .= html_wikilink(':'.$item['id']);
319        } else {
320            $ret .= '<strong>' . trim(substr($item['id'], strrpos($item['id'], ':', -2)), ':') . '</strong>';
321        }
322        return $ret;
323    }
324
325    /**
326     * Build thanks message, trigger indexing and rendering of new pages.
327     *
328     * @param string $thanks
329     * @return string html of thanks message or when redirect the first page id of created pages
330     */
331    protected function buildThankYouPage($thanks) {
332        global $ID;
333        $backupID = $ID;
334
335        $html = "<p>$thanks</p>";
336
337        // Build result tree
338        $pages = array_keys($this->targetpages);
339        usort($pages, array($this, '_sorttargetpages'));
340
341        $data = array();
342        $last_folder = array();
343        foreach ($pages as $ID) {
344            $lvl = substr_count($ID, ':');
345            for ($n = 0; $n < $lvl; ++$n) {
346                if (!isset($last_folder[$n]) || strpos($ID, $last_folder[$n]['id']) !== 0) {
347                    $last_folder[$n] = array(
348                        'id' => substr($ID, 0, strpos($ID, ':', ($n > 0 ? strlen($last_folder[$n - 1]['id']) : 0) + 1) + 1),
349                        'level' => $n + 1,
350                        'open' => 1,
351                        'type' => null,
352                    );
353                    $data[] = $last_folder[$n];
354                }
355            }
356            $data[] = array('id' => $ID, 'level' => 1 + substr_count($ID, ':'), 'type' => 'f');
357        }
358        $index = new dokuwiki\Ui\Index();
359        $html .= html_buildlist($data, 'idx', array($this, 'html_list_index'), array($index, 'tagListItem'));
360
361        // Add indexer bugs for every just-created page
362        $html .= '<div class="no">';
363        ob_start();
364        foreach ($pages as $ID) {
365            // indexerWebBug uses ID and INFO[exists], but the bureaucracy form
366            // page always exists, as does the just-saved page, so INFO[exists]
367            // is correct in any case
368            tpl_indexerWebBug();
369
370            // the iframe will trigger real rendering of the pages to make sure
371            // any used plugins are initialized (eg. the do plugin)
372            echo '<iframe src="' . wl($ID, array('do' => 'export_html')) . '" width="1" height="1" style="visibility:hidden"></iframe>';
373        }
374        $html .= ob_get_contents();
375        ob_end_clean();
376        $html .= '</div>';
377
378        $ID = $backupID;
379        return $html;
380    }
381
382    /**
383     * move the uploaded files to <pagename>:FILENAME
384     *
385     *
386     * @param helper_plugin_bureaucracy_field[] $fields
387     * @throws Exception
388     */
389    protected function processUploads($fields) {
390        foreach($fields as $field) {
391
392            if($field->getFieldType() !== 'file') continue;
393
394            $label = $field->getParam('label');
395            $file  = $field->getParam('file');
396            $ns    = $field->getParam('namespace');
397
398            //skip empty files
399            if(!$file['size']) {
400                $this->values[$label] = '';
401                continue;
402            }
403
404            $id = $ns.':'.$file['name'];
405            resolve_mediaid($this->pagename, $id, $ignored); // resolve relatives
406
407            $auth = $this->aclcheck($id); // runas
408            $move = 'copy_uploaded_file';
409            //prevent from is_uploaded_file() check
410            if(defined('DOKU_UNITTEST')) {
411                $move = 'copy';
412            }
413            $res = media_save(
414                array('name' => $file['tmp_name']),
415                $id,
416                false,
417                $auth,
418                $move);
419
420            if(is_array($res)) throw new Exception($res[0]);
421
422            $this->values[$label] = $res;
423
424        }
425    }
426
427    /**
428     * Load page data and do default pattern replacements like namespace templates do
429     * and add it to list of targetpages
430     *
431     * Note: for runas the values of the real user are used for the placeholders
432     *       @NAME@ => $USERINFO['name']
433     *       @MAIL@ => $USERINFO['mail']
434     *       and the replaced value:
435     *       @USER@ => $INPUT->server->str('REMOTE_USER')
436     *
437     * @param string $targetpageid   pageid of destination
438     * @param string $templatepageid pageid of template for this targetpage
439     */
440    protected function addParsedTargetpage($targetpageid, $templatepageid) {
441        $tpl = rawWiki($templatepageid);
442        $this->noreplace_save($tpl);
443
444        $data = array(
445            'id' => $targetpageid,
446            'tpl' => $tpl,
447            'doreplace' => true,
448        );
449        parsePageTemplate($data);
450
451        //collect and apply some other replacements
452        $patterns = array();
453        $values = array();
454        $keys = array('__lang__', '__trans__', '__year__', '__month__', '__day__', '__time__');
455        foreach($keys as $key) {
456            $patterns[$key] = $this->patterns[$key];
457            $values[$key] = $this->values[$key];
458        }
459
460        $this->targetpages[$targetpageid] = preg_replace($patterns, $values, $data['tpl']);
461    }
462
463}
464// vim:ts=4:sw=4:et:enc=utf-8:
465