1<?php 2 3use dokuwiki\Form\Form; 4 5/** 6 * Tagging Plugin (hlper component) 7 * 8 * @license GPL 2 9 */ 10class helper_plugin_tagging extends DokuWiki_Plugin { 11 12 /** 13 * Gives access to the database 14 * 15 * Initializes the SQLite helper and register the CLEANTAG function 16 * 17 * @return helper_plugin_sqlite|bool false if initialization fails 18 */ 19 public function getDB() { 20 static $db = null; 21 if ($db !== null) { 22 return $db; 23 } 24 25 /** @var helper_plugin_sqlite $db */ 26 $db = plugin_load('helper', 'sqlite'); 27 if ($db === null) { 28 msg('The tagging plugin needs the sqlite plugin', -1); 29 30 return false; 31 } 32 $db->init('tagging', __DIR__ . '/db/'); 33 $db->create_function('CLEANTAG', array($this, 'cleanTag'), 1); 34 $db->create_function('GROUP_SORT', 35 function ($group, $newDelimiter) { 36 $ex = array_filter(explode(',', $group)); 37 sort($ex); 38 39 return implode($newDelimiter, $ex); 40 }, 2); 41 $db->create_function('GET_NS', 'getNS', 1); 42 43 return $db; 44 } 45 46 /** 47 * Return the user to use for accessing tags 48 * 49 * Handles the singleuser mode by returning 'auto' as user. Returnes false when no user is logged in. 50 * 51 * @return bool|string 52 */ 53 public function getUser() { 54 if (!isset($_SERVER['REMOTE_USER'])) { 55 return false; 56 } 57 if ($this->getConf('singleusermode')) { 58 return 'auto'; 59 } 60 61 return $_SERVER['REMOTE_USER']; 62 } 63 64 /** 65 * Canonicalizes the tag to its lower case nospace form 66 * 67 * @param $tag 68 * 69 * @return string 70 */ 71 public function cleanTag($tag) { 72 $tag = str_replace(array(' ', '-', '_'), '', $tag); 73 $tag = utf8_strtolower($tag); 74 75 return $tag; 76 } 77 78 /** 79 * Canonicalizes the namespace, remove the first colon and add glob 80 * 81 * @param $namespace 82 * 83 * @return string 84 */ 85 public function globNamespace($namespace) { 86 return cleanId($namespace) . '*'; 87 } 88 89 /** 90 * Create or Update tags of a page 91 * 92 * Uses the translation plugin to store the language of a page (if available) 93 * 94 * @param string $id The page ID 95 * @param string $user 96 * @param array $tags 97 * 98 * @return bool|SQLiteResult 99 */ 100 public function replaceTags($id, $user, $tags) { 101 global $conf; 102 /** @var helper_plugin_translation $trans */ 103 $trans = plugin_load('helper', 'translation'); 104 if ($trans) { 105 $lang = $trans->realLC($trans->getLangPart($id)); 106 } else { 107 $lang = $conf['lang']; 108 } 109 110 $db = $this->getDB(); 111 $db->query('BEGIN TRANSACTION'); 112 $queries = array(array('DELETE FROM taggings WHERE pid = ? AND tagger = ?', $id, $user)); 113 foreach ($tags as $tag) { 114 $queries[] = array('INSERT INTO taggings (pid, tagger, tag, lang) VALUES(?, ?, ?, ?)', $id, $user, $tag, $lang); 115 } 116 117 foreach ($queries as $query) { 118 if (!call_user_func_array(array($db, 'query'), $query)) { 119 $db->query('ROLLBACK TRANSACTION'); 120 121 return false; 122 } 123 } 124 125 return $db->query('COMMIT TRANSACTION'); 126 } 127 128 /** 129 * Get a list of Tags or Pages matching search criteria 130 * 131 * @param array $filter What to search for array('field' => 'searchterm') 132 * @param string $type What field to return 'tag'|'pid' 133 * @param int $limit Limit to this many results, 0 for all 134 * 135 * @return array associative array in form of value => count 136 */ 137 public function findItems($filter, $type, $limit = 0) { 138 139 global $INPUT; 140 141 /** @var helper_plugin_tagging_querybuilder $queryBuilder */ 142 $queryBuilder = new \helper_plugin_tagging_querybuilder(); 143 144 $queryBuilder->setField($type); 145 $queryBuilder->setLimit($limit); 146 $queryBuilder->setTags($this->extractFromQuery($filter)); 147 if (isset($filter['ns'])) $queryBuilder->includeNS($filter['ns']); 148 if (isset($filter['notns'])) $queryBuilder->excludeNS($filter['notns']); 149 if (isset($filter['tagger'])) $queryBuilder->setTagger($filter['tagger']); 150 if (isset($filter['pid'])) $queryBuilder->setPid($filter['pid']); 151 152 return $this->queryDb($queryBuilder->getQuery()); 153 154 } 155 156 /** 157 * Constructs the URL to search for a tag 158 * 159 * @param string $tag 160 * @param string $ns 161 * 162 * @return string 163 */ 164 public function getTagSearchURL($tag, $ns = '') { 165 // wrap tag in quotes if non clean 166 $ctag = utf8_stripspecials($this->cleanTag($tag)); 167 if ($ctag != utf8_strtolower($tag)) { 168 $tag = '"' . $tag . '"'; 169 } else { 170 $tag = "#$tag"; 171 } 172 173 $ret = '?do=search&sf=1&q=' . rawurlencode($tag); 174 if ($ns) { 175 $ret .= rawurlencode(' @' . $ns); 176 } 177 178 return $ret; 179 } 180 181 /** 182 * Calculates the size levels for the given list of clouds 183 * 184 * Automatically determines sensible tresholds 185 * 186 * @param array $tags list of tags => count 187 * @param int $levels 188 * 189 * @return mixed 190 */ 191 public function cloudData($tags, $levels = 10) { 192 $min = min($tags); 193 $max = max($tags); 194 195 // calculate tresholds 196 $tresholds = array(); 197 for ($i = 0; $i <= $levels; $i++) { 198 $tresholds[$i] = pow($max - $min + 1, $i / $levels) + $min - 1; 199 } 200 201 // assign weights 202 foreach ($tags as $tag => $cnt) { 203 foreach ($tresholds as $tresh => $val) { 204 if ($cnt <= $val) { 205 $tags[$tag] = $tresh; 206 break; 207 } 208 $tags[$tag] = $levels; 209 } 210 } 211 212 return $tags; 213 } 214 215 /** 216 * Display a tag cloud 217 * 218 * @param array $tags list of tags => count 219 * @param string $type 'tag' 220 * @param Callable $func The function to print the link (gets tag and ns) 221 * @param bool $wrap wrap cloud in UL tags? 222 * @param bool $return returnn HTML instead of printing? 223 * @param string $ns Add this namespace to search links 224 * 225 * @return string 226 */ 227 public function html_cloud($tags, $type, $func, $wrap = true, $return = false, $ns = '') { 228 global $INFO; 229 230 $hidden_str = $this->getConf('hiddenprefix'); 231 $hidden_len = strlen($hidden_str); 232 233 $ret = ''; 234 if ($wrap) { 235 $ret .= '<ul class="tagging_cloud clearfix">'; 236 } 237 if (count($tags) === 0) { 238 // Produce valid XHTML (ul needs a child) 239 $this->setupLocale(); 240 $ret .= '<li><div class="li">' . $this->lang['js']['no' . $type . 's'] . '</div></li>'; 241 } else { 242 $tags = $this->cloudData($tags); 243 foreach ($tags as $val => $size) { 244 // skip hidden tags for users that can't edit 245 if ($type === 'tag' and 246 $hidden_len and 247 substr($val, 0, $hidden_len) == $hidden_str and 248 !($this->getUser() && $INFO['writable']) 249 ) { 250 continue; 251 } 252 253 $ret .= '<li class="t' . $size . '"><div class="li">'; 254 $ret .= call_user_func($func, $val, $ns); 255 $ret .= '</div></li>'; 256 } 257 } 258 if ($wrap) { 259 $ret .= '</ul>'; 260 } 261 if ($return) { 262 return $ret; 263 } 264 echo $ret; 265 266 return ''; 267 } 268 269 /** 270 * Display a List of Page Links 271 * 272 * @param array $pids list of pids => count 273 * @return string 274 */ 275 public function html_page_list($pids) { 276 $ret = '<div class="search_quickresult">'; 277 $ret .= '<ul class="search_quickhits">'; 278 279 if (count($pids) === 0) { 280 // Produce valid XHTML (ul needs a child) 281 $ret .= '<li><div class="li">' . $this->lang['js']['nopages'] . '</div></li>'; 282 } else { 283 foreach (array_keys($pids) as $val) { 284 $ret .= '<li><div class="li">'; 285 $ret .= html_wikilink(":$val"); 286 $ret .= '</div></li>'; 287 } 288 } 289 290 $ret .= '</ul>'; 291 $ret .= '</div>'; 292 $ret .= '<div class="clearer"></div>'; 293 294 return $ret; 295 } 296 297 /** 298 * Get the link to a search for the given tag 299 * 300 * @param string $tag search for this tag 301 * @param string $ns limit search to this namespace 302 * 303 * @return string 304 */ 305 protected function linkToSearch($tag, $ns = '') { 306 return '<a href="' . hsc($this->getTagSearchURL($tag, $ns)) . '">' . $tag . '</a>'; 307 } 308 309 /** 310 * Display the Tags for the current page and prepare the tag editing form 311 * 312 * @param bool $print Should the HTML be printed or returned? 313 * 314 * @return string 315 */ 316 public function tpl_tags($print = true) { 317 global $INFO; 318 global $lang; 319 320 $filter = array('pid' => $INFO['id']); 321 if ($this->getConf('singleusermode')) { 322 $filter['tagger'] = 'auto'; 323 } 324 325 $tags = $this->findItems($filter, 'tag'); 326 327 $ret = ''; 328 329 $ret .= '<div class="plugin_tagging_edit">'; 330 $ret .= $this->html_cloud($tags, 'tag', array($this, 'linkToSearch'), true, true); 331 332 if ($this->getUser() && $INFO['writable']) { 333 $lang['btn_tagging_edit'] = $lang['btn_secedit']; 334 $ret .= '<div id="tagging__edit_buttons_group">'; 335 $ret .= html_btn('tagging_edit', $INFO['id'], '', array()); 336 if (auth_isadmin()) { 337 $ret .= '<label>' 338 . $this->getLang('toggle admin mode') 339 . '<input type="checkbox" id="tagging__edit_toggle_admin" /></label>'; 340 } 341 $ret .= '</div>'; 342 $form = new dokuwiki\Form\Form(); 343 $form->id('tagging__edit'); 344 $form->setHiddenField('tagging[id]', $INFO['id']); 345 $form->setHiddenField('call', 'plugin_tagging_save'); 346 $tags = $this->findItems(array( 347 'pid' => $INFO['id'], 348 'tagger' => $this->getUser(), 349 ), 'tag'); 350 $form->addTextarea('tagging[tags]') 351 ->val(implode(', ', array_keys($tags))) 352 ->addClass('edit') 353 ->attr('rows', 4); 354 $form->addButton('', $lang['btn_save'])->id('tagging__edit_save'); 355 $form->addButton('', $lang['btn_cancel'])->id('tagging__edit_cancel'); 356 $ret .= $form->toHTML(); 357 } 358 $ret .= '</div>'; 359 360 if ($print) { 361 echo $ret; 362 } 363 364 return $ret; 365 } 366 367 /** 368 * @param string $namespace empty for entire wiki 369 * 370 * @param string $order_by 371 * @param bool $desc 372 * @param array $filters 373 * @return array 374 */ 375 public function getAllTags($namespace = '', $order_by = 'tid', $desc = false, $filters = []) { 376 $order_fields = array('pid', 'tid', 'taggers', 'ns', 'count'); 377 if (!in_array($order_by, $order_fields)) { 378 msg('cannot sort by ' . $order_by . ' field does not exists', -1); 379 $order_by = 'tag'; 380 } 381 382 list($having, $params) = $this->getFilterSql($filters); 383 384 $db = $this->getDB(); 385 386 $query = 'SELECT "pid", 387 CLEANTAG("tag") AS "tid", 388 GROUP_SORT(GROUP_CONCAT("tagger"), \', \') AS "taggers", 389 GROUP_SORT(GROUP_CONCAT(GET_NS("pid")), \', \') AS "ns", 390 GROUP_SORT(GROUP_CONCAT("pid"), \', \') AS "pids", 391 COUNT(*) AS "count" 392 FROM "taggings" 393 WHERE "pid" GLOB ? AND GETACCESSLEVEL(pid) >= ' . AUTH_READ 394 . ' GROUP BY "tid"'; 395 $query .= $having; 396 $query .= 'ORDER BY ' . $order_by; 397 if ($desc) { 398 $query .= ' DESC'; 399 } 400 401 array_unshift($params, $this->globNamespace($namespace)); 402 $res = $db->query($query, $params); 403 404 return $db->res2arr($res); 405 } 406 407 /** 408 * Get all pages with tags and their tags 409 * 410 * @return array ['pid' => ['tag1','tag2','tag3']] 411 */ 412 public function getAllTagsByPage() { 413 $query = ' 414 SELECT pid, GROUP_CONCAT(tag) AS tags 415 FROM taggings 416 GROUP BY pid 417 '; 418 $db = $this->getDb(); 419 $res = $db->query($query); 420 return array_map( 421 function ($i) { 422 return explode(',', $i); 423 }, 424 array_column($db->res2arr($res), 'tags', 'pid') 425 ); 426 } 427 428 /** 429 * Renames a tag 430 * 431 * @param string $formerTagName 432 * @param string $newTagNames 433 */ 434 public function renameTag($formerTagName, $newTagNames) { 435 436 if (empty($formerTagName) || empty($newTagNames)) { 437 msg($this->getLang("admin enter tag names"), -1); 438 return; 439 } 440 441 $keepFormerTag = false; 442 443 // enable splitting tags on rename 444 $newTagNames = array_map(function ($tag) { 445 return $this->cleanTag($tag); 446 }, explode(',', $newTagNames)); 447 448 $db = $this->getDB(); 449 450 // non-admins can rename only their own tags 451 if (!auth_isadmin()) { 452 $queryTagger =' AND tagger = ?'; 453 $tagger = $this->getUser(); 454 } else { 455 $queryTagger = ''; 456 $tagger = ''; 457 } 458 459 $insertQuery = 'INSERT INTO taggings '; 460 $insertQuery .= 'SELECT pid, ?, tagger, lang FROM taggings'; 461 $where = ' WHERE CLEANTAG(tag) = ?'; 462 $where .= ' AND GETACCESSLEVEL(pid) >= ' . AUTH_EDIT; 463 $where .= $queryTagger; 464 465 $db->query('BEGIN TRANSACTION'); 466 467 // insert new tags first 468 foreach ($newTagNames as $newTag) { 469 if ($newTag === $this->cleanTag($formerTagName)) { 470 $keepFormerTag = true; 471 continue; 472 } 473 $params = [$newTag, $this->cleanTag($formerTagName)]; 474 if ($tagger) array_push($params, $tagger); 475 $res = $db->query($insertQuery . $where, $params); 476 if ($res === false) { 477 $db->query('ROLLBACK TRANSACTION'); 478 return; 479 } 480 $db->res_close($res); 481 } 482 483 // finally delete the renamed tags 484 if (!$keepFormerTag) { 485 $deleteQuery = 'DELETE FROM taggings'; 486 $params = [$this->cleanTag($formerTagName)]; 487 if ($tagger) array_push($params, $tagger); 488 if ($db->query($deleteQuery . $where, $params) === false) { 489 $db->query('ROLLBACK TRANSACTION'); 490 return; 491 } 492 } 493 494 $db->query('COMMIT TRANSACTION'); 495 496 msg($this->getLang("admin renamed"), 1); 497 498 return; 499 } 500 501 /** 502 * Rename or delete a tag for all users 503 * 504 * @param string $pid 505 * @param string $formerTagName 506 * @param string $newTagName 507 * 508 * @return array 509 */ 510 public function modifyPageTag($pid, $formerTagName, $newTagName) { 511 512 $db = $this->getDb(); 513 514 $res = $db->query( 515 'SELECT pid FROM taggings WHERE CLEANTAG(tag) = ? AND pid = ?', 516 $this->cleanTag($formerTagName), 517 $pid 518 ); 519 $check = $db->res2arr($res); 520 521 if (empty($check)) { 522 return array(true, $this->getLang('admin tag does not exists')); 523 } 524 525 if (empty($newTagName)) { 526 $res = $db->query( 527 'DELETE FROM taggings WHERE pid = ? AND CLEANTAG(tag) = ?', 528 $pid, 529 $this->cleanTag($formerTagName) 530 ); 531 } else { 532 $res = $db->query( 533 'UPDATE taggings SET tag = ? WHERE pid = ? AND CLEANTAG(tag) = ?', 534 $newTagName, 535 $pid, 536 $this->cleanTag($formerTagName) 537 ); 538 } 539 $db->res2arr($res); 540 541 return array(false, $this->getLang('admin renamed')); 542 } 543 544 /** 545 * Deletes a tag 546 * 547 * @param array $tags 548 * @param string $namespace current namespace context as in getAllTags() 549 */ 550 public function deleteTags($tags, $namespace = '') { 551 if (empty($tags)) { 552 return; 553 } 554 555 $namespace = cleanId($namespace); 556 557 $db = $this->getDB(); 558 559 $queryBody = 'FROM taggings WHERE pid GLOB ? AND (' . 560 implode(' OR ', array_fill(0, count($tags), 'CLEANTAG(tag) = ?')) . ')'; 561 $args = array_map(array($this, 'cleanTag'), $tags); 562 array_unshift($args, $this->globNamespace($namespace)); 563 564 // non-admins can delete only their own tags 565 if (!auth_isadmin()) { 566 $queryBody .= ' AND tagger = ?'; 567 array_push($args, $this->getUser()); 568 } 569 570 $affectedPagesQuery= 'SELECT DISTINCT pid ' . $queryBody; 571 $resAffectedPages = $db->query($affectedPagesQuery, $args); 572 $numAffectedPages = count($resAffectedPages->fetchAll()); 573 574 $deleteQuery = 'DELETE ' . $queryBody; 575 $db->query($deleteQuery, $args); 576 577 msg(sprintf($this->getLang("admin deleted"), count($tags), $numAffectedPages), 1); 578 } 579 580 /** 581 * Delete taggings of nonexistent pages 582 */ 583 public function deleteInvalidTaggings() 584 { 585 $db = $this->getDB(); 586 $query = 'DELETE FROM "taggings" 587 WHERE NOT PAGEEXISTS(pid) 588 '; 589 $res = $db->query($query); 590 $db->res_close($res); 591 } 592 593 /** 594 * Updates tags with a new page name 595 * 596 * @param string $oldName 597 * @param string $newName 598 */ 599 public function renamePage($oldName, $newName) { 600 $db = $this->getDB(); 601 $db->query('UPDATE taggings SET pid = ? WHERE pid = ?', $newName, $oldName); 602 } 603 604 /** 605 * Extracts tags from search query 606 * 607 * @param array $parsedQuery 608 * @return array 609 */ 610 public function extractFromQuery($parsedQuery) 611 { 612 $tags = []; 613 if (isset($parsedQuery['phrases'][0])) { 614 $tags = $parsedQuery['phrases']; 615 } elseif (isset($parsedQuery['and'][0])) { 616 $tags = $parsedQuery['and']; 617 } elseif (isset($parsedQuery['tag'])) { 618 // handle autocomplete call 619 $tags[] = $parsedQuery['tag']; 620 } 621 return $tags; 622 } 623 624 /** 625 * Search for tagged pages 626 * 627 * @param array $tagFiler 628 * @return array 629 */ 630 public function searchPages($tagFiler) 631 { 632 global $INPUT; 633 global $QUERY; 634 $parsedQuery = ft_queryParser(new Doku_Indexer(), $QUERY); 635 636 /** @var helper_plugin_tagging_querybuilder $queryBuilder */ 637 $queryBuilder = new \helper_plugin_tagging_querybuilder(); 638 639 $queryBuilder->setField('pid'); 640 $queryBuilder->setTags($tagFiler); 641 $queryBuilder->setLogicalAnd($INPUT->str('tagging-logic') === 'and'); 642 if (isset($parsedQuery['ns'])) $queryBuilder->includeNS($parsedQuery['ns']); 643 if (isset($parsedQuery['notns'])) $queryBuilder->excludeNS($parsedQuery['notns']); 644 if (isset($parsedQuery['tagger'])) $queryBuilder->setTagger($parsedQuery['tagger']); 645 if (isset($parsedQuery['pid'])) $queryBuilder->setPid($parsedQuery['pid']); 646 647 return $this->queryDb($queryBuilder->getPages()); 648 } 649 650 /** 651 * Syntax to allow users to manage tags on regular pages, respects ACLs 652 * @param string $ns 653 * @return string 654 */ 655 public function manageTags($ns) 656 { 657 global $INPUT; 658 659 $this->setDefaultSort(); 660 661 // initially set namespace filter to what is defined in syntax 662 if ($ns && !$INPUT->has('tagging__filters')) { 663 $INPUT->set('tagging__filters', ['ns' => $ns]); 664 } 665 666 return $this->html_table(); 667 } 668 669 /** 670 * HTML list of tagged pages 671 * 672 * @param string $tid 673 * @return string 674 */ 675 public function getPagesHtml($tid) 676 { 677 $html = ''; 678 679 $db = $this->getDB(); 680 $sql = 'SELECT pid from taggings where CLEANTAG(tag) = CLEANTAG(?)'; 681 $res = $db->query($sql, $tid); 682 $pages = $db->res2arr($res); 683 684 if ($pages) { 685 $html .= '<ul>'; 686 foreach ($pages as $page) { 687 $pid = $page['pid']; 688 $html .= '<li><a href="' . wl($pid) . '" target="_blank">' . $pid . '</li>'; 689 } 690 $html .= '</ul>'; 691 } 692 693 return $html; 694 } 695 696 /** 697 * Display tag management table 698 */ 699 public function html_table() { 700 global $ID, $INPUT; 701 702 $headers = array( 703 array('value' => $this->getLang('admin tag'), 'sort_by' => 'tid'), 704 array('value' => $this->getLang('admin occurrence'), 'sort_by' => 'count') 705 ); 706 707 if (!$this->conf['hidens']) { 708 array_push( 709 $headers, 710 ['value' => $this->getLang('admin namespaces'), 'sort_by' => 'ns'] 711 ); 712 } 713 714 array_push($headers, 715 array('value' => $this->getLang('admin taggers'), 'sort_by' => 'taggers'), 716 array('value' => $this->getLang('admin actions'), 'sort_by' => false) 717 ); 718 719 $sort = explode(',', $this->getParam('sort')); 720 $order_by = $sort[0]; 721 $desc = false; 722 if (isset($sort[1]) && $sort[1] === 'desc') { 723 $desc = true; 724 } 725 $filters = $INPUT->arr('tagging__filters'); 726 727 $tags = $this->getAllTags($INPUT->str('filter'), $order_by, $desc, $filters); 728 729 $form = new \dokuwiki\Form\Form(); 730 // required in admin mode 731 $form->setHiddenField('page', 'tagging'); 732 $form->setHiddenField('id', $ID); 733 $form->setHiddenField('[tagging]sort', $this->getParam('sort')); 734 735 /** 736 * Actions dialog 737 */ 738 $form->addTagOpen('div')->id('tagging__action-dialog')->attr('style', "display:none;"); 739 $form->addTagClose('div'); 740 741 /** 742 * Tag pages dialog 743 */ 744 $form->addTagOpen('div')->id('tagging__taggedpages-dialog')->attr('style', "display:none;"); 745 $form->addTagClose('div'); 746 747 /** 748 * Tag management table 749 */ 750 $form->addTagOpen('table')->addClass('inline plugin_tagging'); 751 752 $nscol = $this->conf['hidens'] ? '' : '<col class="wide-col"></col>'; 753 $form->addHTML( 754 '<colgroup> 755 <col></col> 756 <col class="narrow-col"></col>' 757 . $nscol . 758 '<col></col> 759 <col class="narrow-col"></col> 760 </colgroup>' 761 ); 762 763 /** 764 * Table headers 765 */ 766 $form->addTagOpen('tr'); 767 foreach ($headers as $header) { 768 $form->addTagOpen('th'); 769 if ($header['sort_by'] !== false) { 770 $param = $header['sort_by']; 771 $icon = 'arrow-both'; 772 $title = $this->getLang('admin sort ascending'); 773 if ($header['sort_by'] === $order_by) { 774 if ($desc === false) { 775 $icon = 'arrow-up'; 776 $title = $this->getLang('admin sort descending'); 777 $param .= ',desc'; 778 } else { 779 $icon = 'arrow-down'; 780 } 781 } 782 $form->addButtonHTML( 783 "tagging[sort]", 784 $header['value'] . ' ' . inlineSVG(__DIR__ . "/images/$icon.svg")) 785 ->addClass('plugin_tagging sort_button') 786 ->attr('title', $title) 787 ->val($param); 788 } else { 789 $form->addHTML($header['value']); 790 } 791 $form->addTagClose('th'); 792 } 793 $form->addTagClose('tr'); 794 795 /** 796 * Table filters for all sortable columns 797 */ 798 $form->addTagOpen('tr'); 799 foreach ($headers as $header) { 800 $form->addTagOpen('th'); 801 if ($header['sort_by'] !== false) { 802 $field = $header['sort_by']; 803 $input = $form->addTextInput("tagging__filters[$field]"); 804 $input->addClass('full-col'); 805 } 806 $form->addTagClose('th'); 807 } 808 $form->addTagClose('tr'); 809 810 811 foreach ($tags as $taginfo) { 812 $tagname = $taginfo['tid']; 813 $taggers = $taginfo['taggers']; 814 $ns = $taginfo['ns']; 815 $pids = explode(',',$taginfo['pids']); 816 817 $form->addTagOpen('tr'); 818 $form->addHTML('<td>'); 819 $form->addHTML('<a class="tagslist" href="#" data-tid="' . $taginfo['tid'] . '">'); 820 $form->addHTML( hsc($tagname) . '</a>'); 821 $form->addHTML('</td>'); 822 $form->addHTML('<td>' . $taginfo['count'] . '</td>'); 823 if (!$this->conf['hidens']) { 824 $form->addHTML('<td>' . hsc($ns) . '</td>'); 825 } 826 $form->addHTML('<td>' . hsc($taggers) . '</td>'); 827 828 /** 829 * action buttons 830 */ 831 $form->addHTML('<td>'); 832 833 // check ACLs 834 $userEdit = false; 835 /** @var \helper_plugin_sqlite $sqliteHelper */ 836 $sqliteHelper = plugin_load('helper', 'sqlite'); 837 foreach ($pids as $pid) { 838 if ($sqliteHelper->_getAccessLevel($pid) >= AUTH_EDIT) { 839 $userEdit = true; 840 continue; 841 } 842 } 843 844 if ($userEdit) { 845 $form->addButtonHTML( 846 'tagging[actions][rename][' . $taginfo['tid'] . ']', 847 inlineSVG(__DIR__ . '/images/edit.svg')) 848 ->addClass('plugin_tagging action_button') 849 ->attr('data-action', 'rename') 850 ->attr('data-tid', $taginfo['tid']); 851 $form->addButtonHTML( 852 'tagging[actions][delete][' . $taginfo['tid'] . ']', 853 inlineSVG(__DIR__ . '/images/delete.svg')) 854 ->addClass('plugin_tagging action_button') 855 ->attr('data-action', 'delete') 856 ->attr('data-tid', $taginfo['tid']); 857 } 858 859 $form->addHTML('</td>'); 860 $form->addTagClose('tr'); 861 } 862 863 $form->addTagClose('table'); 864 return '<div class="table">' . $form->toHTML() . '</div>'; 865 } 866 867 /** 868 * Display tag cleaner 869 * 870 * @return string 871 */ 872 public function html_clean() 873 { 874 $invalid = $this->getInvalidTaggings(); 875 876 if (!$invalid) { 877 return '<p><strong>' . $this->getLang('admin no invalid') . '</strong></p>'; 878 } 879 880 $form = new Form(); 881 $form->setHiddenField('do', 'admin'); 882 $form->setHiddenField('page', $this->getPluginName()); 883 $form->addButton('cmd[clean]', $this->getLang('admin clean')); 884 885 $html = $form->toHTML(); 886 887 $html .= '<div class="table"><table class="inline plugin_tagging">'; 888 $html .= '<thead><tr><th>' . 889 $this->getLang('admin nonexistent page') . 890 '</th><th>' . 891 $this->getLang('admin tags') . 892 '</th></tr></thead><tbody>'; 893 894 foreach ($invalid as $row) { 895 $html .= '<tr><td>' . $row['pid'] . '</td><td>' . $row['tags'] . '</td></tr>'; 896 } 897 898 $html .= '</tbody></table></div>'; 899 900 return $html; 901 } 902 903 /** 904 * Returns all tagging parameters from the query string 905 * 906 * @return mixed 907 */ 908 public function getParams() 909 { 910 global $INPUT; 911 return $INPUT->param('tagging', []); 912 } 913 914 /** 915 * Get a tagging parameter, empty string if not set 916 * 917 * @param string $name 918 * @return mixed 919 */ 920 public function getParam($name) 921 { 922 $params = $this->getParams(); 923 if ($params) { 924 return $params[$name] ?: ''; 925 } 926 } 927 928 /** 929 * Sets a tagging parameter 930 * 931 * @param string $name 932 * @param string|array $value 933 */ 934 public function setParam($name, $value) 935 { 936 global $INPUT; 937 $params = $this->getParams(); 938 $params = array_merge($params, [$name => $value]); 939 $INPUT->set('tagging', $params); 940 } 941 942 /** 943 * Default sorting by tag id 944 */ 945 public function setDefaultSort() 946 { 947 if (!$this->getParam('sort')) { 948 $this->setParam('sort', 'tid'); 949 } 950 } 951 952 /** 953 * Executes the query and returns the results as array 954 * 955 * @param array $query 956 * @return array 957 */ 958 protected function queryDb($query) 959 { 960 $db = $this->getDB(); 961 if (!$db) { 962 return []; 963 } 964 965 $res = $db->query($query[0], $query[1]); 966 $res = $db->res2arr($res); 967 968 $ret = []; 969 foreach ($res as $row) { 970 $ret[$row['item']] = $row['cnt']; 971 } 972 return $ret; 973 } 974 975 /** 976 * Construct the HAVING part of the search query 977 * 978 * @param array $filters 979 * @return array 980 */ 981 protected function getFilterSql($filters) 982 { 983 $having = ''; 984 $parts = []; 985 $params = []; 986 $filters = array_filter($filters); 987 if (!empty($filters)) { 988 $having = ' HAVING '; 989 foreach ($filters as $filter => $value) { 990 $parts[] = " $filter LIKE ? "; 991 $params[] = "%$value%"; 992 } 993 $having .= implode(' AND ', $parts); 994 } 995 return [$having, $params]; 996 } 997 998 /** 999 * Returns taggings of nonexistent pages 1000 * 1001 * @return array 1002 */ 1003 protected function getInvalidTaggings() 1004 { 1005 $db = $this->getDB(); 1006 $query = 'SELECT "pid", 1007 GROUP_CONCAT(CLEANTAG("tag")) AS "tags" 1008 FROM "taggings" 1009 WHERE NOT PAGEEXISTS(pid) 1010 GROUP BY pid 1011 '; 1012 $res = $db->query($query); 1013 return $db->res2arr($res); 1014 } 1015} 1016