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