1<?php 2 3use dokuwiki\Cache\Cache; 4 5/** 6 * DokuWiki Plugin tagfilter (Syntax Component) 7 * 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @author lisps 10 */ 11/* 12 * All DokuWiki plugins to extend the parser/rendering mechanism 13 * need to inherit from this class 14 */ 15 16class syntax_plugin_tagfilter_filter extends DokuWiki_Syntax_Plugin 17{ 18 19 /** @var int[] counts forms per page for creating an unique form id */ 20 protected $formCounter = []; 21 22 protected function incrementFormCounter() 23 { 24 global $ID; 25 if (array_key_exists($ID, $this->formCounter)) { 26 return $this->formCounter[$ID]++; 27 } else { 28 $this->formCounter[$ID] = 1; 29 return 0; 30 } 31 } 32 33 protected function getFormCounter() 34 { 35 global $ID; 36 if (array_key_exists($ID, $this->formCounter)) { 37 return $this->formCounter[$ID]; 38 } else { 39 return 0; 40 } 41 } 42 43 /* 44 * What kind of syntax are we? 45 */ 46 public function getType() 47 { 48 return 'substition'; 49 } 50 51 /* 52 * Where to sort in? 53 */ 54 function getSort() 55 { 56 return 155; 57 } 58 59 /* 60 * Paragraph Type 61 */ 62 public function getPType() 63 { 64 return 'block'; 65 } 66 67 /* 68 * Connect pattern to lexer 69 */ 70 public function connectTo($mode) 71 { 72 $this->Lexer->addSpecialPattern("\{\{tagfilter>.*?\}\}", $mode, 'plugin_tagfilter_filter'); 73 } 74 75 /* 76 * Handle the matches 77 */ 78 public function handle($match, $state, $pos, Doku_Handler $handler) 79 { 80 $match = trim(substr($match, 12, -2)); 81 82 return $this->getOpts($match); 83 } 84 85 /** 86 * Parses syntax written by user 87 * 88 * @param string $match The text matched in the pattern 89 * @return array with:<br> 90 * int 'id' unique number for current form, 91 * string 'ns' list only pages from this namespace, 92 * array 'pagelistFlags' all flags set by user in syntax, will be supplied directly to pagelist plugin, 93 * array 'tagfilterFlags' only tags for the tagfilter plugin @see helper_plugin_tagfilter_syntax::parseFlags() 94 */ 95 protected function getOpts($match) 96 { 97 global $ID; 98 99 /** @var helper_plugin_tagfilter_syntax $HtagfilterSyntax */ 100 $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax'); 101 $opts['id'] = $this->incrementFormCounter(); 102 103 list($match, $flags) = array_pad(explode('&', $match, 2), 2, ''); 104 $flags = explode('&', $flags); 105 106 107 list($ns, $tag) = array_pad(explode('?', $match), 2, ''); 108 if ($tag === '') { 109 $tag = $ns; 110 $ns = ''; 111 } 112 113 if (($ns == '*') || ($ns == ':')) { 114 $ns = ''; 115 } elseif ($ns == '.') { 116 $ns = getNS($ID); 117 } else { 118 $ns = cleanID($ns); 119 } 120 121 $opts['ns'] = $ns; 122 123 //only flags for tagfilter 124 $opts['tagfilterFlags'] = $HtagfilterSyntax->parseFlags($flags); 125 126 //all flags set by user for pagelist plugin 127 $opts['pagelistFlags'] = array_map('trim', $flags); 128 129 //read and parse tag 130 $tagFilters = []; 131 $selectExpressions = array_map('trim', explode('|', $tag)); 132 foreach ($selectExpressions as $key => $parts) { 133 $parts = explode("=", $parts);//split in Label,RegExp,Default value 134 135 $tagFilters['label'][$key] = trim($parts[0]); 136 $tagFilters['tagExpression'][$key] = trim($parts[1] ?? ''); 137 $tagFilters['selectedTags'][$key] = isset($parts[2]) ? explode(' ', $parts[2]) : []; 138 } 139 140 $opts['tagFilters'] = $tagFilters; 141 142 return $opts; 143 } 144 145 /** 146 * Create output 147 * 148 * @param string $format output format being rendered 149 * @param Doku_Renderer $renderer the current renderer object 150 * @param array $opt data created by handler() 151 * @return boolean rendered correctly? 152 */ 153 public function render($format, Doku_Renderer $renderer, $opt) 154 { 155 global $INFO, $ID, $conf, $INPUT; 156 157 /* @var helper_plugin_tagfilter_syntax $HtagfilterSyntax */ 158 $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax'); 159 $flags = $opt['tagfilterFlags']; 160 161 if ($format === 'metadata') return false; 162 if ($format === 'xhtml') { 163 $renderer->nocache(); 164 165 $renderer->cdata("\n"); 166 167 $depends = [ 168 'files' => [ 169 $INFO['filepath'], 170 DOKU_CONF . 'acl.auth.php', 171 ] 172 ]; 173 $depends['files'] = array_merge($depends['files'], getConfigFiles('main')); 174 175 if ($flags['cache']) { 176 $depends['age'] = $flags['cache']; 177 } else if ($flags['cache'] === false) { 178 //build cache dependencies TODO check if this bruteforce method (adds just all pages of namespace as dependency) is proportional 179 $dir = utf8_encodeFN(str_replace(':', '/', $opt['ns'])); 180 $data = []; 181 $opts = [ 182 'ns' => $opt['ns'], 183 'excludeNs' => $flags['excludeNs'] 184 ]; 185 search($data, $conf['datadir'], [$HtagfilterSyntax, 'search_all_pages'], $opts, $dir); //all pages inside namespace 186 $depends['files'] = array_merge($depends['files'], $data); 187 } else { 188 $depends['purge'] = true; 189 } 190 191 //cache to store tagfilter options, matched pages and prepared data 192 $filterDataCacheKey = 'plugin_tagfilter_' . $ID . '_' . $opt['id']; 193 $filterDataCache = new Cache($filterDataCacheKey, '.tcache'); 194 if (!$filterDataCache->useCache($depends)) { 195 $cachedata = $HtagfilterSyntax->getTagPageRelations($opt); 196 $cachedata[] = $HtagfilterSyntax->prepareList($cachedata[1], $flags); 197 $filterDataCache->storeCache(serialize($cachedata)); 198 } else { 199 $cachedata = unserialize($filterDataCache->retrieveCache()); 200 } 201 202 list($tagFilters, $allPageids, $preparedPages) = $cachedata; 203 204 // cache to store html per user 205 $htmlPerUserCacheKey = 'plugin_tagfilter_' . $ID . '_' . $opt['id'] . '_' . $INPUT->server->str('REMOTE_USER') 206 . $INPUT->server->str('HTTP_HOST') . $INPUT->server->str('SERVER_PORT'); 207 $htmlPerUserCache = new Cache($htmlPerUserCacheKey, '.tucache'); 208 209 //purge cache if pages does not exist anymore 210 foreach ($allPageids as $key => $pageid) { 211 if (!page_exists($pageid)) { 212 unset($allPageids[$key]); 213 $filterDataCache->removeCache(); 214 $htmlPerUserCache->removeCache(); 215 } 216 } 217 218 if (empty($flags['include'])) { 219 if (!$htmlPerUserCache->useCache(['files' => [$filterDataCache->cache]])) { 220 $html = $this->htmlOutput($tagFilters, $allPageids, $preparedPages, $opt); 221 $htmlPerUserCache->storeCache($html); 222 } else { 223 $html = $htmlPerUserCache->retrieveCache(); 224 } 225 226 $renderer->doc .= $html; 227 } else { 228 // Use include plugin. Does not use the htmlPerUserCache. TODO? 229 230 // attention: htmlPrepareOutput modifies $tagFilters, $allPageids, $preparedPages. 231 $this->htmlPrepareOutput($tagFilters, $allPageids, $preparedPages, $opt); 232 $renderer->doc .= $this->htmlFormOutput($tagFilters, $allPageids, $opt); 233 $renderer->doc .= "<div id='tagfilter_ergebnis_" . $opt['id'] . "' class='tagfilter'>"; 234 235 $includeHelper = $this->loadHelper('include'); 236 $includeFlags = $includeHelper->get_flags($flags['include']); 237 238 foreach($preparedPages as $page) { 239 $renderer->nest($includeHelper->_get_instructions($page['id'], '', 'page', 0, $includeFlags)); 240 } 241 242 $renderer->doc .= "</div>"; 243 } 244 } 245 return true; 246 } 247 248 /** 249 * Returns html of the tagfilter form 250 * 251 * @param array $tagFilters 252 * @param array $allPageids 253 * @param array $preparedPages 254 * @param array $opt option array from the handler 255 * @return string 256 */ 257 private function htmlOutput($tagFilters, $allPageids, $preparedPages, array $opt) 258 { 259 // attention: htmlPrepareOutput modifies $tagFilters, $allPageids, $preparedPages. 260 $this->htmlPrepareOutput($tagFilters, $allPageids, $preparedPages, $opt); 261 262 $output = $this->htmlFormOutput($tagFilters, $allPageids, $opt) 263 . $this->htmlPagelistOutput($preparedPages, $opt); 264 265 return $output; 266 } 267 268 private function htmlPrepareOutput(&$tagFilters, &$allPageids, &$preparedPages, array $opt) 269 { 270 /* @var helper_plugin_tagfilter $Htagfilter */ 271 $Htagfilter = $this->loadHelper('tagfilter'); 272 273 //check for read access 274 foreach ($allPageids as $key => $pageid) { 275 if (!$Htagfilter->canRead($pageid)) { 276 unset($allPageids[$key]); 277 } 278 } 279 280 //check tags for visibility 281 foreach ($tagFilters['pagesPerMatchedTags'] as &$pagesPerMatchedTag) { 282 if (!is_array($pagesPerMatchedTag)) { 283 $pagesPerMatchedTag = []; 284 } 285 foreach ($pagesPerMatchedTag as $tag => $pageidsPerTag) { 286 if (count(array_intersect($pageidsPerTag, $allPageids)) == 0) { 287 unset($pagesPerMatchedTag[$tag]); 288 } 289 } 290 } 291 unset($pagesPerMatchedTag); 292 293 foreach ($preparedPages as $key => $page) { 294 if (!in_array($page['id'], $allPageids)) { 295 unset($preparedPages[$key]); 296 } 297 } 298 } 299 300 private function htmlFormOutput($tagFilters, $allPageids, array $opt) { 301 /* @var helper_plugin_tagfilter $Htagfilter */ 302 $Htagfilter = $this->loadHelper('tagfilter'); 303 304 $flags = $opt['tagfilterFlags']; 305 $output = ''; 306 307 $form = new Doku_Form([ 308 'id' => 'tagdd_' . $opt['id'], 309 'data-idx' => $opt['id'], 310 'data-plugin' => 'tagfilter', 311 'data-tags' => json_encode($tagFilters['pagesPerMatchedTags']), 312 ]); 313 $output .= "\n"; 314 //Fieldset manuell hinzufügen da ein style Parameter übergeben werden soll 315 $form->addElement([ 316 '_elem' => 'openfieldset', 317 '_legend' => 'Tagfilter', 318 'style' => 'text-align:left;width:99%', 319 'id' => '__tagfilter_' . $opt['id'], 320 'class' => ($flags['labels'] !== false) ? '' : 'hidelabel', 321 322 ]); 323 $form->_infieldset = true; //Fieldset starten 324 325 if ($flags['pagesearch']) { 326 $label = $flags['pagesearchlabel']; 327 328 $pagetitles = []; 329 foreach ($allPageids as $pageid) { 330 $pagetitles[$pageid] = $Htagfilter->getPageTitle($pageid); 331 } 332 asort($pagetitles, SORT_NATURAL | SORT_FLAG_CASE); 333 334 $selectedTags = []; 335 $id = '__tagfilter_page_' . $opt['id']; 336 337 $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist 338 'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')', 339 'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''), 340 'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')), 341 'data-label' => hsc(utf8_strtolower(trim($label))), 342 ]; 343 if ($flags['multi']) { //unterscheidung ob Multiple oder Single 344 $attrs['multiple'] = 'multiple'; 345 $attrs['size'] = $this->getConf("DropDownList_size"); 346 } else { 347 $attrs['size'] = 1; 348 $pagetitles = array_reverse($pagetitles, true); 349 $pagetitles[''] = ''; 350 $pagetitles = array_reverse($pagetitles, true); 351 } 352 $form->addElement(form_makeListboxField($label, $pagetitles, $selectedTags, $label, $id, 'tagfilter', $attrs)); 353 } 354 $output .= '<script type="text/javascript">/*<![CDATA[*/ var tagfilter_container = {}; /*!]]>*/</script>' . "\n"; 355 //$output .= '<script type="text/javascript">/*<![CDATA[*/ '.'tagfilter_container.tagfilter_'.$opt['id'].' = '.json_encode($tagFilters['tags2']).'; /*!]]>*/</script>'."\n"; 356 foreach ($tagFilters['pagesPerMatchedTags'] as $key => $pagesPerMatchedTag) { 357 $id = false; 358 $label = $tagFilters['label'][$key]; 359 $selectedTags = $tagFilters['selectedTags'][$key]; 360 361 //get tag labels 362 $tags = []; 363 364 foreach (array_keys($pagesPerMatchedTag) as $tagid) { 365 $tags[$tagid] = $Htagfilter->getTagLabel($tagid); 366 } 367 368 foreach ($selectedTags as &$item) { 369 $item = utf8_strtolower(trim($item)); 370 } 371 unset($item); 372 373 374 $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist 375 'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')', 376 'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''), 377 'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')), 378 'data-label' => hsc(str_replace(' ', '_', utf8_strtolower(trim($label)))), 379 380 ]; 381 if ($flags['multi']) { //unterscheidung ob Multiple oder Single 382 $attrs['multiple'] = 'multiple'; 383 $attrs['size'] = $this->getConf("DropDownList_size"); 384 } else { 385 $attrs['size'] = 1; 386 $tags = array_reverse($tags, true); 387 $tags[''] = ''; 388 $tags = array_reverse($tags, true); 389 } 390 391 if ($flags['chosen']) { 392 $links = []; 393 foreach ($tags as $k => $t) { 394 $links[$k] = [ 395 'link' => $Htagfilter->getImageLinkByTag($k), 396 ]; 397 } 398 $jsVar = 'tagfilter_jsVar_' . rand(); 399 $output .= '<script type="text/javascript">/*<![CDATA[*/ tagfilter_container.' . $jsVar . ' =' 400 . json_encode($links) . 401 '; /*!]]>*/</script>' . "\n"; 402 403 $id = '__tagfilter_' . $opt["id"] . '_' . rand(); 404 405 if ($flags['tagimage']) { 406 $attrs['data-tagimage'] = $jsVar; 407 } 408 409 } 410 $form->addElement(form_makeListboxField($label, $tags, $selectedTags, $label, $id, 'tagfilter', $attrs)); 411 } 412 413 $form->addElement(form_makeButton('button', '', $this->getLang('Delete filter'), ['onclick' => 'tagfilter_cleanform(' . $opt['id'] . ',true)'])); 414 if ($flags['count']) { 415 $form->addElement('<div class="tagfilter_count">' . $this->getLang('found_count') . ': ' . '<span class="tagfilter_count_number"></span></div>'); 416 } 417 $form->endFieldset(); 418 $output .= $form->getForm();//Form Ausgeben 419 420 return $output; 421 } 422 423 private function htmlPagelistOutput($preparedPages, array $opt) { 424 /* @var helper_plugin_tagfilter_syntax $HtagfilterSyntax */ 425 $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax'); 426 427 $output = ''; 428 429 $output .= "<div id='tagfilter_ergebnis_" . $opt['id'] . "' class='tagfilter'>"; 430 //dbg($opt['pagelistFlags']); 431 $output .= $HtagfilterSyntax->renderList($preparedPages, $opt['tagfilterFlags'], $opt['pagelistFlags']); 432 $output .= "</div>"; 433 434 return $output; 435 } 436} 437