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 // wrap tag in quotes if non clean 143 $ctag = $this->cleanTag($tag); 144 if($ctag != utf8_strtolower($tag)) $tag = '"'.$tag.'"'; 145 146 $ret = '?do=search&id=' . rawurlencode($tag); 147 if($ns) $ret .= rawurlencode(' @' . $ns); 148 149 return $ret; 150 } 151 152 /** 153 * Calculates the size levels for the given list of clouds 154 * 155 * Automatically determines sensible tresholds 156 * 157 * @param array $tags list of tags => count 158 * @param int $levels 159 * @return mixed 160 */ 161 public function cloudData($tags, $levels = 10) { 162 $min = min($tags); 163 $max = max($tags); 164 165 // calculate tresholds 166 $tresholds = array(); 167 for($i = 0; $i <= $levels; $i++) { 168 $tresholds[$i] = pow($max - $min + 1, $i / $levels) + $min - 1; 169 } 170 171 // assign weights 172 foreach($tags as $tag => $cnt) { 173 foreach($tresholds as $tresh => $val) { 174 if($cnt <= $val) { 175 $tags[$tag] = $tresh; 176 break; 177 } 178 $tags[$tag] = $levels; 179 } 180 } 181 return $tags; 182 } 183 184 /** 185 * Display a tag cloud 186 * 187 * @param array $tags list of tags => count 188 * @param string $type 'tag' 189 * @param Callable $func The function to print the link (gets tag and ns) 190 * @param bool $wrap wrap cloud in UL tags? 191 * @param bool $return returnn HTML instead of printing? 192 * @param string $ns Add this namespace to search links 193 * @return string 194 */ 195 public function html_cloud($tags, $type, $func, $wrap = true, $return = false, $ns = '') { 196 $ret = ''; 197 if($wrap) $ret .= '<ul class="tagging_cloud clearfix">'; 198 if(count($tags) === 0) { 199 // Produce valid XHTML (ul needs a child) 200 $this->setupLocale(); 201 $ret .= '<li><div class="li">' . $this->lang['js']['no' . $type . 's'] . '</div></li>'; 202 } else { 203 $tags = $this->cloudData($tags); 204 foreach($tags as $val => $size) { 205 $ret .= '<li class="t' . $size . '"><div class="li">'; 206 $ret .= call_user_func($func, $val, $ns); 207 $ret .= '</div></li>'; 208 } 209 } 210 if($wrap) $ret .= '</ul>'; 211 if($return) return $ret; 212 echo $ret; 213 return ''; 214 } 215 216 /** 217 * Get the link to a search for the given tag 218 * 219 * @param string $tag search for this tag 220 * @param string $ns limit search to this namespace 221 * @return string 222 */ 223 protected function linkToSearch($tag, $ns = '') { 224 return '<a href="' . hsc($this->getTagSearchURL($tag, $ns)) . '">' . $tag . '</a>'; 225 } 226 227 228 public function tpl_tags() { 229 global $INFO; 230 global $lang; 231 $tags = $this->findItems(array('pid' => $INFO['id']), 'tag'); 232 echo '<div class="plugin_tagging_edit">'; 233 $this->html_cloud($tags, 'tag', array($this, 'linkToSearch')); 234 235 if(isset($_SERVER['REMOTE_USER']) && $INFO['writable']) { 236 $lang['btn_tagging_edit'] = $lang['btn_secedit']; 237 echo html_btn('tagging_edit', $INFO['id'], '', array()); 238 $form = new Doku_Form(array('id' => 'tagging__edit')); 239 $form->addHidden('tagging[id]', $INFO['id']); 240 $form->addHidden('call', 'plugin_tagging_save'); 241 $form->addElement(form_makeTextField('tagging[tags]', implode(', ', array_keys($this->findItems(array('pid' => $INFO['id'], 'tagger' => $_SERVER['REMOTE_USER']), 'tag'))))); 242 $form->addElement(form_makeButton('submit', 'save', $lang['btn_save'], array('id' => 'tagging__edit_save'))); 243 $form->addElement(form_makeButton('submit', 'cancel', $lang['btn_cancel'], array('id' => 'tagging__edit_cancel'))); 244 $form->printForm(); 245 } 246 echo '</div>'; 247 } 248 249 /** 250 * @return array 251 */ 252 public function getAllTags() { 253 254 $db = $this->getDb(); 255 $res = $db->query('SELECT pid, tag, tagger FROM taggings ORDER BY tag'); 256 257 $tags_tmp = $db->res2arr($res); 258 $tags = array(); 259 foreach($tags_tmp as $tag) { 260 $tid = $this->cleanTag($tag['tag']); 261 262 //$tags[$tid]['pid'][] = $tag['pid']; 263 264 if(isset($tags[$tid]['count'])) { 265 $tags[$tid]['count']++; 266 $tags[$tid]['tagger'][] = $tag['tagger']; 267 } else { 268 $tags[$tid]['count'] = 1; 269 $tags[$tid]['tagger'] = array($tag['tagger']); 270 } 271 } 272 return $tags; 273 } 274 275 /** 276 * Renames a tag 277 * 278 * @param string $formerTagName 279 * @param string $newTagName 280 */ 281 public function renameTag($formerTagName, $newTagName) { 282 283 if(empty($formerTagName) || empty($newTagName)) { 284 msg($this->getLang("admin enter tag names"), -1); 285 return; 286 } 287 288 $db = $this->getDb(); 289 290 $res = $db->query('SELECT pid FROM taggings WHERE tag= ?', $formerTagName); 291 $check = $db->res2arr($res); 292 293 if(empty($check)) { 294 msg($this->getLang("admin tag does not exists"), -1); 295 return; 296 } 297 298 $res = $db->query("UPDATE taggings SET tag = ? WHERE tag = ?", $newTagName, $formerTagName); 299 $db->res2arr($res); 300 301 msg($this->getLang("admin saved"), 1); 302 return; 303 } 304 305} 306