1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Esther Brunner <wikidesign@gmail.com> 5 */ 6 7/** 8 * Helper part of the tag plugin, allows to query and print tags 9 */ 10class helper_plugin_tag extends DokuWiki_Plugin { 11 12 var $namespace = ''; // namespace tag links point to 13 14 var $sort = ''; // sort key 15 var $topic_idx = array(); 16 17 /** 18 * Constructor gets default preferences and language strings 19 */ 20 function __construct() { 21 global $ID; 22 23 $this->namespace = $this->getConf('namespace'); 24 if (!$this->namespace) $this->namespace = getNS($ID); 25 $this->sort = $this->getConf('sortkey'); 26 } 27 28 /** 29 * Returns some documentation of the methods provided by this helper part 30 * 31 * @return array Method description 32 */ 33 function getMethods() { 34 $result = array(); 35 $result[] = array( 36 'name' => 'th', 37 'desc' => 'returns the header for the tags column for pagelist', 38 'return' => array('header' => 'string'), 39 ); 40 $result[] = array( 41 'name' => 'td', 42 'desc' => 'returns the tag links of a given page', 43 'params' => array('id' => 'string'), 44 'return' => array('links' => 'string'), 45 ); 46 $result[] = array( 47 'name' => 'tagLinks', 48 'desc' => 'generates tag links for given words', 49 'params' => array('tags' => 'array'), 50 'return' => array('links' => 'string'), 51 ); 52 $result[] = array( 53 'name' => 'getTopic', 54 'desc' => 'returns a list of pages tagged with the given keyword', 55 'params' => array( 56 'namespace (optional)' => 'string', 57 'number (not used)' => 'integer', 58 'tag (required)' => 'string'), 59 'return' => array('pages' => 'array'), 60 ); 61 $result[] = array( 62 'name' => 'tagRefine', 63 'desc' => 'refines an array of pages with tags', 64 'params' => array( 65 'pages to refine' => 'array', 66 'refinement tags' => 'string'), 67 'return' => array('pages' => 'array'), 68 ); 69 $result[] = array( 70 'name' => 'tagOccurrences', 71 'desc' => 'returns a list of tags with their number of occurrences', 72 'params' => array( 73 'list of tags to get the occurrences for' => 'array', 74 'namespaces to which the search shall be restricted' => 'array', 75 'if all tags shall be returned (then the first parameter is ignored)' => 'boolean', 76 'if the namespaces shall be searched recursively' => 'boolean'), 77 'return' => array('tags' => 'array'), 78 ); 79 return $result; 80 } 81 82 /** 83 * Returns the column header for th Pagelist Plugin 84 */ 85 function th() { 86 return $this->getLang('tags'); 87 } 88 89 /** 90 * Returns the cell data for the Pagelist Plugin 91 */ 92 function td($id) { 93 $subject = $this->_getSubjectMetadata($id); 94 return $this->tagLinks($subject); 95 } 96 97 /** 98 * Returns the links for given tags 99 * 100 * @param array $tags an array of tags 101 * @return string HTML link tags 102 */ 103 function tagLinks($tags) { 104 if (empty($tags) || ($tags[0] == '')) return ''; 105 106 $links = array(); 107 foreach ($tags as $tag) { 108 $links[] = $this->tagLink($tag); 109 } 110 return implode(','.DOKU_LF.DOKU_TAB, $links); 111 } 112 113 /** 114 * Returns the link for one given tag 115 * 116 * @param string $tag the tag the link shall point to 117 * @param string $title the title of the link (optional) 118 * @param bool $dynamic if the link class shall be changed if no pages with the specified tag exist 119 * @return string The HTML code of the link 120 */ 121 function tagLink($tag, $title = '', $dynamic = false) { 122 global $conf; 123 $svtag = $tag; 124 $tag_title = str_replace('_', ' ', noNS($tag)); 125 resolve_pageid($this->namespace, $tag, $exists); // resolve shortcuts 126 if ($exists) { 127 $class = 'wikilink1'; 128 $url = wl($tag); 129 if ($conf['useheading']) { 130 // important: set sendond param to false to prevent recursion! 131 $heading = p_get_first_heading($tag, false); 132 if ($heading) $tag_title = $heading; 133 } 134 } else { 135 if ($dynamic) { 136 $pages = $this->getTopic('', 1, $svtag); 137 if (empty($pages)) { 138 $class = 'wikilink2'; 139 } else { 140 $class = 'wikilink1'; 141 } 142 } else { 143 $class = 'wikilink1'; 144 } 145 $url = wl($tag, array('do'=>'showtag', 'tag'=>$svtag)); 146 } 147 if (!$title) $title = $tag_title; 148 $link = array( 149 'href' => $url, 150 'class' => $class, 151 'tooltip' => hsc($tag), 152 'title' => hsc($title) 153 ); 154 trigger_event('PLUGIN_TAG_LINK', $link); 155 $link = '<a href="'.$link['href'].'" class="'.$link['class'].'" title="'.$link['tooltip'].'" rel="tag">'.$link['title'].'</a>'; 156 return $link; 157 } 158 159 /** 160 * Returns a list of pages with a certain tag; very similar to ft_backlinks() 161 * 162 * @param string $ns A namespace to which all pages need to belong, "." for only the root namespace 163 * @param int $num The maximum number of pages that shall be returned 164 * @param string $tag The tag that shall be searched 165 * @return array The list of pages 166 * 167 * @author Esther Brunner <wikidesign@gmail.com> 168 */ 169 function getTopic($ns = '', $num = NULL, $tag = '') { 170 if (!$tag) $tag = $_REQUEST['tag']; 171 $tag = $this->_parseTagList($tag, true); 172 $result = array(); 173 174 // find the pages using topic.idx 175 $pages = $this->_tagIndexLookup($tag); 176 if (!count($pages)) return $result; 177 178 foreach ($pages as $page) { 179 // exclude pages depending on ACL and namespace 180 if($this->_notVisible($page, $ns)) continue; 181 $tags = $this->_getSubjectMetadata($page); 182 // don't trust index 183 if (!$this->_checkPageTags($tags, $tag)) continue; 184 185 // get metadata 186 $meta = p_get_metadata($page); 187 188 $perm = auth_quickaclcheck($page); 189 190 // skip drafts unless for users with create privilege 191 $draft = ($meta['type'] == 'draft'); 192 if ($draft && ($perm < AUTH_CREATE)) continue; 193 194 $title = $meta['title']; 195 $date = ($this->sort == 'mdate' ? $meta['date']['modified'] : $meta['date']['created'] ); 196 $taglinks = $this->tagLinks($tags); 197 198 // determine the sort key 199 if ($this->sort == 'id') $key = $page; 200 elseif ($this->sort == 'ns') { 201 $pos = strrpos($page, ':'); 202 if ($pos === false) $key = "\0".$page; 203 else $key = substr_replace($page, "\0\0", $pos, 1); 204 $key = str_replace(':', "\0", $key); 205 } elseif ($this->sort == 'pagename') $key = noNS($page); 206 elseif ($this->sort == 'title') { 207 $key = utf8_strtolower($title); 208 if (empty($key)) $key = str_replace('_', ' ', noNS($page)); 209 } else $key = $date; 210 // make sure that the key is unique 211 $key = $this->_uniqueKey($key, $result); 212 213 $result[$key] = array( 214 'id' => $page, 215 'title' => $title, 216 'date' => $date, 217 'user' => $meta['creator'], 218 'desc' => $meta['description']['abstract'], 219 'cat' => $tags[0], 220 'tags' => $taglinks, 221 'perm' => $perm, 222 'exists' => true, 223 'draft' => $draft, ); 224 225 if ($num && count($result) >= $num) break; 226 } 227 228 // finally sort by sort key 229 if ($this->getConf('sortorder') == 'ascending') ksort($result); 230 else krsort($result); 231 232 return $result; 233 } 234 235 /** 236 * Refine found pages with tags (+tag: AND, -tag: (AND) NOT) 237 * 238 * @param array $pages The pages that shall be filtered, each page needs to be an array with a key "id" 239 * @param string $refine The list of tags in the form "tag +tag2 -tag3". The tags will be cleaned. 240 * @return array The filtered list of pages 241 */ 242 function tagRefine($pages, $refine) { 243 if (!is_array($pages)) return $pages; // wrong data type 244 $tags = $this->_parseTagList($refine, true); 245 $all_pages = $this->_tagIndexLookup($tags); 246 247 foreach ($pages as $key => $page) { 248 if (!in_array($page['id'], $all_pages)) unset($pages[$key]); 249 } 250 251 return $pages; 252 } 253 254 /** 255 * Get count of occurrences for a list of tags 256 * 257 * @param array $tags array of tags 258 * @param array $namespaces array of namespaces where to count the tags 259 * @param boolean $allTags boolean if all available tags should be counted 260 * @param boolean $recursive boolean if pages in subnamespaces are allowed 261 * @return array 262 */ 263 function tagOccurrences($tags, $namespaces = NULL, $allTags = false, $recursive = NULL) { 264 // map with trim here in order to remove newlines from tags 265 if($allTags) $tags = array_map('trim', idx_getIndex('subject', '_w')); 266 $tags = $this->_cleanTagList($tags); 267 $otags = array(); //occurrences 268 if(!$namespaces || $namespaces[0] == '' || !is_array($namespaces)) $namespaces = NULL; // $namespaces not specified 269 270 $indexer = idx_get_indexer(); 271 $indexer_pages = $indexer->lookupKey('subject', $tags, array($this, '_tagCompare')); 272 273 $root_allowed = ($namespaces == NULL ? false : in_array('.', $namespaces)); 274 if ($recursive === NULL) 275 $recursive = $this->getConf('list_tags_of_subns'); 276 277 foreach ($tags as $tag) { 278 if (!isset($indexer_pages[$tag])) continue; 279 280 // just to be sure remove duplicate pages from the list of pages 281 $pages = array_unique($indexer_pages[$tag]); 282 283 // don't count hidden pages or pages the user can't access 284 // for performance reasons this doesn't take drafts into account 285 $pages = array_filter($pages, array($this, '_isVisible')); 286 287 if (empty($pages)) continue; 288 289 if ($namespaces == NULL || ($root_allowed && $recursive)) { 290 // count all pages 291 $otags[$tag] = count($pages); 292 } else if (!$recursive) { 293 // filter by exact namespace 294 $otags[$tag] = 0; 295 foreach ($pages as $page) { 296 $ns = getNS($page); 297 if (($ns == false && $root_allowed) || in_array($ns, $namespaces)) $otags[$tag]++; 298 } 299 } else { // recursive, no root 300 $otags[$tag] = 0; 301 foreach ($pages as $page) { 302 foreach ($namespaces as $ns) { 303 if(strpos($page, $ns.':') === 0 ) { 304 $otags[$tag]++ ; 305 break; 306 } 307 } 308 } 309 } 310 // don't return tags without pages 311 if ($otags[$tag] == 0) unset($otags[$tag]); 312 } 313 return $otags; 314 } 315 316 /** 317 * Get the subject metadata cleaning the result 318 * 319 * @param string $id the page id 320 * @return array 321 */ 322 function _getSubjectMetadata($id){ 323 $tags = p_get_metadata($id, 'subject'); 324 if (!is_array($tags)) $tags = explode(' ', $tags); 325 return array_unique($tags); 326 } 327 328 /** 329 * Tag index lookup 330 * 331 * @param array $tags the tags to filter 332 * @return array the matching page ids 333 */ 334 function _tagIndexLookup($tags) { 335 $result = array(); // array of page ids 336 337 $clean_tags = array(); 338 foreach ($tags as $i => $tag) { 339 if (($tag[0] == '+') || ($tag[0] == '-')) 340 $clean_tags[$i] = substr($tag, 1); 341 else 342 $clean_tags[$i] = $tag; 343 } 344 345 $indexer = idx_get_indexer(); 346 $pages = $indexer->lookupKey('subject', $clean_tags, array($this, '_tagCompare')); 347 // use all pages as basis if the first tag isn't an "or"-tag or if there are no tags given 348 if (empty($tags) || $clean_tags[0] != $tags[0]) $result = $indexer->getPages(); 349 350 foreach ($tags as $i => $tag) { 351 $t = $clean_tags[$i]; 352 if (!is_array($pages[$t])) $pages[$t] = array(); 353 354 if ($tag[0] == '+') { // AND: add only if in both arrays 355 $result = array_intersect($result, $pages[$t]); 356 } elseif ($tag[0] == '-') { // NOT: remove array from docs 357 $result = array_diff($result, $pages[$t]); 358 } else { // OR: add array to docs 359 $result = array_unique(array_merge($result, $pages[$t])); 360 } 361 } 362 363 return $result; 364 } 365 366 367 /** 368 * Splits a string into an array of tags 369 */ 370 function _parseTagList($tags, $clean = false) { 371 372 // support for "quoted phrase tags" 373 if (preg_match_all('#".*?"#', $tags, $matches)) { 374 foreach ($matches[0] as $match) { 375 $replace = str_replace(' ', '_', substr($match, 1, -1)); 376 $tags = str_replace($match, $replace, $tags); 377 } 378 } 379 380 $tags = preg_split('/ /', $tags, -1, PREG_SPLIT_NO_EMPTY); 381 382 if ($clean) { 383 return $this->_cleanTagList($tags); 384 } else { 385 return $tags; 386 } 387 } 388 389 /** 390 * Clean a list (array) of tags using _cleanTag 391 */ 392 function _cleanTagList($tags) { 393 return array_unique(array_map(array($this, '_cleanTag'), $tags)); 394 } 395 396 /** 397 * Cleans a tag using cleanID while preserving a possible prefix of + or - 398 */ 399 function _cleanTag($tag) { 400 $prefix = substr($tag, 0, 1); 401 $tag = $this->_applyMacro($tag); 402 if ($prefix === '-' || $prefix === '+') { 403 return $prefix.cleanID($tag); 404 } else { 405 return cleanID($tag); 406 } 407 } 408 409 /** 410 * Makes user or date dependent topic lists possible 411 */ 412 function _applyMacro($id) { 413 /** @var DokuWiki_Auth_Plugin $auth */ 414 global $INFO, $auth; 415 416 $user = $_SERVER['REMOTE_USER']; 417 $group = ''; 418 // .htaccess auth doesn't provide the auth object 419 if($auth) { 420 $userdata = $auth->getUserData($user); 421 $group = $userdata['grps'][0]; 422 } 423 424 $replace = array( 425 '@USER@' => cleanID($user), 426 '@NAME@' => cleanID($INFO['userinfo']['name']), 427 '@GROUP@' => cleanID($group), 428 '@YEAR@' => date('Y'), 429 '@MONTH@' => date('m'), 430 '@DAY@' => date('d'), 431 ); 432 return str_replace(array_keys($replace), array_values($replace), $id); 433 } 434 435 /** 436 * Non-recursive function to check whether an array key is unique 437 * 438 * @author Esther Brunner <wikidesign@gmail.com> 439 * @author Ilya S. Lebedev <ilya@lebedev.net> 440 */ 441 function _uniqueKey($key, &$result) { 442 443 // increase numeric keys by one 444 if (is_numeric($key)) { 445 while (array_key_exists($key, $result)) $key++; 446 return $key; 447 448 // append a number to literal keys 449 } else { 450 $num = 0; 451 $testkey = $key; 452 while (array_key_exists($testkey, $result)) { 453 $testkey = $key.$num; 454 $num++; 455 } 456 return $testkey; 457 } 458 } 459 460 /** 461 * Opposite of _notVisible 462 */ 463 function _isVisible($id, $ns='') { 464 return !$this->_notVisible($id, $ns); 465 } 466 /** 467 * Check visibility of the page 468 * 469 * @param string $id the page id 470 * @param string $ns the namespace authorized 471 * @return bool if the page is hidden 472 */ 473 function _notVisible($id, $ns="") { 474 if (isHiddenPage($id)) return true; // discard hidden pages 475 // discard if user can't read 476 if (auth_quickaclcheck($id) < AUTH_READ) return true; 477 // filter by namespace, root namespace is identified with a dot 478 if($ns == '.') { 479 // root namespace is specified, discard all pages who lay outside the root namespace 480 if(getNS($id) != false) return true; 481 } else { 482 // ("!==0" namespace found at position 0) 483 if ($ns && (strpos(':'.getNS($id).':', ':'.$ns.':') !== 0)) return true; 484 } 485 return !page_exists($id, '', false); 486 } 487 488 /** 489 * Helper function for the indexer in order to avoid interpreting wildcards 490 */ 491 function _tagCompare($tag1, $tag2) { 492 return $tag1 === $tag2; 493 } 494 495 /** 496 * Check if the page is a real candidate for the result of the getTopic 497 * 498 * @param array $pagetags tags on the metadata of the page 499 * @param array $tags tags we are looking 500 * @return bool 501 */ 502 function _checkPageTags($pagetags, $tags) { 503 $result = false; 504 foreach($tags as $tag) { 505 if ($tag[0] == "+" and !in_array(substr($tag, 1), $pagetags)) $result = false; 506 if ($tag[0] == "-" and in_array(substr($tag, 1), $pagetags)) $result = false; 507 if (in_array($tag, $pagetags)) $result = true; 508 } 509 return $result; 510 } 511 512} 513// vim:ts=4:sw=4:et: 514