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