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