*/ // must be run within Dokuwiki if (!defined('DOKU_INC')) die(); if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); require_once(DOKU_INC.'inc/indexer.php'); class helper_plugin_tag extends DokuWiki_Plugin { var $namespace = ''; // namespace tag links point to var $doc = ''; // the final output XHTML string var $references = array(); // $meta['relation']['references'] data for metadata renderer var $sort = ''; // sort key var $idx_dir = ''; // directory for index files var $topic_idx = array(); /** * Constructor gets default preferences and language strings */ function helper_plugin_tag() { global $ID, $conf; $this->namespace = $this->getConf('namespace'); if (!$this->namespace) $this->namespace = getNS($ID); $this->sort = $this->getConf('sortkey'); // determine where index files are saved if (@file_exists($conf['indexdir'].'/page.idx')) { // new word length based index $this->idx_dir = $conf['indexdir']; if (!@file_exists($this->idx_dir.'/topic.idx')) $this->_importTagIndex(); } else { // old index $this->idx_dir = $conf['cachedir']; if (!@file_exists($this->idx_dir.'/topic.idx')) $this->_generateTagIndex(); } // load page and tag index $this->topic_idx = unserialize(io_readFile($this->idx_dir.'/topic.idx', false)); } function getInfo() { return array( 'author' => 'Gina Häußge, Michael Klier, Esther Brunner', 'email' => 'dokuwiki@chimeric.de', 'date' => @file_get_contents(DOKU_PLUGIN.'tag/VERSION'), 'name' => 'Tag Plugin (helper class)', 'desc' => 'Functions to return tag links and topic lists', 'url' => 'http://www.dokuwiki.org/plugin:tag', ); } function getMethods() { $result = array(); $result[] = array( 'name' => 'th', 'desc' => 'returns the header for the tags column for pagelist', 'return' => array('header' => 'string'), ); $result[] = array( 'name' => 'td', 'desc' => 'returns the tag links of a given page', 'params' => array('id' => 'string'), 'return' => array('links' => 'string'), ); $result[] = array( 'name' => 'tagLinks', 'desc' => 'generates tag links for given words', 'params' => array('tags' => 'array'), 'return' => array('links' => 'string'), ); $result[] = array( 'name' => 'getTopic', 'desc' => 'returns a list of pages tagged with the given keyword', 'params' => array( 'namespace (optional)' => 'string', 'number (not used)' => 'integer', 'tag (required)' => 'string'), 'return' => array('pages' => 'array'), ); $result[] = array( 'name' => 'tagRefine', 'desc' => 'refines an array of pages with tags', 'params' => array( 'pages to refine' => 'array', 'refinement tags' => 'string'), 'return' => array('pages' => 'array'), ); return $result; } /** * Returns the column header for th Pagelist Plugin */ function th() { return $this->getLang('tags'); } /** * Returns the cell data for the Pagelist Plugin */ function td($id) { $subject = p_get_metadata($id, 'subject'); return $this->tagLinks($subject); } /** * Returns the links for given tags */ function tagLinks($tags) { global $conf; if (!is_array($tags)) $tags = explode(' ', $tags); if (empty($tags) || ($tags[0] == '')) return ''; foreach ($tags as $tag) { $title = str_replace('_', ' ', noNS($tag)); resolve_pageid($this->namespace, $tag, $exists); // resolve shortcuts if ($exists) { $class = 'wikilink1'; $url = wl($tag); if ($conf['useheading']) { // important: set sendond param to false to prevent recursion! $heading = p_get_first_heading($tag, false); if ($heading) $title = $heading; } } else { $class = 'wikilink1'; $url = wl($tag, array('do'=>'showtag', 'tag'=>noNS($tag))); } $links[] = ''; $this->references[$tag] = $exists; } return implode(','.DOKU_LF.DOKU_TAB, $links); } /** * Returns a list of pages with a certain tag; very similar to ft_backlinks() * * @author Esther Brunner */ function getTopic($ns = '', $num = NULL, $tag = '') { if (!$tag) $tag = $_REQUEST['tag']; $tag = $this->_parseTagList($tag, true); $result = array(); $docs = $this->_tagIndexLookup($tag); $docs = array_filter($docs, 'isVisiblePage'); // discard hidden pages if (!count($docs)) return $result; // check metadata for matching subject foreach ($docs as $match) { // filter by namespace if ($ns && (strpos(':'.getNS($match), ':'.$ns) !== 0)) continue; // check ACL permission; if okay, then add the page $perm = auth_quickaclcheck($match); if ($perm < AUTH_READ) continue; // get metadata $meta = array(); $meta = p_get_metadata($match); // skip drafts unless for users with create priviledge $draft = ($meta['type'] == 'draft'); if ($draft && ($perm < AUTH_CREATE)) continue; $title = $meta['title']; $tags = $meta['subject']; $date = ($this->sort == 'mdate' ? $meta['date']['modified'] : $meta['date']['created'] ); if (!is_array($tags)) $tags = explode(' ', $tags); $taglinks = $this->tagLinks($tags); // determine the sort key if ($this->sort == 'id') $key = $match; elseif ($this->sort == 'pagename') $key = noNS($match); elseif ($this->sort == 'title') $key = $title; else $key = $date; // make sure that the key is unique $key = $this->_uniqueKey($key, $result); // is the page really tagged with one of our tags? foreach ($tags as $word) { if (in_array(utf8_strtolower($word), $tag)) { $result[$key] = array( 'id' => $match, 'title' => $title, 'date' => $date, 'user' => $meta['creator'], 'desc' => $meta['description']['abstract'], 'cat' => $tags[0], 'tags' => $taglinks, 'perm' => $perm, 'exists' => true, 'draft' => $draft, ); break; } } // if not, the tag index was out of date: refresh it! if (!is_array($result[$key])) $this->_refreshTagIndex($match, $tag); } // finally sort by sort key if ($this->getConf('sortorder') == 'ascending') ksort($result); else krsort($result); return $result; } /** * Refine found pages with tags (+tag: AND, -tag: (AND) NOT) */ function tagRefine($pages, $refine) { if (!is_array($pages)) return $pages; // wrong data type $tags = $this->_parseTagList($refine, true); foreach ($tags as $tag) { if (!(($tag{0} == '+') || ($tag{0} == '-'))) continue; $cleaned_tag = substr($tag, 1); //$tagpages = $this->topic_idx[$cleaned_tag]; $and = ($tag{0} == '+'); foreach ($pages as $key => $page) { // get metadata $pagetags = p_get_metadata($page['id'], 'subject'); if (!is_array($pagetags)) $pagetags = explode(' ', $pagetags); $cond = in_array($cleaned_tag, $pagetags); // index breaks constantly, so don't use it //$cond = in_array($page['id'], $tagpages); if ($and) $cond = (!$cond); if ($cond) unset($pages[$key]); } } return $pages; } /** * Refresh tag index * Deletes all tags of page id which are not defined in the page's metadata * as well. * * @param id the page id * @param tags tags as defined in the index to double-check */ function _refreshTagIndex($id, $tags) { if (!is_array($tags) || empty($tags)) return false; $changed = false; // clean array first $c = count($tags); for ($i = 0; $i <= $c; $i++) { $tags[$i] = utf8_strtolower($tags[$i]); } // get actual tags as saved in metadata $meta = p_get_metadata($id); $metatags = $meta['subject']; if (!is_array($metatags)) $metatags = array(); foreach ($tags as $tag) { if (!$tag) continue; // skip empty tags if (in_array($tag, $metatags)) continue; // tag is still there $this->topic_idx[$tag] = array_diff($this->topic_idx[$tag], array($id)); $changed = true; } if ($changed) return $this->_saveIndex(); else return true; } /** * Update tag index */ function _updateTagIndex($id, $tags) { global $ID, $INFO; if (!is_array($tags) || empty($tags)) return false; $changed = false; // clean array first $c = count($tags); for ($i = 0; $i <= $c; $i++) { $tags[$i] = utf8_strtolower($tags[$i]); } // clear no longer used tags if ($ID == $id) { $oldtags = $INFO['meta']['subject']; if (!is_array($oldtags)) $oldtags = explode(' ', $oldtags); foreach ($oldtags as $oldtag) { if (!$oldtag) continue; // skip empty tags $oldtag = utf8_strtolower($oldtag); if (in_array($oldtag, $tags)) continue; // tag is still there if (!is_array($this->topic_idx[$oldtag])) $this->topic_idx[$oldtag] = array(); $this->topic_idx[$oldtag] = array_diff($this->topic_idx[$oldtag], array($id)); $changed = true; } } // fill tag in foreach ($tags as $tag) { if (!$tag) continue; // skip empty tags if (!is_array($this->topic_idx[$tag])) $this->topic_idx[$tag] = array(); if (!in_array($id, $this->topic_idx[$tag])) { $this->topic_idx[$tag][] = $id; $changed = true; } } // save tag index if ($changed) return $this->_saveIndex(); else return true; } /** * Save tag or page index */ function _saveIndex() { return io_saveFile($this->idx_dir.'/topic.idx', serialize($this->topic_idx)); } /** * Import old creation date index */ function _importTagIndex() { global $conf; $old = $conf['indexdir'].'/tag.idx'; $new = $conf['indexdir'].'/topic.idx'; if (!@file_exists($old)) return $this->_generateTagIndex(); $tag_index = @file($this->idx_dir.'/tag.idx'); $topic_index = array(); if (is_array($tag_index)) { foreach ($tag_index as $idx_line) { list($key, $value) = explode(' ', $idx_line, 2); $topic_index[$key] = $this->_numToID(explode(':', trim($value))); } return io_saveFile($new, serialize($topic_index)); } return false; } /** * Generates the tag index */ function _generateTagIndex() { global $conf; require_once (DOKU_INC.'inc/search.php'); $pages = array(); search($pages, $conf['datadir'], 'search_allpages', array()); foreach ($pages as $page) { $this->_generateTagData($page); } return true; } /** * Generates the tag data for a single page. */ function _generateTagData($page) { $tags = p_get_metadata($page['id'], 'subject'); if (!is_array($tags)) $tags = explode(' ', $tags); $this->_updateTagIndex($page['id'], $tags); } /** * Tag index lookup */ function _tagIndexLookup($tags) { $result = array(); // array of line numbers in the page index // get the line numbers in page index foreach ($tags as $tag) { if (($tag{0} == '+') || ($tag{0} == '-')) $t = substr($tag, 1); else $t = $tag; if (!is_array($this->topic_idx[$t])) $this->topic_idx[$t] = array(); if ($tag{0} == '+') { // AND: add only if in both arrays $result = array_intersect($result, $this->topic_idx[$t]); } elseif ($tag{0} == '-') { // NOT: remove array from docs $result = array_diff($result, $this->topic_idx[$t]); } else { // OR: add array to docs $result = array_unique(array_merge($result, $this->topic_idx[$t])); } } // now convert to page IDs and return return $result; } /** * Converts an array of page numbers to IDs */ function _numToID($nums) { $page_index = idx_getIndex('page', ''); if (is_array($nums)) { $docs = array(); foreach ($nums as $num) { $docs[] = trim($page_index[$num]); } return $docs; } else { return trim($page_index[$nums]); } } /** * Splits a string into an array of tags */ function _parseTagList($tags, $clean = false) { // support for "quoted phrase tags" if (preg_match_all('#".*?"#', $tags, $matches)) { foreach ($matches[0] as $match) { $replace = str_replace(' ', '_', substr($match, 1, -1)); $tags = str_replace($match, $replace, $tags); } } if ($clean) $tags = utf8_strtolower($this->_applyMacro($tags)); return explode(' ', $tags); } /** * Makes user or date dependent topic lists possible */ function _applyMacro($id) { global $INFO, $auth; $user = $_SERVER['REMOTE_USER']; $userdata = $auth->getUserData($user); $group = $userdata['grps'][0]; $replace = array( '@USER@' => cleanID($user), '@NAME@' => cleanID($INFO['userinfo']['name']), '@GROUP@' => cleanID($group), '@YEAR@' => date('Y'), '@MONTH@' => date('m'), '@DAY@' => date('d'), ); return str_replace(array_keys($replace), array_values($replace), $id); } /** * Non-recursive function to check whether an array key is unique * * @author Esther Brunner * @author Ilya S. Lebedev */ function _uniqueKey($key, &$result) { // increase numeric keys by one if (is_numeric($key)) { while (array_key_exists($key, $result)) $key++; return $key; // append a number to literal keys } else { $num = 0; $testkey = $key; while (array_key_exists($testkey, $result)) { $testkey = $key.$num; $num++; } return $testkey; } } function _notEmpty($val) { return !empty($val); } } // vim:ts=4:sw=4:et:enc=utf-8: