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 111 . '" accept-charset="' . $lang['encoding'] . '">' 112 . $namespaceinput 113 . '<input class="edit" type="text" name="title" size="20" maxlength="255" tabindex="2" placeholder="' 114 . $this->getLang('name') . '"/>' 115 . $newpagetemplateinput 116 . '<input type="hidden" name="newpagevars" value="' . $data['newpagevars'] . '"/>' 117 . '<input type="hidden" name="do" value="edit" />' 118 . '<input type="hidden" name="id" />' 119 . '<input class="button" type="submit" value="' . $this->getLang('okbutton') . '" tabindex="4" />' 120 . '</form>' 121 . '</p></div>'; 122 123 $renderer->doc .= $form; 124 125 return true; 126 } 127 return false; 128 } 129 130 /** 131 * Parse namespace request 132 * 133 * @author Samuele Tognini <samuele@cli.di.unipi.it> 134 * @author Michael Braun <michael-dev@fami-braun.de> 135 */ 136 protected function _parseNS($ns) { 137 $ID=getID(); 138 if(strpos($ns, '@PAGE@') !== false) { 139 return cleanID(str_replace('@PAGE@', $ID, $ns)); 140 } 141 if($ns == "@NS@") return getNS($ID); 142 $ns = preg_replace("/^\.(:|$)/", dirname(str_replace(':', '/', $ID)) . "$1", $ns); 143 $ns = str_replace("/", ":", $ns); 144 145 return cleanID($ns); 146 } 147 148 /** 149 * Create the HTML Select element for namespace selection. 150 * 151 * @param string|false $dest_ns The destination namespace, or false if none provided. 152 * @param bool $disablecache reference indicates if caching need to be disabled 153 * @global string $ID The page ID 154 * @return string Select element with appropriate NS selected. 155 */ 156 protected function _htmlNamespaceInput($dest_ns, &$disablecache) { 157 global $ID; 158 $disablecache = false; 159 160 // If a NS has been provided: 161 // Whether to hide the NS selection (otherwise, show only subnamespaces). 162 $hide = $this->getConf('addpage_hide'); 163 164 $parsed_dest_ns = $this->_parseNS($dest_ns); 165 // Whether the user can create pages in the provided NS (or root, if no 166 // destination NS has been set. 167 $can_create = (auth_quickaclcheck($parsed_dest_ns . ":") >= AUTH_CREATE); 168 169 //namespace given, but hidden 170 if($hide && !empty($dest_ns)) { 171 if($can_create) { 172 return '<input type="hidden" name="np_cat" id="np_cat" value="' . $parsed_dest_ns . '"/>'; 173 } else { 174 return false; 175 } 176 } 177 178 //show select of given namespace 179 $currentns = getNS($ID); 180 181 $ret = '<select class="edit" id="np_cat" name="np_cat" tabindex="1">'; 182 183 // Whether the NS select element has any options 184 $someopt = false; 185 186 // Show root namespace if requested and allowed 187 if($this->getConf('addpage_showroot') && $can_create) { 188 if(empty($dest_ns)) { 189 // If no namespace has been provided, add an option for the root NS. 190 $ret .= '<option ' . (($currentns == '') ? 'selected ' : '') . ' value="">' . $this->getLang('namespaceRoot') . '</option>'; 191 } else { 192 // If a namespace has been provided, add an option for it. 193 $ret .= '<option ' . (($currentns == $dest_ns) ? 'selected ' : '') . ' value="' . formText($dest_ns) . '">' . formText($dest_ns) . '</option>'; 194 } 195 $someopt = true; 196 } 197 198 $subnamespaces = $this->_getNamespaceList($dest_ns); 199 200 // The top of this stack will always be the last printed ancestor namespace 201 $ancestor_stack = array(); 202 if (!empty($dest_ns)) { 203 $ancestor_stack[] = $dest_ns; 204 } 205 206 foreach($subnamespaces as $ns) { 207 208 if(auth_quickaclcheck($ns . ":") < AUTH_CREATE) continue; 209 210 // Pop any elements off the stack that are not ancestors of the current namespace 211 while(!empty($ancestor_stack) && strpos($ns, $ancestor_stack[count($ancestor_stack) - 1] . ':') !== 0) { 212 array_pop($ancestor_stack); 213 } 214 215 $nsparts = explode(':', $ns); 216 $first_unprinted_depth = empty($ancestor_stack)? 1 : (2 + substr_count($ancestor_stack[count($ancestor_stack) - 1], ':')); 217 for ($i = $first_unprinted_depth, $end = count($nsparts); $i <= $end; $i++) { 218 $namespace = implode(':', array_slice($nsparts, 0, $i)); 219 $ancestor_stack[] = $namespace; 220 $selectOptionText = str_repeat(' ', substr_count($namespace, ':')) . $nsparts[$i - 1]; 221 $ret .= '<option ' . 222 (($currentns == $namespace) ? 'selected ' : '') . 223 ($i == $end? ('value="' . $namespace . '">') : 'disabled>') . 224 $selectOptionText . 225 '</option>'; 226 } 227 $someopt = true; 228 $disablecache = true; 229 } 230 231 $ret .= '</select>'; 232 233 if($someopt) { 234 return $ret; 235 } else { 236 return false; 237 } 238 } 239 240 /** 241 * Get a list of namespaces below the given namespace. 242 * Recursively fetches subnamespaces. 243 * 244 * @param string $topns The top namespace 245 * @return array Multi-dimensional array of all namespaces below $tns 246 */ 247 protected function _getNamespaceList($topns = '') { 248 global $conf; 249 250 $topns = utf8_encodeFN(str_replace(':', '/', $topns)); 251 252 $excludes = $this->getConf('addpage_exclude'); 253 if($excludes == "") { 254 $excludes = array(); 255 } else { 256 $excludes = @explode(';', strtolower($excludes)); 257 } 258 $searchdata = array(); 259 search($searchdata, $conf['datadir'], 'search_namespaces', array(), $topns); 260 261 $namespaces = array(); 262 foreach($searchdata as $ns) { 263 foreach($excludes as $exclude) { 264 if( ! empty($exclude) && strpos($ns['id'], $exclude) === 0) { 265 continue 2; 266 } 267 } 268 $namespaces[] = $ns['id']; 269 } 270 271 return $namespaces; 272 } 273 274 /** 275 * Create html for selection of namespace templates 276 * 277 * @param array $newpagetemplates array of namespace templates 278 * @return string html of select or hidden input 279 */ 280 public function _htmlTemplateInput($newpagetemplates) { 281 $cnt = count($newpagetemplates); 282 if($cnt < 1 || $cnt == 1 && $newpagetemplates[0] == '') { 283 $input = ''; 284 285 } else { 286 if($cnt == 1) { 287 list($template, ) = $this->_parseNSTemplatePage($newpagetemplates[0]); 288 $input = '<input type="hidden" name="newpagetemplate" value="' . formText($template) . '" />'; 289 } else { 290 $first = true; 291 $input = '<select name="newpagetemplate" tabindex="3">'; 292 foreach($newpagetemplates as $template) { 293 $p = ($first ? ' selected="selected"' : ''); 294 $first = false; 295 296 list($template, $name) = $this->_parseNSTemplatePage($template); 297 $p .= ' value="'.formText($template).'"'; 298 $input .= "<option $p>".formText($name)."</option>"; 299 } 300 $input .= '</select>'; 301 } 302 $input = DOKU_TAB . DOKU_TAB . $input . DOKU_LF; 303 } 304 return $input; 305 } 306 307 /** 308 * Parses and resolves the namespace template page 309 * 310 * @param $nstemplate 311 * @return array 312 */ 313 protected function _parseNSTemplatePage($nstemplate) { 314 global $ID; 315 316 @list($template, $name) = explode('|', $nstemplate, 2); 317 $template = (new PageResolver($ID))->resolveId($template); 318 if (is_null($name)) $name = $template; 319 320 return array($template, $name); 321 } 322 323} 324