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