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