1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Gina Haeussge <gina@foosel.net>
5 */
6
7/**
8 * Delivers tag related functions
9 */
10class helper_plugin_blogtng_tags extends DokuWiki_Plugin {
11
12    /** @var helper_plugin_blogtng_sqlite */
13    private $sqlitehelper;
14
15    private $tags = array();
16
17    private $pid = null;
18
19    /**
20     * Constructor, loads the sqlite helper plugin
21     */
22    public function __construct() {
23        $this->sqlitehelper = plugin_load('helper', 'blogtng_sqlite');
24    }
25
26    /**
27     * Set pid of page
28     *
29     * @param string $pid
30     */
31    public function setPid($pid) {
32        $this->pid = trim($pid);
33    }
34
35    /**
36     * Set tags, filtered and unique
37     *
38     * @param array $tags
39     */
40    public function setTags($tags) {
41        $this->tags = array_unique(array_filter(array_map('trim', $tags)));
42    }
43
44    /**
45     * Return the tag array
46     *
47     * @return array tags
48     */
49    public function getTags() {
50        return $this->tags;
51    }
52
53    /**
54     * Load tags for specified pid
55     *
56     * @param $pid
57     * @return bool
58     */
59    public function load($pid) {
60        $this->setPid($pid);
61
62        if(!$this->sqlitehelper->ready()) {
63            msg('blogtng plugin: failed to load sqlite helper plugin!', -1);
64            $this->tags = [];
65            return false;
66        }
67        $query = 'SELECT tag
68                    FROM tags
69                   WHERE pid = ?
70                ORDER BY tag ASC';
71        $resid = $this->sqlitehelper->getDB()->query($query, $this->pid);
72        if ($resid === false) {
73            msg('blogtng plugin: failed to load tags!', -1);
74            $this->tags = [];
75            return false;
76        }
77        if ($this->sqlitehelper->getDB()->res2count($resid) == 0) {
78            $this->tags = [];
79            return true;
80        }
81
82        $tags_from_db = $this->sqlitehelper->getDB()->res2arr($resid);
83        $tags = [];
84        foreach ($tags_from_db as $tag_from_db) {
85            $tags[] = $tag_from_db['tag'];
86        }
87        $this->tags = $tags;
88        return true;
89    }
90
91    /**
92     * Count tags for specified pid
93     *
94     * @param $pid
95     * @return int
96     */
97    public function count($pid) {
98        if(!$this->sqlitehelper->ready()) {
99            msg('BlogTNG plugin: failed to load tags. (sqlite helper plugin not available)', -1);
100            return 0;
101        }
102
103        $pid = trim($pid);
104        $query = 'SELECT COUNT(tag) AS tagcount
105                    FROM tags
106                   WHERE pid = ?';
107
108        $resid = $this->sqlitehelper->getDB()->query($query, $pid);
109        if ($resid === false) {
110            msg('BlogTNG plugin: failed to load tags!', -1);
111            return 0;
112        }
113
114        $tagcount = $this->sqlitehelper->getDB()->res2row($resid, 0);
115        return (int)$tagcount['tagcount'];
116    }
117
118    /**
119     * Load tags for a specified blog
120     *
121     * @param $blogs
122     * @return array|bool
123     */
124    public function load_by_blog($blogs) {
125        if(!$this->sqlitehelper->ready()) return false;
126
127        $query = 'SELECT tags.tag AS tag, tags.pid AS pid
128                    FROM tags, entries
129                   WHERE tags.pid = entries.pid
130                     AND entries.blog IN ("' . implode('","', $blogs) . '")
131                     AND GETACCESSLEVEL(page) >= '.AUTH_READ;
132
133        $resid = $this->sqlitehelper->getDB()->query($query);
134        if($resid) {
135            return $this->sqlitehelper->getDB()->res2arr($resid);
136        }
137        return false;
138    }
139
140    /**
141     * Save tags
142     */
143    public function save() {
144        if (!$this->sqlitehelper->ready()) return;
145
146        $query = 'BEGIN TRANSACTION';
147        if (!$this->sqlitehelper->getDB()->query($query)) {
148            $this->sqlitehelper->getDB()->query('ROLLBACK TRANSACTION');
149            return;
150        }
151        $query = 'DELETE FROM tags WHERE pid = ?';
152        if (!$this->sqlitehelper->getDB()->query($query, $this->pid)) {
153            $this->sqlitehelper->getDB()->query('ROLLBACK TRANSACTION');
154            return;
155        }
156        foreach ($this->tags as $tag) {
157            $query = 'INSERT INTO tags (pid, tag) VALUES (?, ?)';
158            if (!$this->sqlitehelper->getDB()->query($query, $this->pid, $tag)) {
159                $this->sqlitehelper->getDB()->query('ROLLBACK TRANSACTION');
160                return;
161            }
162        }
163        $query = 'END TRANSACTION';
164        if (!$this->sqlitehelper->getDB()->query($query)) {
165            $this->sqlitehelper->getDB()->query('ROLLBACK TRANSACTION');
166        }
167    }
168
169    /**
170     * Parses query string to a where clause for use in a query
171     * in the query string:
172     *  - tags are space separated
173     *  - prefix + is AND
174     *  - prefix - is NOT
175     *  - no prefix is OR
176     *
177     * @param string $tagquery query string to be parsed
178     * @return null|string
179     */
180    public function parse_tag_query($tagquery) {
181        if (!$tagquery) {
182            return null;
183        }
184        if(!$this->sqlitehelper->ready()) return null;
185
186        $tags = array_map('trim', explode(' ', $tagquery));
187        $tag_clauses = array(
188            'OR' => array(),
189            'AND' => array(),
190            'NOT' => array(),
191        );
192        foreach ($tags as $tag) {
193            if ($tag[0] == '+') {
194                $tag_clauses['AND'][] = 'tag = ' . $this->sqlitehelper->getDB()->quote_string(substr($tag, 1));
195            } else if ($tag[0] == '-') {
196                $tag_clauses['NOT'][] = 'tag != ' . $this->sqlitehelper->getDB()->quote_string(substr($tag, 1));
197            } else {
198                $tag_clauses['OR'][] = 'tag = ' . $this->sqlitehelper->getDB()->quote_string($tag);
199            }
200        }
201        $tag_clauses = array_map('array_unique', $tag_clauses);
202
203        $where = '';
204        if ($tag_clauses['OR']) {
205            $where .= '('.join(' OR ', $tag_clauses['OR']).')';
206        }
207        if ($tag_clauses['AND']) {
208            $where .= (!empty($where) ? ' AND ' : '').join(' AND ', $tag_clauses['AND']);
209        }
210        if ($tag_clauses['NOT']) {
211            $where .= (!empty($where) ? ' AND ' : '').join(' AND ', $tag_clauses['NOT']);
212        }
213        return $where;
214    }
215
216    /**
217     * Print a list of tags
218     *
219     * @param string $target - tag links will point to this page, tag is passed as parameter
220     */
221    public function tpl_tags($target){
222        $prepared = array();
223        foreach ($this->tags as $tag) {
224            $prepared[] = '<li><div class="li">' . $this->_format_tag_link($tag, $target) . '</div></li>';
225        }
226        $html = '<ul class="blogtng_tags">'.DOKU_LF.join(DOKU_LF, $prepared).'</ul>'.DOKU_LF;
227        echo $html;
228    }
229
230    /**
231     * Print the joined tags as a string
232     *
233     * @param string $target - tag links will point to this page
234     * @param string $separator
235     */
236    public function tpl_tagstring($target, $separator) {
237        echo join($separator, array_map(array($this, '_format_tag_link'), $this->tags, array_fill(0, count($this->tags), $target)));
238    }
239
240    /**
241     * Displays a tag cloud
242     *
243     * @author Michael Klier <chi@chimeric.de>
244     *
245     * @param $conf
246     * @return string
247     */
248    public function xhtml_tagcloud($conf) {
249        $tags = $this->load_by_blog($conf['blog']);
250        if(!$tags) return '';
251        $cloud = array();
252        foreach($tags as $tag) {
253            if(!isset($cloud[$tag['tag']])) {
254                $cloud[$tag['tag']] = 1;
255            } else {
256                $cloud[$tag['tag']]++;
257            }
258            //$cloud[$tag['tag']][] = $tag['pid'];
259        }
260        asort($cloud);
261        $cloud = array_slice(array_reverse($cloud), 0, $conf['limit']);
262        $this->_cloud_weight($cloud, min($cloud), max($cloud), 5);
263        ksort($cloud);
264        $output = "";
265        foreach($cloud as $tag => $weight) {
266            $output .= '<a href="' . wl($conf['target'], ['post-tags'=>$tag])
267                    . '" class="tag cloud_weight' . $weight
268                    . '" title="' . $tag . '">' . $tag . "</a>\n";
269        }
270        return $output;
271    }
272
273    /**
274     * Happily stolen (and slightly modified) from
275     *
276     * http://www.splitbrain.org/blog/2007-01/03-tagging_splitbrain
277     *
278     * @param $tags
279     * @param $min
280     * @param $max
281     * @param $levels
282     */
283    private function _cloud_weight(&$tags,$min,$max,$levels){
284        // calculate tresholds
285        $tresholds = array();
286        $tresholds[0]= $min; // lowest treshold should always be min
287        for($i=1; $i<=$levels; $i++){
288            $tresholds[$i] = pow($max - $min + 1, $i/$levels) + $min;
289        }
290
291        // assign weights
292        foreach($tags as $tag => $cnt){
293            foreach($tresholds as $tresh => $val){
294                if($cnt <= $val){
295                    $tags[$tag] = $tresh;
296                    break;
297                }
298                $tags[$tag] = $levels;
299            }
300        }
301    }
302
303    /**
304     * Create html of url to target page for given tag
305     *
306     * @param string $tag
307     * @param string $target pageid
308     * @return string html of url
309     */
310    private function _format_tag_link($tag, $target) {
311        return '<a href="'.wl($target,array('post-tags'=>$tag)).'" class="tag">'.hsc($tag).'</a>';
312    }
313}
314