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 *               PhpMissingParamTypeInspection, PhpMissingReturnTypeInspection
11 */
12
13// must be run within Dokuwiki
14use dokuwiki\File\PageResolver;
15
16if(!defined('DOKU_INC')) die();
17
18class syntax_plugin_addnewpage extends DokuWiki_Syntax_Plugin {
19
20    /**
21     * Syntax Type
22     */
23    public function getType() {
24        return 'substition';
25    }
26
27    /**
28     * Paragraph Type
29     */
30    public function getPType() {
31        return 'block';
32    }
33
34    /**
35     * @return int
36     */
37    public function getSort() {
38        return 199;
39    }
40
41    /**
42     * @param string $mode
43     */
44    public function connectTo($mode) {
45        $this->Lexer->addSpecialPattern('\{\{NEWPAGE[^\}]*\}\}', $mode, 'plugin_addnewpage');
46    }
47
48    /**
49     * Handler to prepare matched data for the rendering process
50     *
51     * Handled syntax options:
52     *   {{NEWPAGE}}
53     *   {{NEWPAGE>your:namespace}}
54     *   {{NEWPAGE#newtpl1,newtpl2}}
55     *   {{NEWPAGE#newtpl1|Title1,newtpl2|Title1}}
56     *   {{NEWPAGE>your:namespace#newtpl1|Title1,newtpl2|Title1}}
57     *   {{NEWPAGE>your:namespace#newtpl1|Title1,newtpl2|Title1#@HI@,Howdy}}
58     *
59     * @param   string       $match   The text matched by the patterns
60     * @param   int          $state   The lexer state for the match
61     * @param   int          $pos     The character position of the matched text
62     * @param   Doku_Handler $handler The Doku_Handler object
63     * @return  array Return an array with all data you want to use in render
64     * @codingStandardsIgnoreStart
65     */
66    public function handle($match, $state, $pos, Doku_Handler $handler) {
67        /* @codingStandardsIgnoreEnd */
68        $options = substr($match, 9, -2); // strip markup
69        $options = explode('#', $options, 3);
70
71        $namespace = trim(ltrim($options[0], '>'));
72        $templates = explode(',', $options[1] ?? '');
73        $templates = array_map('trim', $templates);
74        $newpagevars = trim($options[2] ?? '');
75        return array(
76            'namespace' => $namespace,
77            'newpagetemplates' => $templates,
78            'newpagevars' => $newpagevars
79        );
80    }
81
82    /**
83     * Create the new-page form.
84     *
85     * @param   $format     string        output format being rendered
86     * @param   $renderer Doku_Renderer the current renderer object
87     * @param   $data     array         data created by handler()
88     * @return  boolean                 rendered correctly?
89     */
90    public function render($format, Doku_Renderer $renderer, $data) {
91        global $lang;
92
93        if($format == 'xhtml') {
94            $disablecache = false;
95            $namespaceinput = $this->_htmlNamespaceInput($data['namespace'], $disablecache);
96            if($namespaceinput === false) {
97                if($this->getConf('addpage_hideACL')) {
98                    $renderer->doc .= '';
99                } else {
100                    $renderer->doc .= $this->getLang('nooption');
101                }
102                return true;
103            }
104            if($disablecache) $renderer->info['cache'] = false;
105
106            $newpagetemplateinput = $this->_htmlTemplateInput($data['newpagetemplates']);
107
108            $form = '<div class="addnewpage"><p>'
109                . '<form name="addnewpage" method="get" action="' . DOKU_BASE . DOKU_SCRIPT . '" accept-charset="' . $lang['encoding'] . '">'
110                . $namespaceinput
111                . '<input class="edit" type="text" name="title" size="20" maxlength="255" tabindex="2" />'
112                . $newpagetemplateinput
113                . '<input type="hidden" name="newpagevars" value="' . $data['newpagevars'] . '"/>'
114                . '<input type="hidden" name="do" value="edit" />'
115                . '<input type="hidden" name="id" />'
116                . '<input class="button" type="submit" value="' . $this->getLang('okbutton') . '" tabindex="4" />'
117                . '</form>'
118                . '</p></div>';
119
120            $renderer->doc .= $form;
121
122            return true;
123        }
124        return false;
125    }
126
127    /**
128     * Parse namespace request
129     *
130     * @author Samuele Tognini <samuele@cli.di.unipi.it>
131     * @author Michael Braun <michael-dev@fami-braun.de>
132     */
133    protected function _parseNS($ns) {
134        $ID=getID();
135        if(strpos($ns, '@PAGE@') !== false) {
136            return cleanID(str_replace('@PAGE@', $ID, $ns));
137        }
138        if($ns == "@NS@") return getNS($ID);
139        $ns = preg_replace("/^\.(:|$)/", dirname(str_replace(':', '/', $ID)) . "$1", $ns);
140        $ns = str_replace("/", ":", $ns);
141
142        return cleanID($ns);
143    }
144
145    /**
146     * Create the HTML Select element for namespace selection.
147     *
148     * @param string|false $dest_ns The destination namespace, or false if none provided.
149     * @param bool $disablecache reference indicates if caching need to be disabled
150     * @global string $ID The page ID
151     * @return string Select element with appropriate NS selected.
152     */
153    protected function _htmlNamespaceInput($dest_ns, &$disablecache) {
154        global $ID;
155        $disablecache = false;
156
157        // If a NS has been provided:
158        // Whether to hide the NS selection (otherwise, show only subnamespaces).
159        $hide = $this->getConf('addpage_hide');
160
161        $parsed_dest_ns = $this->_parseNS($dest_ns);
162        // Whether the user can create pages in the provided NS (or root, if no
163        // destination NS has been set.
164        $can_create = (auth_quickaclcheck($parsed_dest_ns . ":") >= AUTH_CREATE);
165
166        //namespace given, but hidden
167        if($hide && !empty($dest_ns)) {
168            if($can_create) {
169                return '<input type="hidden" name="np_cat" id="np_cat" value="' . $parsed_dest_ns . '"/>';
170            } else {
171                return false;
172            }
173        }
174
175        //show select of given namespace
176        $currentns = getNS($ID);
177
178        $ret = '<select class="edit" id="np_cat" name="np_cat" tabindex="1">';
179
180        // Whether the NS select element has any options
181        $someopt = false;
182
183        // Show root namespace if requested and allowed
184        if($this->getConf('addpage_showroot') && $can_create) {
185            if(empty($dest_ns)) {
186                // If no namespace has been provided, add an option for the root NS.
187                $ret .= '<option ' . (($currentns == '') ? 'selected ' : '') . ' value="">' . $this->getLang('namespaceRoot') . '</option>';
188            } else {
189                // If a namespace has been provided, add an option for it.
190                $ret .= '<option ' . (($currentns == $dest_ns) ? 'selected ' : '') . ' value="' . formText($dest_ns) . '">' . formText($dest_ns) . '</option>';
191            }
192            $someopt = true;
193        }
194
195        $subnamespaces = $this->_getNamespaceList($dest_ns);
196
197        // The top of this stack will always be the last printed ancestor namespace
198        $ancestor_stack = array();
199        if (!empty($dest_ns)) {
200            $ancestor_stack[] = $dest_ns;
201        }
202
203        foreach($subnamespaces as $ns) {
204
205            if(auth_quickaclcheck($ns . ":") < AUTH_CREATE) continue;
206
207            // Pop any elements off the stack that are not ancestors of the current namespace
208            while(!empty($ancestor_stack) && strpos($ns, $ancestor_stack[count($ancestor_stack) - 1] . ':') !== 0) {
209                array_pop($ancestor_stack);
210            }
211
212            $nsparts = explode(':', $ns);
213            $first_unprinted_depth = empty($ancestor_stack)? 1 : (2 + substr_count($ancestor_stack[count($ancestor_stack) - 1], ':'));
214            for ($i = $first_unprinted_depth, $end = count($nsparts); $i <= $end; $i++) {
215                $namespace = implode(':', array_slice($nsparts, 0, $i));
216                $ancestor_stack[] = $namespace;
217                $selectOptionText = str_repeat('&nbsp;&nbsp;', substr_count($namespace, ':')) . $nsparts[$i - 1];
218                $ret .= '<option ' .
219                    (($currentns == $namespace) ? 'selected ' : '') .
220                    ($i == $end? ('value="' . $namespace . '">') : 'disabled>') .
221                    $selectOptionText .
222                    '</option>';
223            }
224            $someopt = true;
225            $disablecache = true;
226        }
227
228        $ret .= '</select>';
229
230        if($someopt) {
231            return $ret;
232        } else {
233            return false;
234        }
235    }
236
237    /**
238     * Get a list of namespaces below the given namespace.
239     * Recursively fetches subnamespaces.
240     *
241     * @param string $topns The top namespace
242     * @return array Multi-dimensional array of all namespaces below $tns
243     */
244    protected function _getNamespaceList($topns = '') {
245        global $conf;
246
247        $topns = utf8_encodeFN(str_replace(':', '/', $topns));
248
249        $excludes = $this->getConf('addpage_exclude');
250        if($excludes == "") {
251            $excludes = array();
252        } else {
253            $excludes = @explode(';', strtolower($excludes));
254        }
255        $searchdata = array();
256        search($searchdata, $conf['datadir'], 'search_namespaces', array(), $topns);
257
258        $namespaces = array();
259        foreach($searchdata as $ns) {
260            foreach($excludes as $exclude) {
261                if( ! empty($exclude) && strpos($ns['id'], $exclude) === 0) {
262                    continue 2;
263                }
264            }
265            $namespaces[] = $ns['id'];
266        }
267
268        return $namespaces;
269    }
270
271    /**
272     * Create html for selection of namespace templates
273     *
274     * @param array $newpagetemplates array of namespace templates
275     * @return string html of select or hidden input
276     */
277    public function _htmlTemplateInput($newpagetemplates) {
278        $cnt = count($newpagetemplates);
279        if($cnt < 1 || $cnt == 1 && $newpagetemplates[0] == '') {
280            $input = '';
281
282        } else {
283            if($cnt == 1) {
284                list($template, ) = $this->_parseNSTemplatePage($newpagetemplates[0]);
285                $input = '<input type="hidden" name="newpagetemplate" value="' . formText($template) . '" />';
286            } else {
287                $first = true;
288                $input = '<select name="newpagetemplate" tabindex="3">';
289                foreach($newpagetemplates as $template) {
290                    $p = ($first ? ' selected="selected"' : '');
291                    $first = false;
292
293                    list($template, $name) = $this->_parseNSTemplatePage($template);
294                    $p .= ' value="'.formText($template).'"';
295                    $input .= "<option $p>".formText($name)."</option>";
296                }
297                $input .= '</select>';
298            }
299            $input = DOKU_TAB . DOKU_TAB . $input . DOKU_LF;
300        }
301        return $input;
302    }
303
304    /**
305     * Parses and resolves the namespace template page
306     *
307     * @param $nstemplate
308     * @return array
309     */
310    protected function _parseNSTemplatePage($nstemplate) {
311        global $ID;
312
313        @list($template, $name) = explode('|', $nstemplate, 2);
314        $template = (new PageResolver($ID))->resolveId($template);
315        if (is_null($name)) $name = $template;
316
317        return array($template, $name);
318    }
319
320}
321