xref: /plugin/tagging/helper.php (revision a66f671574f69969693d2da0bf986e22936283c9)
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