1<?php
2/**
3 * Action Plugin for SphinxSearch - Full DOM Generation Version
4 */
5
6if(!defined('DOKU_INC')) die();
7
8require_once(__DIR__ . '/sphinxapi.php');
9require_once(__DIR__ . '/PageMapper.php');
10require_once(__DIR__ . '/SphinxSearch.php');
11require_once(__DIR__ . '/functions.php');
12
13class action_plugin_sphinxsearchwas extends DokuWiki_Action_Plugin {
14
15    private $_sObj = null;
16    private $_dom = null;
17
18    public function register(\dokuwiki\Extension\EventHandler $controller) {
19        $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handle_act_unknown');
20    }
21
22    public function handle_act_unknown(\dokuwiki\Extension\Event $event, $param) {
23        global $ACT, $QUERY;
24        if ($ACT !== 'search') return;
25        $event->stopPropagation(); $event->preventDefault();
26        $this->_search($QUERY, (int)($_REQUEST['start'] ?? 0));
27    }
28
29    private function _search($query, $start) {
30        $search = new SphinxSearch($this->getConf('host'), $this->getConf('port'), $this->getConf('index'));
31        $maxResults = (int)$this->getConf('maxresults') ?: 10;
32        $search->setSnippetSize((int)$this->getConf('snippetsize') ?: 512);
33
34        $keywords = $this->_getKeywords($query);
35        $cats = $this->_getCategories($query);
36        if (empty($keywords)) { echo "No keywords."; return; }
37
38        if (empty($cats)) $search->setSearchAllQuery($keywords, '');
39        else $search->setSearchAllQueryWithCategoryFilter($keywords, $cats);
40
41        $res = $search->search($start, $maxResults);
42        $this->_sObj = $search;
43
44        if ($search->getError()) { echo "Sphinx Error: " . hsc($search->getError()); return; }
45
46        $pages = $search->getPages($keywords);
47        $total = $search->getTotalFound();
48        if (!$res || empty($pages) || $total == 0) { echo "No results."; return; }
49
50        $this->_dom = new DOMDocument('1.0', 'UTF-8');
51        $root = $this->_dom->createElement('div');
52        $this->_dom->appendChild($root);
53
54        $h1 = $this->_dom->createElement('h1', "Found $total matches for \"" . hsc($query) . "\"");
55        $root->appendChild($h1);
56
57        $container = $this->_dom->createElement('div');
58        $container->setAttribute('class', 'sphinx_search_container');
59        $root->appendChild($container);
60
61        $sidebar = $this->_dom->createElement('div');
62        $sidebar->setAttribute('class', 'search_sidebar');
63        ob_start();
64        if (function_exists('printNamespacesNew')) printNamespacesNew($this->_getMatchingPagenames($keywords, $cats));
65        $this->_appendRawHTML($sidebar, ob_get_clean());
66        $container->appendChild($sidebar);
67
68        $h2 = $this->_dom->createElement('h2', "Matching keywords");
69        $container->appendChild($h2);
70
71        $list = $this->_dom->createElement('div');
72        $list->setAttribute('class', 'search_result_list');
73        $container->appendChild($list);
74
75        foreach ($pages as $row) {
76            $list->appendChild($this->_createResultNode($row, $keywords));
77        }
78
79        $this->_addNumberedPagination($container, $query, $start, $total, $maxResults);
80        echo $this->_dom->saveHTML($root);
81    }
82
83    private function _createResultNode($row, $keywords) {
84        $div = $this->_dom->createElement('div');
85        $div->setAttribute('class', 'search_result_row');
86        $a = $this->_dom->createElement('a', hsc($row['titleTextExcerpt']));
87        $a->setAttribute('class', 'title');
88        $a->setAttribute('href', wl($row['page']));
89        $div->appendChild($a);
90
91        $snippet = $this->_dom->createElement('div');
92        $snippet->setAttribute('class', 'search_snippet');
93        $this->_appendRawHTML($snippet, $row['bodyExcerpt']);
94        $div->appendChild($snippet);
95
96        $nmsp = $this->_dom->createElement('span');
97        $nmsp->setAttribute('class', 'search_nmsp');
98        foreach (getNsLinks($row['page'], $keywords, $this->_sObj) as $i => $n) {
99            if ($i > 0) $nmsp->appendChild($this->_dom->createTextNode(' : '));
100            $na = $this->_dom->createElement('a', hsc($n['title']));
101            $na->setAttribute('href', wl($n['link']));
102            $nmsp->appendChild($na);
103        }
104        $div->appendChild($nmsp);
105        return $div;
106    }
107
108    private function _addNumberedPagination($parent, $query, $start, $total, $perPage) {
109        $nav = $this->_dom->createElement('div');
110        $nav->setAttribute('class', 'sphinxsearch_pagination');
111        $totalPages = (int)ceil($total / $perPage);
112        $currentPage = (int)floor($start / $perPage) + 1;
113        $range = 4;
114        $pageStart = max(1, $currentPage - $range);
115        $pageEnd   = min($totalPages, $currentPage + $range);
116
117        if ($currentPage > 1) $this->_addPageLink($nav, $query, ($currentPage - 2) * $perPage, '« Prev', 'page_box prev');
118        for ($i = $pageStart; $i <= $pageEnd; $i++) {
119            $class = ($i == $currentPage) ? 'page_box active' : 'page_box';
120            $this->_addPageLink($nav, $query, ($i - 1) * $perPage, $i, $class);
121        }
122        if ($currentPage < $totalPages) $this->_addPageLink($nav, $query, $currentPage * $perPage, 'Next »', 'page_box next');
123        $parent->appendChild($nav);
124    }
125
126    private function _addPageLink($parent, $query, $startValue, $label, $class) {
127        $a = $this->_dom->createElement('a', $label);
128        $a->setAttribute('href', wl('', ['do'=>'search', 'id'=>$query, 'start'=>$startValue]));
129        $a->setAttribute('class', $class);
130        $parent->appendChild($a);
131    }
132
133    private function _appendRawHTML($node, $html) {
134        if (empty($html)) return;
135        $tmp = new DOMDocument();
136        @$tmp->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $html);
137        $body = $tmp->getElementsByTagName('body')->item(0);
138        if ($body) {
139            foreach ($body->childNodes as $child) {
140                if ($child->nodeName === 'meta') continue;
141                $node->appendChild($this->_dom->importNode($child, true));
142            }
143        }
144    }
145
146    private function _getCategories($q) { preg_match('/@ns\s+([^\s]+)/i', urldecode($q), $m); return $m[1] ?? ''; }
147    private function _getKeywords($q) { return trim(preg_replace('/@ns\s+[^\s]+/i', '', urldecode($q))); }
148    private function _getMatchingPagenames($kw, $cat) {
149        $this->_sObj->setSearchOnlyPagename();
150        if (!$this->_sObj->search(0, 50)) return false;
151        $m = [];
152        foreach ($this->_sObj->getPagesIds() as $p) $m[$p['page']] = $p['hid'];
153        return $m;
154    }
155}
156