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