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