1<?php 2 3if (!defined('DOKU_INC')) { 4 die(); 5} 6 7class helper_plugin_tagging extends DokuWiki_Plugin { 8 9 /** 10 * Gives access to the database 11 * 12 * Initializes the SQLite helper and register the CLEANTAG function 13 * 14 * @return helper_plugin_sqlite|bool false if initialization fails 15 */ 16 public function getDB() { 17 static $db = null; 18 if (!is_null($db)) { 19 return $db; 20 } 21 22 /** @var helper_plugin_sqlite $db */ 23 $db = plugin_load('helper', 'sqlite'); 24 if (is_null($db)) { 25 msg('The tagging plugin needs the sqlite plugin', -1); 26 return false; 27 } 28 $db->init('tagging', dirname(__FILE__) . '/db/'); 29 $db->create_function('CLEANTAG', array($this, 'cleanTag'), 1); 30 return $db; 31 } 32 33 /** 34 * Return the user to use for accessing tags 35 * 36 * Handles the singleuser mode by returning 'auto' as user. Returnes false when no user is logged in. 37 * 38 * @return bool|string 39 */ 40 public function getUser() { 41 if (!isset($_SERVER['REMOTE_USER'])) { 42 return false; 43 } 44 if ($this->getConf('singleusermode')) { 45 return 'auto'; 46 } 47 return $_SERVER['REMOTE_USER']; 48 } 49 50 /** 51 * Canonicalizes the tag to its lower case nospace form 52 * 53 * @param $tag 54 * 55 * @return string 56 */ 57 public function cleanTag($tag) { 58 $tag = str_replace(' ', '', $tag); 59 $tag = str_replace('-', '', $tag); 60 $tag = str_replace('_', '', $tag); 61 $tag = utf8_strtolower($tag); 62 return $tag; 63 } 64 65 /** 66 * Create or Update tags of a page 67 * 68 * Uses the translation plugin to store the language of a page (if available) 69 * 70 * @param string $id The page ID 71 * @param string $user 72 * @param array $tags 73 * 74 * @return bool|SQLiteResult 75 */ 76 public function replaceTags($id, $user, $tags) { 77 global $conf; 78 /** @var helper_plugin_translation $trans */ 79 $trans = plugin_load('helper', 'translation'); 80 if ($trans) { 81 $lang = $trans->realLC($trans->getLangPart($id)); 82 } else { 83 $lang = $conf['lang']; 84 } 85 86 $db = $this->getDB(); 87 $db->query('BEGIN TRANSACTION'); 88 $queries = array(array('DELETE FROM taggings WHERE pid = ? AND tagger = ?', $id, $user)); 89 foreach ($tags as $tag) { 90 $queries[] = array('INSERT INTO taggings (pid, tagger, tag, lang) VALUES(?, ?, ?, ?)', $id, $user, $tag, $lang); 91 } 92 93 foreach ($queries as $query) { 94 if (!call_user_func_array(array($db, 'query'), $query)) { 95 $db->query('ROLLBACK TRANSACTION'); 96 return false; 97 } 98 } 99 return $db->query('COMMIT TRANSACTION'); 100 } 101 102 /** 103 * Get a list of Tags or Pages matching search criteria 104 * 105 * @param array $filter What to search for array('field' => 'searchterm') 106 * @param string $type What field to return 'tag'|'pid' 107 * @param int $limit Limit to this many results, 0 for all 108 * 109 * @return array associative array in form of value => count 110 */ 111 public function findItems($filter, $type, $limit = 0) { 112 $db = $this->getDB(); 113 if (!$db) { 114 return array(); 115 } 116 117 // create WHERE clause 118 $where = '1=1'; 119 foreach ($filter as $field => $value) { 120 // compare clean tags only 121 if ($field === 'tag') { 122 $field = 'CLEANTAG(tag)'; 123 $q = 'CLEANTAG(?)'; 124 } else { 125 $q = '?'; 126 } 127 128 if (substr($field, 0, 6) === 'notpid') { 129 $field = 'pid'; 130 131 // detect LIKE filters 132 if ($this->useLike($value)) { 133 $where .= " AND $field NOT LIKE $q"; 134 } else { 135 $where .= " AND $field != $q"; 136 } 137 } else { 138 // detect LIKE filters 139 if ($this->useLike($value)) { 140 $where .= " AND $field LIKE $q"; 141 } else { 142 $where .= " AND $field = $q"; 143 } 144 } 145 } 146 $where .= ' AND GETACCESSLEVEL(pid) >= ' . AUTH_READ; 147 148 // group and order 149 if ($type == 'tag') { 150 $groupby = 'CLEANTAG(tag)'; 151 $orderby = 'CLEANTAG(tag)'; 152 } else { 153 $groupby = $type; 154 $orderby = "cnt DESC, $type"; 155 } 156 157 // limit results 158 if ($limit) { 159 $limit = " LIMIT $limit"; 160 } else { 161 $limit = ''; 162 } 163 164 // create SQL 165 $sql = "SELECT $type AS item, COUNT(*) AS cnt 166 FROM taggings 167 WHERE $where 168 GROUP BY $groupby 169 ORDER BY $orderby 170 $limit 171 "; 172 173 // run query and turn into associative array 174 $res = $db->query($sql, array_values($filter)); 175 $res = $db->res2arr($res); 176 177 $ret = array(); 178 foreach ($res as $row) { 179 $ret[$row['item']] = $row['cnt']; 180 } 181 return $ret; 182 } 183 184 /** 185 * Check if the given string is a LIKE statement 186 * 187 * @param string $value 188 * 189 * @return bool 190 */ 191 private function useLike($value) { 192 return strpos($value, '%') === 0 || strrpos($value, '%') === strlen($value) - 1; 193 } 194 195 /** 196 * Constructs the URL to search for a tag 197 * 198 * @param string $tag 199 * @param string $ns 200 * 201 * @return string 202 */ 203 public function getTagSearchURL($tag, $ns = '') { 204 // wrap tag in quotes if non clean 205 $ctag = utf8_stripspecials($this->cleanTag($tag)); 206 if ($ctag != utf8_strtolower($tag)) { 207 $tag = '"' . $tag . '"'; 208 } 209 210 $ret = '?do=search&id=' . rawurlencode($tag); 211 if ($ns) { 212 $ret .= rawurlencode(' @' . $ns); 213 } 214 215 return $ret; 216 } 217 218 /** 219 * Calculates the size levels for the given list of clouds 220 * 221 * Automatically determines sensible tresholds 222 * 223 * @param array $tags list of tags => count 224 * @param int $levels 225 * 226 * @return mixed 227 */ 228 public function cloudData($tags, $levels = 10) { 229 $min = min($tags); 230 $max = max($tags); 231 232 // calculate tresholds 233 $tresholds = array(); 234 for ($i = 0; $i <= $levels; $i++) { 235 $tresholds[$i] = pow($max - $min + 1, $i / $levels) + $min - 1; 236 } 237 238 // assign weights 239 foreach ($tags as $tag => $cnt) { 240 foreach ($tresholds as $tresh => $val) { 241 if ($cnt <= $val) { 242 $tags[$tag] = $tresh; 243 break; 244 } 245 $tags[$tag] = $levels; 246 } 247 } 248 return $tags; 249 } 250 251 /** 252 * Display a tag cloud 253 * 254 * @param array $tags list of tags => count 255 * @param string $type 'tag' 256 * @param Callable $func The function to print the link (gets tag and ns) 257 * @param bool $wrap wrap cloud in UL tags? 258 * @param bool $return returnn HTML instead of printing? 259 * @param string $ns Add this namespace to search links 260 * 261 * @return string 262 */ 263 public function html_cloud($tags, $type, $func, $wrap = true, $return = false, $ns = '') { 264 global $INFO; 265 266 $hidden_str = $this->getConf('hiddenprefix'); 267 $hidden_len = strlen($hidden_str); 268 269 $ret = ''; 270 if ($wrap) { 271 $ret .= '<ul class="tagging_cloud clearfix">'; 272 } 273 if (count($tags) === 0) { 274 // Produce valid XHTML (ul needs a child) 275 $this->setupLocale(); 276 $ret .= '<li><div class="li">' . $this->lang['js']['no' . $type . 's'] . '</div></li>'; 277 } else { 278 $tags = $this->cloudData($tags); 279 foreach ($tags as $val => $size) { 280 // skip hidden tags for users that can't edit 281 if ($type == 'tag' and 282 $hidden_len and 283 substr($val, 0, $hidden_len) == $hidden_str and 284 !($this->getUser() && $INFO['writable']) 285 ) { 286 continue; 287 } 288 289 $ret .= '<li class="t' . $size . '"><div class="li">'; 290 $ret .= call_user_func($func, $val, $ns); 291 $ret .= '</div></li>'; 292 } 293 } 294 if ($wrap) { 295 $ret .= '</ul>'; 296 } 297 if ($return) { 298 return $ret; 299 } 300 echo $ret; 301 return ''; 302 } 303 304 /** 305 * Get the link to a search for the given tag 306 * 307 * @param string $tag search for this tag 308 * @param string $ns limit search to this namespace 309 * 310 * @return string 311 */ 312 protected function linkToSearch($tag, $ns = '') { 313 return '<a href="' . hsc($this->getTagSearchURL($tag, $ns)) . '">' . $tag . '</a>'; 314 } 315 316 /** 317 * Display the Tags for the current page and prepare the tag editing form 318 * 319 * @param bool $print Should the HTML be printed or returned? 320 * 321 * @return string 322 */ 323 public function tpl_tags($print = true) { 324 global $INFO; 325 global $lang; 326 327 $filter = array('pid' => $INFO['id']); 328 if ($this->getConf('singleusermode')) { 329 $filter['tagger'] = 'auto'; 330 } 331 332 $tags = $this->findItems($filter, 'tag'); 333 334 $ret = ''; 335 336 $ret .= '<div class="plugin_tagging_edit">'; 337 $ret .= $this->html_cloud($tags, 'tag', array($this, 'linkToSearch'), true, true); 338 339 if ($this->getUser() && $INFO['writable']) { 340 $lang['btn_tagging_edit'] = $lang['btn_secedit']; 341 $ret .= html_btn('tagging_edit', $INFO['id'], '', array()); 342 343 $form = new dokuwiki\Form\Form(); 344 $form->id('tagging__edit'); 345 $form->setHiddenField('tagging[id]', $INFO['id']); 346 $form->setHiddenField('call', 'plugin_tagging_save'); 347 $tags = $this->findItems(array( 348 'pid' => $INFO['id'], 349 'tagger' => $this->getUser() 350 ), 'tag'); 351 $form->addTextInput('tagging[tags]')->val(implode(', ', array_keys($tags)))->addClass('edit'); 352 $form->addButton('do[save]', $lang['btn_save'])->id('tagging__edit_save'); 353 $form->addButton('do[cancel]', $lang['btn_cancel'])->id('tagging__edit_cancel'); 354 $ret .= $form->toHTML(); 355 } 356 $ret .= '</div>'; 357 358 if ($print) { 359 echo $ret; 360 } 361 return $ret; 362 } 363 364 /** 365 * @return array 366 */ 367 public function getAllTags() { 368 369 $db = $this->getDb(); 370 $res = $db->query('SELECT pid, tag, tagger FROM taggings ORDER BY tag'); 371 372 $tags_tmp = $db->res2arr($res); 373 $tags = array(); 374 foreach ($tags_tmp as $tag) { 375 $tid = $this->cleanTag($tag['tag']); 376 377 if (!isset($tags[$tid]['orig'])) { 378 $tags[$tid]['orig'] = array(); 379 } 380 $tags[$tid]['orig'][] = $tag['tag']; 381 382 if (isset($tags[$tid]['count'])) { 383 $tags[$tid]['count']++; 384 $tags[$tid]['tagger'][] = $tag['tagger']; 385 } else { 386 $tags[$tid]['count'] = 1; 387 $tags[$tid]['tagger'] = array($tag['tagger']); 388 } 389 } 390 return $tags; 391 } 392 393 /** 394 * Renames a tag 395 * 396 * @param string $formerTagName 397 * @param string $newTagName 398 */ 399 public function renameTag($formerTagName, $newTagName) { 400 401 if (empty($formerTagName) || empty($newTagName)) { 402 msg($this->getLang("admin enter tag names"), -1); 403 return; 404 } 405 406 $db = $this->getDb(); 407 408 $res = $db->query('SELECT pid FROM taggings WHERE CLEANTAG(tag) = ?', $this->cleanTag($formerTagName)); 409 $check = $db->res2arr($res); 410 411 if (empty($check)) { 412 msg($this->getLang("admin tag does not exists"), -1); 413 return; 414 } 415 416 $res = $db->query("UPDATE taggings SET tag = ? WHERE CLEANTAG(tag) = ?", $newTagName, $this->cleanTag($formerTagName)); 417 $db->res2arr($res); 418 419 msg($this->getLang("admin renamed"), 1); 420 return; 421 } 422 423} 424