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