xref: /plugin/addnewpage/syntax.php (revision d2f65217798b544337d1cb03b53dd4153f522c55)
1<?php
2/**
3 * Add-New-Page Plugin: a simple form for adding new pages.
4 *
5 * @license  GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author   iDO <ido@idotech.info>
7 * @author   Sam Wilson <sam@samwilson.id.au>
8 *
9 * @noinspection PhpUnused
10 * @noinspection PhpMissingParamTypeInspection, PhpMissingReturnTypeInspection
11 */
12
13use dokuwiki\File\PageResolver;
14
15// must be run within Dokuwiki
16if(!defined('DOKU_INC')) die();
17
18class syntax_plugin_addnewpage extends DokuWiki_Syntax_Plugin {
19
20    /** @var array the parsed options */
21    protected $options;
22
23    /**
24     * Syntax Type
25     */
26    public function getType() {
27        return 'substition';
28    }
29
30    /**
31     * Paragraph Type
32     */
33    public function getPType() {
34        return 'block';
35    }
36
37    /**
38     * @return int
39     */
40    public function getSort() {
41        return 199;
42    }
43
44    /**
45     * @param string $mode
46     */
47    public function connectTo($mode) {
48        $this->Lexer->addSpecialPattern('\{\{NEWPAGE[^\}]*\}\}', $mode, 'plugin_addnewpage');
49    }
50
51    /**
52     * Handler to prepare matched data for the rendering process.
53     *
54     * Handled syntax options:
55     * - {{NEWPAGE}}
56     * - {{NEWPAGE>your:namespace}}
57     * - {{NEWPAGE>your:namespace:@INPUT@:start}}
58     * - {{NEWPAGE>your:namespace:[date formats]}} {@see strftime()}
59     * - {{NEWPAGE?config_overrides}}
60     * - {{NEWPAGE#newtpl1,newtpl2}}
61     * - {{NEWPAGE#newtpl1|Title1,newtpl2|Title1}}
62     * - {{NEWPAGE>your:namespace#newtpl1|Title1,newtpl2|Title1}}
63     * - {{NEWPAGE>your:namespace#newtpl1|Title1,newtpl2|Title1#@HI@,Howdy}}
64     *
65     * Refer to {@see https://www.dokuwiki.org/plugin:addnewpage} for details.
66     *
67     * @param   string $match The text matched by the patterns
68     * @param   int $state The lexer state for the match
69     * @param   int $pos The character position of the matched text
70     * @param   Doku_Handler $handler The Doku_Handler object
71     *
72     * @return  array Return an array with all data you want to use in render
73     * @codingStandardsIgnoreStart
74     */
75    public function handle($match, $state, $pos, Doku_Handler $handler) {
76        /* @codingStandardsIgnoreEnd */
77        $match = substr($match, 9, -2); // strip markup
78
79        $data = array(
80            'namespace' => '',
81            'newpagetemplates' => array(),
82            'newpagevars' => '',
83            'options' => array(
84                'exclude' => $this->getConf('addpage_exclude'),
85                'showroot' => $this->getConf('addpage_showroot'),
86                'hide' => $this->getConf('addpage_hide'),
87                'hideacl' => $this->getConf('addpage_hideACL'),
88                'autopage' => $this->getConf('addpage_autopage'),
89            )
90        );
91
92        if(preg_match('/>(.*?)(#|\?|$)/', $match, $m)) {
93            $data['namespace'] = trim($m[1]);
94        }
95
96        # Extract the newpagetemplate plugin parameters
97        # - after the initial #: the template name
98        # - after optional 2nd #: custom variable names
99        if(preg_match('/#(.*?)(?:#(.*?))?(?:\?|$)/', $match, $m)) {
100            $data['newpagetemplates'] = array_map('trim', explode(',', $m[1]));
101            $data['newpagevars'] = trim($m[2] ?? '');
102        }
103
104        if(preg_match('/\?(.*?)(#|$)/', $match, $m)) {
105            $this->_parseOptions($m[1], $data['options']);
106        }
107
108        return $data;
109    }
110
111    /**
112     * Create the new-page form.
113     *
114     * @param   $format     string        output format being rendered
115     * @param   $renderer Doku_Renderer the current renderer object
116     * @param   $data     array         data created by handler()
117     * @return  boolean                 rendered correctly?
118     */
119    public function render($format, Doku_Renderer $renderer, $data) {
120        global $lang;
121
122        // make options available in class
123        $this->options = $data['options'];
124
125        if($format == 'xhtml') {
126            $disablecache = false;
127            $namespaceinput = $this->_htmlNamespaceInput($data['namespace'], $disablecache);
128            if($namespaceinput === false) {
129                if($this->options['hideacl']) {
130                    $renderer->doc .= '';
131                } else {
132                    $renderer->doc .= $this->getLang('nooption');
133                }
134                return true;
135            }
136            if($disablecache) $renderer->info['cache'] = false;
137
138            $newpagetemplateinput = $this->_htmlTemplateInput($data['newpagetemplates']);
139
140            $input = 'text';
141            if($this->options['autopage']) $input = 'hidden';
142
143            $form = '<div class="addnewpage"><p>'
144                . '<form name="addnewpage" method="get" action="' . DOKU_BASE . DOKU_SCRIPT . '" accept-charset="' . $lang['encoding'] . '">'
145                . $namespaceinput
146                . '<input class="edit" type="'.$input.'" name="title" size="20" maxlength="255" tabindex="2" />'
147                . $newpagetemplateinput
148                . '<input type="hidden" name="newpagevars" value="' . $data['newpagevars'] . '"/>'
149                . '<input type="hidden" name="do" value="edit" />'
150                . '<input type="hidden" name="id" />'
151                . '<input class="button" type="submit" value="' . $this->getLang('okbutton') . '" tabindex="4" />'
152                . '</form>'
153                . '</p></div>';
154
155            $renderer->doc .= $form;
156
157            return true;
158        }
159        return false;
160    }
161
162    /**
163     * Overwrites the $options with the ones parsed from $optstr
164     *
165     * @param string $optstr
166     * @param array $options
167     * @author Andreas Gohr <gohr@cosmocode.de>
168     */
169    protected function _parseOptions($optstr, &$options) {
170        $opts = preg_split('/[,&]/', $optstr);
171
172        foreach($opts as $opt) {
173            $opt = strtolower(trim($opt));
174            $val = true;
175            // booleans can be negated with a no prefix
176            if(substr($opt, 0, 2) == 'no') {
177                $opt = substr($opt, 2);
178                $val = false;
179            }
180
181            // not a known option? might be a key=value pair
182            if(!isset($options[$opt])) {
183                list($opt, $val) = array_map('trim', explode('=', $opt, 2));
184            }
185
186            // still unknown? skip it
187            if(!isset($options[$opt])) continue;
188
189            // overwrite the current value
190            $options[$opt] = $val;
191        }
192    }
193
194    /**
195     * Parse namespace request
196     *
197     * This creates the final ID to be created (still having an @INPUT@ variable
198     * which is filled in via JavaScript)
199     *
200     * @author Samuele Tognini <samuele@cli.di.unipi.it>
201     * @author Michael Braun <michael-dev@fami-braun.de>
202     * @author Andreas Gohr <gohr@cosmocode.de>
203     * @param string $ns The namespace as given in the syntax
204     * @return string
205     */
206    protected function _parseNS($ns) {
207        global $INFO;
208
209        $selfid = $INFO['id'];
210        $selfns = getNS($selfid);
211        // replace the input variable with something unique that survives cleanID
212        $keep = sha1(time());
213
214        // by default append the input to the namespace (except on autopage)
215        if(strpos($ns, '@INPUT@') === false && !$this->options['autopage']) $ns .= ":@INPUT@";
216
217        // date replacements
218        $ns = dformat(null, $ns);
219
220        // placeholders
221        $replacements = array(
222            '/\//' => ':', // forward slashes to colons
223            '/@PAGE@/' => $selfid,
224            '/@NS@/' => $selfns,
225            '/^\.(:|\/|$)/' => "$selfns:",
226            '/@INPUT@/' => $keep,
227        );
228        $ns = preg_replace(array_keys($replacements), array_values($replacements), $ns);
229
230        // clean up, then reinsert the input variable
231        $ns = cleanID($ns);
232        return str_replace($keep, '@INPUT@', $ns);
233    }
234
235    /**
236     * Create the HTML Select element for namespace selection.
237     *
238     * @param string|false $dest_ns The destination namespace, or false if none provided.
239     * @param bool $disablecache reference indicates if caching need to be disabled
240     * @global string $ID The page ID
241     * @return string Select element with appropriate NS selected.
242     */
243    protected function _htmlNamespaceInput($dest_ns, &$disablecache) {
244        global $ID;
245        $disablecache = false;
246
247        // If a NS has been provided:
248        // Whether to hide the NS selection (otherwise, show only subnamespaces).
249        $hide = $this->options['hide'];
250
251        $parsed_dest_ns = $this->_parseNS($dest_ns);
252        // Whether the user can create pages in the provided NS (or root, if no
253        // destination NS has been set.
254        $can_create = (auth_quickaclcheck($parsed_dest_ns . ":") >= AUTH_CREATE);
255
256        //namespace given, but hidden
257        if($hide && !empty($dest_ns)) {
258            if($can_create) {
259                return '<input type="hidden" name="np_cat" id="np_cat" value="' . $parsed_dest_ns . '"/>';
260            } else {
261                return false;
262            }
263        }
264
265        //show select of given namespace
266        $currentns = getNS($ID);
267
268        $ret = '<select class="edit" id="np_cat" name="np_cat" tabindex="1">';
269
270        // Whether the NS select element has any options
271        $someopt = false;
272
273        // Show root namespace if requested and allowed
274        if($this->options['showroot'] && $can_create) {
275            if(empty($dest_ns)) {
276                // If no namespace has been provided, add an option for the root NS.
277                $ret .= '<option ' . (($currentns == '') ? 'selected ' : '') . ' value="">' . $this->getLang('namespaceRoot') . '</option>';
278            } else {
279                // If a namespace has been provided, add an option for it.
280                $ret .= '<option ' . (($currentns == $dest_ns) ? 'selected ' : '') . ' value="' . formText($dest_ns) . '">' . formText($dest_ns) . '</option>';
281            }
282            $someopt = true;
283        }
284
285        $subnamespaces = $this->_getNamespaceList($dest_ns);
286
287        // The top of this stack will always be the last printed ancestor namespace
288        $ancestor_stack = array();
289        if (!empty($dest_ns)) {
290            $ancestor_stack[] = $dest_ns;
291        }
292
293        foreach($subnamespaces as $ns) {
294
295            if(auth_quickaclcheck($ns . ":") < AUTH_CREATE) continue;
296
297            // Pop any elements off the stack that are not ancestors of the current namespace
298            while(!empty($ancestor_stack) && strpos($ns, $ancestor_stack[count($ancestor_stack) - 1] . ':') !== 0) {
299                array_pop($ancestor_stack);
300            }
301
302            $nsparts = explode(':', $ns);
303            $first_unprinted_depth = empty($ancestor_stack) ? 1 : (2 + substr_count($ancestor_stack[count($ancestor_stack) - 1], ':'));
304            for($i = $first_unprinted_depth, $end = count($nsparts); $i <= $end; $i++) {
305                $namespace = implode(':', array_slice($nsparts, 0, $i));
306                $ancestor_stack[] = $namespace;
307                $selectOptionText = str_repeat('&nbsp;&nbsp;', substr_count($namespace, ':')) . $nsparts[$i - 1];
308                $ret .= '<option ' .
309                    (($currentns == $namespace) ? 'selected ' : '') .
310                    ($i == $end ? ('value="' . $namespace . '">') : 'disabled>') .
311                    $selectOptionText .
312                    '</option>';
313            }
314            $someopt = true;
315            $disablecache = true;
316        }
317
318        $ret .= '</select>';
319
320        if($someopt) {
321            return $ret;
322        } else {
323            return false;
324        }
325    }
326
327    /**
328     * Get a list of namespaces below the given namespace.
329     * Recursively fetches subnamespaces.
330     *
331     * @param string $topns The top namespace
332     * @return array Multi-dimensional array of all namespaces below $tns
333     */
334    protected function _getNamespaceList($topns = '') {
335        global $conf;
336
337        $topns = utf8_encodeFN(str_replace(':', '/', $topns));
338
339        $excludes = $this->options['exclude'];
340        if($excludes == "") {
341            $excludes = array();
342        } else {
343            $excludes = @explode(';', strtolower($excludes));
344        }
345        $searchdata = array();
346        search($searchdata, $conf['datadir'], 'search_namespaces', array(), $topns);
347
348        $namespaces = array();
349        foreach($searchdata as $ns) {
350            foreach($excludes as $exclude) {
351                if(!empty($exclude) && strpos($ns['id'], $exclude) === 0) {
352                    continue 2;
353                }
354            }
355            $namespaces[] = $ns['id'];
356        }
357
358        return $namespaces;
359    }
360
361    /**
362     * Create html for selection of namespace templates
363     *
364     * @param array $newpagetemplates array of namespace templates
365     * @return string html of select or hidden input
366     */
367    public function _htmlTemplateInput($newpagetemplates) {
368        $cnt = count($newpagetemplates);
369        if($cnt < 1 || $cnt == 1 && $newpagetemplates[0] == '') {
370            $input = '';
371
372        } else {
373            if($cnt == 1) {
374                list($template,) = $this->_parseNSTemplatePage($newpagetemplates[0]);
375                $input = '<input type="hidden" name="newpagetemplate" value="' . formText($template) . '" />';
376            } else {
377                $first = true;
378                $input = '<select name="newpagetemplate" tabindex="3">';
379                foreach($newpagetemplates as $template) {
380                    $p = ($first ? ' selected="selected"' : '');
381                    $first = false;
382
383                    list($template, $name) = $this->_parseNSTemplatePage($template);
384                    $p .= ' value="' . formText($template) . '"';
385                    $input .= "<option $p>" . formText($name) . "</option>";
386                }
387                $input .= '</select>';
388            }
389            $input = DOKU_TAB . DOKU_TAB . $input . DOKU_LF;
390        }
391        return $input;
392    }
393
394    /**
395     * Parses and resolves the namespace template page
396     *
397     * @param $nstemplate
398     * @return array
399     */
400    protected function _parseNSTemplatePage($nstemplate) {
401        global $ID;
402
403        @list($template, $name) = explode('|', $nstemplate, 2);
404        $template = (new PageResolver($ID))->resolveId($template);
405        if (is_null($name)) $name = $template;
406
407        return array($template, $name);
408    }
409
410}
411