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