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 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 } 228 return true; 229 } 230 231 /** 232 * Returns html of the tagfilter form 233 * 234 * @param array $tagFilters 235 * @param array $allPageids 236 * @param array $preparedPages 237 * @param array $opt option array from the handler 238 * @return string 239 */ 240 private function htmlOutput($tagFilters, $allPageids, $preparedPages, array $opt) 241 { 242 /* @var helper_plugin_tagfilter $Htagfilter */ 243 $Htagfilter = $this->loadHelper('tagfilter'); 244 /* @var helper_plugin_tagfilter_syntax $HtagfilterSyntax */ 245 $HtagfilterSyntax = $this->loadHelper('tagfilter_syntax'); 246 $flags = $opt['tagfilterFlags']; 247 248 $output = ''; 249 250 //check for read access 251 foreach ($allPageids as $key => $pageid) { 252 if (!$Htagfilter->canRead($pageid)) { 253 unset($allPageids[$key]); 254 } 255 } 256 257 //check tags for visibility 258 foreach ($tagFilters['pagesPerMatchedTags'] as &$pagesPerMatchedTag) { 259 if (!is_array($pagesPerMatchedTag)) { 260 $pagesPerMatchedTag = []; 261 } 262 foreach ($pagesPerMatchedTag as $tag => $pageidsPerTag) { 263 if (count(array_intersect($pageidsPerTag, $allPageids)) == 0) { 264 unset($pagesPerMatchedTag[$tag]); 265 } 266 } 267 } 268 unset($pagesPerMatchedTag); 269 270 foreach ($preparedPages as $key => $page) { 271 if (!in_array($page['id'], $allPageids)) { 272 unset($preparedPages[$key]); 273 } 274 } 275 276 $form = new Doku_Form([ 277 'id' => 'tagdd_' . $opt['id'], 278 'data-idx' => $opt['id'], 279 'data-plugin' => 'tagfilter', 280 'data-tags' => json_encode($tagFilters['pagesPerMatchedTags']), 281 ]); 282 $output .= "\n"; 283 //Fieldset manuell hinzufügen da ein style Parameter übergeben werden soll 284 $form->addElement([ 285 '_elem' => 'openfieldset', 286 '_legend' => 'Tagfilter', 287 'style' => 'text-align:left;width:99%', 288 'id' => '__tagfilter_' . $opt['id'], 289 'class' => ($flags['labels'] !== false) ? '' : 'hidelabel', 290 291 ]); 292 $form->_infieldset = true; //Fieldset starten 293 294 if ($flags['pagesearch']) { 295 $label = $flags['pagesearchlabel']; 296 297 $pagetitles = []; 298 foreach ($allPageids as $pageid) { 299 $pagetitles[$pageid] = $Htagfilter->getPageTitle($pageid); 300 } 301 asort($pagetitles, SORT_NATURAL | SORT_FLAG_CASE); 302 303 $selectedTags = []; 304 $id = '__tagfilter_page_' . $opt['id']; 305 306 $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist 307 'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')', 308 'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''), 309 'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')), 310 'data-label' => hsc(utf8_strtolower(trim($label))), 311 ]; 312 if ($flags['multi']) { //unterscheidung ob Multiple oder Single 313 $attrs['multiple'] = 'multiple'; 314 $attrs['size'] = $this->getConf("DropDownList_size"); 315 } else { 316 $attrs['size'] = 1; 317 $pagetitles = array_reverse($pagetitles, true); 318 $pagetitles[''] = ''; 319 $pagetitles = array_reverse($pagetitles, true); 320 } 321 $form->addElement(form_makeListboxField($label, $pagetitles, $selectedTags, $label, $id, 'tagfilter', $attrs)); 322 } 323 $output .= '<script type="text/javascript">/*<![CDATA[*/ var tagfilter_container = {}; /*!]]>*/</script>' . "\n"; 324 //$output .= '<script type="text/javascript">/*<![CDATA[*/ '.'tagfilter_container.tagfilter_'.$opt['id'].' = '.json_encode($tagFilters['tags2']).'; /*!]]>*/</script>'."\n"; 325 foreach ($tagFilters['pagesPerMatchedTags'] as $key => $pagesPerMatchedTag) { 326 $id = false; 327 $label = $tagFilters['label'][$key]; 328 $selectedTags = $tagFilters['selectedTags'][$key]; 329 330 //get tag labels 331 $tags = []; 332 333 foreach (array_keys($pagesPerMatchedTag) as $tagid) { 334 $tags[$tagid] = $Htagfilter->getTagLabel($tagid); 335 } 336 337 foreach ($selectedTags as &$item) { 338 $item = utf8_strtolower(trim($item)); 339 } 340 unset($item); 341 342 343 $attrs = [//generelle Optionen für DropDownListe onchange->submit von id namespace und den flags für pagelist 344 'onChange' => 'tagfilter_submit(' . $opt['id'] . ',' . json_encode($opt['ns']) . ',' . json_encode([$opt['pagelistFlags'], $flags]) . ')', 345 'class' => 'tagdd_select tagfilter tagdd_select_' . $opt['id'] . ($flags['chosen'] ? ' chosen' : ''), 346 'data-placeholder' => hsc($label . ' ' . $this->getLang('choose')), 347 'data-label' => hsc(str_replace(' ', '_', utf8_strtolower(trim($label)))), 348 349 ]; 350 if ($flags['multi']) { //unterscheidung ob Multiple oder Single 351 $attrs['multiple'] = 'multiple'; 352 $attrs['size'] = $this->getConf("DropDownList_size"); 353 } else { 354 $attrs['size'] = 1; 355 $tags = array_reverse($tags, true); 356 $tags[''] = ''; 357 $tags = array_reverse($tags, true); 358 } 359 360 if ($flags['chosen']) { 361 $links = []; 362 foreach ($tags as $k => $t) { 363 $links[$k] = [ 364 'link' => $Htagfilter->getImageLinkByTag($k), 365 ]; 366 } 367 $jsVar = 'tagfilter_jsVar_' . rand(); 368 $output .= '<script type="text/javascript">/*<![CDATA[*/ tagfilter_container.' . $jsVar . ' =' 369 . json_encode($links) . 370 '; /*!]]>*/</script>' . "\n"; 371 372 $id = '__tagfilter_' . $opt["id"] . '_' . rand(); 373 374 if ($flags['tagimage']) { 375 $attrs['data-tagimage'] = $jsVar; 376 } 377 378 } 379 $form->addElement(form_makeListboxField($label, $tags, $selectedTags, $label, $id, 'tagfilter', $attrs)); 380 } 381 382 $form->addElement(form_makeButton('button', '', $this->getLang('Delete filter'), ['onclick' => 'tagfilter_cleanform(' . $opt['id'] . ',true)'])); 383 if ($flags['count']) { 384 $form->addElement('<div class="tagfilter_count">' . $this->getLang('found_count') . ': ' . '<span class="tagfilter_count_number"></span></div>'); 385 } 386 $form->endFieldset(); 387 $output .= $form->getForm();//Form Ausgeben 388 389 $output .= "<div id='tagfilter_ergebnis_" . $opt['id'] . "' class='tagfilter'>"; 390 //dbg($opt['pagelistFlags']); 391 $output .= $HtagfilterSyntax->renderList($preparedPages, $flags, $opt['pagelistFlags']); 392 $output .= "</div>"; 393 394 return $output; 395 } 396 397} 398