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 } 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 extractFromQuery($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 $tagFiler 626 * @return array 627 */ 628 public function searchPages($tagFiler) 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 $queryBuilder->setTags($tagFiler); 639 $queryBuilder->setLogicalAnd($INPUT->str('tagging-logic') === 'and'); 640 if (isset($parsedQuery['ns'])) $queryBuilder->includeNS($parsedQuery['ns']); 641 if (isset($parsedQuery['notns'])) $queryBuilder->excludeNS($parsedQuery['notns']); 642 if (isset($parsedQuery['tagger'])) $queryBuilder->setTagger($parsedQuery['tagger']); 643 if (isset($parsedQuery['pid'])) $queryBuilder->setPid($parsedQuery['pid']); 644 645 return $this->queryDb($queryBuilder->getPages()); 646 } 647 648 /** 649 * Syntax to allow users to manage tags on regular pages, respects ACLs 650 * @param string $ns 651 * @return string 652 */ 653 public function manageTags($ns) 654 { 655 global $INPUT; 656 657 $this->setDefaultSort(); 658 659 // initially set namespace filter to what is defined in syntax 660 if ($ns && !$INPUT->has('tagging__filters')) { 661 $INPUT->set('tagging__filters', ['ns' => $ns]); 662 } 663 664 return $this->html_table(); 665 } 666 667 /** 668 * HTML list of tagged pages 669 * 670 * @param string $tid 671 * @return string 672 */ 673 public function getPagesHtml($tid) 674 { 675 $html = ''; 676 677 $db = $this->getDB(); 678 $sql = 'SELECT pid from taggings where CLEANTAG(tag) = CLEANTAG(?)'; 679 $res = $db->query($sql, $tid); 680 $pages = $db->res2arr($res); 681 682 if ($pages) { 683 $html .= '<ul>'; 684 foreach ($pages as $page) { 685 $pid = $page['pid']; 686 $html .= '<li><a href="' . wl($pid) . '" target="_blank">' . $pid . '</li>'; 687 } 688 $html .= '</ul>'; 689 } 690 691 return $html; 692 } 693 694 /** 695 * Display tag management table 696 */ 697 public function html_table() { 698 global $ID, $INPUT; 699 700 $headers = array( 701 array('value' => $this->getLang('admin tag'), 'sort_by' => 'tid'), 702 array('value' => $this->getLang('admin occurrence'), 'sort_by' => 'count') 703 ); 704 705 if (!$this->conf['hidens']) { 706 array_push( 707 $headers, 708 ['value' => $this->getLang('admin namespaces'), 'sort_by' => 'ns'] 709 ); 710 } 711 712 array_push($headers, 713 array('value' => $this->getLang('admin taggers'), 'sort_by' => 'taggers'), 714 array('value' => $this->getLang('admin actions'), 'sort_by' => false) 715 ); 716 717 $sort = explode(',', $this->getParam('sort')); 718 $order_by = $sort[0]; 719 $desc = false; 720 if (isset($sort[1]) && $sort[1] === 'desc') { 721 $desc = true; 722 } 723 $filters = $INPUT->arr('tagging__filters'); 724 725 $tags = $this->getAllTags($INPUT->str('filter'), $order_by, $desc, $filters); 726 727 $form = new \dokuwiki\Form\Form(); 728 // required in admin mode 729 $form->setHiddenField('page', 'tagging'); 730 $form->setHiddenField('id', $ID); 731 $form->setHiddenField('[tagging]sort', $this->getParam('sort')); 732 733 /** 734 * Actions dialog 735 */ 736 $form->addTagOpen('div')->id('tagging__action-dialog')->attr('style', "display:none;"); 737 $form->addTagClose('div'); 738 739 /** 740 * Tag pages dialog 741 */ 742 $form->addTagOpen('div')->id('tagging__taggedpages-dialog')->attr('style', "display:none;"); 743 $form->addTagClose('div'); 744 745 /** 746 * Tag management table 747 */ 748 $form->addTagOpen('table')->addClass('inline plugin_tagging'); 749 750 $nscol = $this->conf['hidens'] ? '' : '<col class="wide-col"></col>'; 751 $form->addHTML( 752 '<colgroup> 753 <col></col> 754 <col class="narrow-col"></col>' 755 . $nscol . 756 '<col></col> 757 <col class="narrow-col"></col> 758 </colgroup>' 759 ); 760 761 /** 762 * Table headers 763 */ 764 $form->addTagOpen('tr'); 765 foreach ($headers as $header) { 766 $form->addTagOpen('th'); 767 if ($header['sort_by'] !== false) { 768 $param = $header['sort_by']; 769 $icon = 'arrow-both'; 770 $title = $this->getLang('admin sort ascending'); 771 if ($header['sort_by'] === $order_by) { 772 if ($desc === false) { 773 $icon = 'arrow-up'; 774 $title = $this->getLang('admin sort descending'); 775 $param .= ',desc'; 776 } else { 777 $icon = 'arrow-down'; 778 } 779 } 780 $form->addButtonHTML( 781 "tagging[sort]", 782 $header['value'] . ' ' . inlineSVG(__DIR__ . "/images/$icon.svg")) 783 ->addClass('plugin_tagging sort_button') 784 ->attr('title', $title) 785 ->val($param); 786 } else { 787 $form->addHTML($header['value']); 788 } 789 $form->addTagClose('th'); 790 } 791 $form->addTagClose('tr'); 792 793 /** 794 * Table filters for all sortable columns 795 */ 796 $form->addTagOpen('tr'); 797 foreach ($headers as $header) { 798 $form->addTagOpen('th'); 799 if ($header['sort_by'] !== false) { 800 $field = $header['sort_by']; 801 $input = $form->addTextInput("tagging__filters[$field]"); 802 $input->addClass('full-col'); 803 } 804 $form->addTagClose('th'); 805 } 806 $form->addTagClose('tr'); 807 808 809 foreach ($tags as $taginfo) { 810 $tagname = $taginfo['tid']; 811 $taggers = $taginfo['taggers']; 812 $ns = $taginfo['ns']; 813 $pids = explode(',',$taginfo['pids']); 814 815 $form->addTagOpen('tr'); 816 $form->addHTML('<td>'); 817 $form->addHTML('<a class="tagslist" href="#" data-tid="' . $taginfo['tid'] . '">'); 818 $form->addHTML( hsc($tagname) . '</a>'); 819 $form->addHTML('</td>'); 820 $form->addHTML('<td>' . $taginfo['count'] . '</td>'); 821 if (!$this->conf['hidens']) { 822 $form->addHTML('<td>' . hsc($ns) . '</td>'); 823 } 824 $form->addHTML('<td>' . hsc($taggers) . '</td>'); 825 826 /** 827 * action buttons 828 */ 829 $form->addHTML('<td>'); 830 831 // check ACLs 832 $userEdit = false; 833 /** @var \helper_plugin_sqlite $sqliteHelper */ 834 $sqliteHelper = plugin_load('helper', 'sqlite'); 835 foreach ($pids as $pid) { 836 if ($sqliteHelper->_getAccessLevel($pid) >= AUTH_EDIT) { 837 $userEdit = true; 838 continue; 839 } 840 } 841 842 if ($userEdit) { 843 $form->addButtonHTML( 844 'tagging[actions][rename][' . $taginfo['tid'] . ']', 845 inlineSVG(__DIR__ . '/images/edit.svg')) 846 ->addClass('plugin_tagging action_button') 847 ->attr('data-action', 'rename') 848 ->attr('data-tid', $taginfo['tid']); 849 $form->addButtonHTML( 850 'tagging[actions][delete][' . $taginfo['tid'] . ']', 851 inlineSVG(__DIR__ . '/images/delete.svg')) 852 ->addClass('plugin_tagging action_button') 853 ->attr('data-action', 'delete') 854 ->attr('data-tid', $taginfo['tid']); 855 } 856 857 $form->addHTML('</td>'); 858 $form->addTagClose('tr'); 859 } 860 861 $form->addTagClose('table'); 862 return '<div class="table">' . $form->toHTML() . '</div>'; 863 } 864 865 /** 866 * Display tag cleaner 867 * 868 * @return string 869 */ 870 public function html_clean() 871 { 872 $invalid = $this->getInvalidTaggings(); 873 874 if (!$invalid) { 875 return '<p><strong>' . $this->getLang('admin no invalid') . '</strong></p>'; 876 } 877 878 $form = new Form(); 879 $form->setHiddenField('do', 'admin'); 880 $form->setHiddenField('page', $this->getPluginName()); 881 $form->addButton('cmd[clean]', $this->getLang('admin clean')); 882 883 $html = $form->toHTML(); 884 885 $html .= '<div class="table"><table class="inline plugin_tagging">'; 886 $html .= '<thead><tr><th>' . 887 $this->getLang('admin nonexistent page') . 888 '</th><th>' . 889 $this->getLang('admin tags') . 890 '</th></tr></thead><tbody>'; 891 892 foreach ($invalid as $row) { 893 $html .= '<tr><td>' . $row['pid'] . '</td><td>' . $row['tags'] . '</td></tr>'; 894 } 895 896 $html .= '</tbody></table></div>'; 897 898 return $html; 899 } 900 901 /** 902 * Returns all tagging parameters from the query string 903 * 904 * @return mixed 905 */ 906 public function getParams() 907 { 908 global $INPUT; 909 return $INPUT->param('tagging', []); 910 } 911 912 /** 913 * Get a tagging parameter, empty string if not set 914 * 915 * @param string $name 916 * @return mixed 917 */ 918 public function getParam($name) 919 { 920 $params = $this->getParams(); 921 if ($params) { 922 return $params[$name] ?: ''; 923 } 924 } 925 926 /** 927 * Sets a tagging parameter 928 * 929 * @param string $name 930 * @param string|array $value 931 */ 932 public function setParam($name, $value) 933 { 934 global $INPUT; 935 $params = $this->getParams(); 936 $params = array_merge($params, [$name => $value]); 937 $INPUT->set('tagging', $params); 938 } 939 940 /** 941 * Default sorting by tag id 942 */ 943 public function setDefaultSort() 944 { 945 if (!$this->getParam('sort')) { 946 $this->setParam('sort', 'tid'); 947 } 948 } 949 950 /** 951 * Executes the query and returns the results as array 952 * 953 * @param array $query 954 * @return array 955 */ 956 protected function queryDb($query) 957 { 958 $db = $this->getDB(); 959 if (!$db) { 960 return []; 961 } 962 963 $res = $db->query($query[0], $query[1]); 964 $res = $db->res2arr($res); 965 966 $ret = []; 967 foreach ($res as $row) { 968 $ret[$row['item']] = $row['cnt']; 969 } 970 return $ret; 971 } 972 973 /** 974 * Construct the HAVING part of the search query 975 * 976 * @param array $filters 977 * @return array 978 */ 979 protected function getFilterSql($filters) 980 { 981 $having = ''; 982 $parts = []; 983 $params = []; 984 $filters = array_filter($filters); 985 if (!empty($filters)) { 986 $having = ' HAVING '; 987 foreach ($filters as $filter => $value) { 988 $parts[] = " $filter LIKE ? "; 989 $params[] = "%$value%"; 990 } 991 $having .= implode(' AND ', $parts); 992 } 993 return [$having, $params]; 994 } 995 996 /** 997 * Returns taggings of nonexistent pages 998 * 999 * @return array 1000 */ 1001 protected function getInvalidTaggings() 1002 { 1003 $db = $this->getDB(); 1004 $query = 'SELECT "pid", 1005 GROUP_CONCAT(CLEANTAG("tag")) AS "tags" 1006 FROM "taggings" 1007 WHERE NOT PAGEEXISTS(pid) 1008 GROUP BY pid 1009 '; 1010 $res = $db->query($query); 1011 return $db->res2arr($res); 1012 } 1013} 1014