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