1<?php
2/**
3 *
4 * @package    solr
5 * @author     Gabriel Birke <birke@d-scribe.de>
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 */
8
9if(!defined('DOKU_INC')) die();
10if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
11require_once(DOKU_INC.'inc/plugin.php');
12require_once dirname(__FILE__).'/ConnectionException.php';
13
14class helper_plugin_solr extends DokuWiki_Plugin {
15
16  protected $curl_ch;
17  protected $curl_initialized = false;
18
19  const INDEXER_VERSION = 1;
20
21  public function getMethods(){
22    return array(
23      array(
24        'name'   => 'tpl_searchform',
25        'desc'   => 'Prints HTML for search form',
26        'params' => array(),
27        'return' => array()
28      ),
29      array(
30        'name'   => 'html_render_titles',
31        'desc'   => 'Prints HTML list with search result of titles',
32        'params' => array(
33          'title_result' => 'array',
34          'ul_class' => 'string'
35         ),
36        'return' => array()
37      ),
38      array(
39        'name'   => 'lock_index',
40        'desc'   => 'Lock index',
41        'params' => array(),
42        'return' => array('lockdir' => 'string')
43      ),
44      array(
45        'name'   => 'needs_indexing',
46        'desc'   => 'Check if page needs indexing',
47        'params' => array('id' => 'string'),
48        'return' => array('needs_index' => 'boolean')
49      ),
50      array(
51        'name'   => 'update_idxfile',
52        'desc'   => 'Mark page as indexed',
53        'params' => array('id' => 'string'),
54        'return' => array()
55      ),
56      array(
57        'name'   => 'solr_query',
58        'desc'   => 'Send request to Solr server and return result string',
59        'params' => array(
60          'path' => 'string',
61          'query' => 'string',
62          'method' => 'string',
63          'postfields' => 'string'
64        ),
65        'return' => array('result' => 'string')
66      ),
67    );
68  }
69
70  public function tpl_searchform($ajax=false, $autocomplete=true) {
71    global $lang;
72    global $ACT;
73    global $QUERY;
74
75    print '<form action="'.wl().'" accept-charset="utf-8" class="search" id="dw__search" method="get"><div class="no">';
76    print '<input type="hidden" name="do" value="solr_search" />';
77    print '<input type="text" ';
78    if($ACT == 'solr_search' || ($ACT == 'solr_adv_search' && !empty($QUERY))) print 'value="'.htmlspecialchars($QUERY).'" ';
79    if(!$autocomplete) print 'autocomplete="off" ';
80    print 'id="solr_qsearch__in" accesskey="f" name="id" class="edit" title="[F]" />';
81    print '<input type="submit" value="'.$lang['btn_search'].'" class="button" title="'.$lang['btn_search'].'" />';
82    if($ajax) print '<div id="solr_qsearch__out" class="ajax_qsearch JSpopup"></div>';
83    print '</div></form>';
84    return true;
85  }
86
87  /**
88   * Render found pagenames as list
89   *
90   * @param array $title_result Solr result array
91   * @param string $ul_class Class for UL tag
92   */
93  public function html_render_titles($title_result, $ul_class="") {
94    print '<ul'.($ul_class?' class="'.$ul_class.'"':'').'>';
95    $count = 0;
96    foreach($title_result['response']['docs'] as $doc){
97      $id = $doc['id'];
98      if (isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ || !page_exists($id, '', false)) {
99        continue;
100      }
101      print '<li> ';
102      if (useHeading('navigation')) {
103          $name = $doc['title'];
104      }else{
105          $ns = getNS($id);
106          if($ns){
107              $name = shorten(noNS($id), ' ('.$ns.')',30);
108          }else{
109              $name = $id;
110          }
111      }
112      print html_wikilink(':'.$id,$name);
113      print '</li> ';
114    }
115    print '</ul> ';
116  }
117
118
119  /**
120   * Connect to SOLR server and return result
121   *
122   * @param string $path Solr action path (select, update, etc)
123   * @param string $query URL query string parameters
124   * @param string $method GET or POST
125   * @param string $postfields POST data, used for CURLOPT_POSTFIELDS
126   * @return string Solr response (XML or serialized PHP)
127   */
128  public function solr_query($path, $query, $method='GET', $postfields='') {
129    $url = $this->getConf('url')."/{$path}?{$query}";
130    $header = array("Content-type:text/xml; charset=utf-8");
131    if(!$this->curl_initialized) {
132      $this->curl_ch = curl_init();
133      $this->curl_initialized = true;
134    }
135    curl_setopt($this->curl_ch, CURLOPT_URL, $url);
136    curl_setopt($this->curl_ch, CURLOPT_HTTPHEADER, $header);
137    curl_setopt($this->curl_ch, CURLOPT_RETURNTRANSFER, 1);
138    if($method == 'POST') {
139      curl_setopt($this->curl_ch, CURLOPT_POST, 1);
140      curl_setopt($this->curl_ch, CURLOPT_POSTFIELDS, $postfields);
141    }
142    curl_setopt($this->curl_ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
143    curl_setopt($this->curl_ch, CURLINFO_HEADER_OUT, 1);
144
145    $event_data = array(
146      'path' => $path,
147      'query' => $query,
148      'method' => $method,
149      'postfields' => $postfields,
150      'curl' => $this->curl_ch,
151      'result' => null
152    );
153    $evt = new Doku_Event('SOLR_QUERY', $event_data);
154    if($evt->advise_before(true)) {
155      $evt->data['result'] = curl_exec($this->curl_ch);
156      if (curl_errno($this->curl_ch)) {
157        throw new ConnectionException(curl_error($this->curl_ch));
158      }
159    }
160    $evt->advise_after();
161    return $evt->data['result'];
162  }
163
164  /**
165   * Return name of the file if page needs to be indexed,
166   * otherwise false.
167   * @param string $id
168   * @return string|boolean
169   */
170  function needs_indexing($id) {
171    $idxtag = metaFN($id,'.solr_indexed');
172    if(@file_exists($idxtag)){
173        if(io_readFile($idxtag) >= self::INDEXER_VERSION ){
174            $last = @filemtime($idxtag);
175            if($last > @filemtime(wikiFN($id))){
176                return false;
177            }
178        }
179    }
180    return $idxtag;
181  }
182
183  /**
184   * Mark page as indexed
185   */
186  function update_idxfile($id) {
187    $idxtag = metaFN($id,'.solr_indexed');
188    return file_put_contents($idxtag, self::INDEXER_VERSION);
189  }
190
191  /**
192   * Lock Solr index with a lock directory
193   */
194  public function lock_index(){
195    global $conf;
196    $lock = $conf['lockdir'].'/_solr_indexer.lock';
197    while(!@mkdir($lock,$conf['dmode'])){
198        usleep(50);
199        if(time()-@filemtime($lock) > 60*5){
200            // looks like a stale lock - remove it
201            @rmdir($lock);
202            print "solr_indexer: stale lock removed".NL;
203        }else{
204            print "solr_indexer: indexer locked".NL;
205            return;
206        }
207    }
208    if($conf['dperm']) chmod($lock, $conf['dperm']);
209    return $lock;
210  }
211
212  function htmlAdvancedSearchBtn() {
213    global $ACT;
214    global $ID;
215    $id = $ACT == 'solr_search' ? $ID : '';
216    return html_btn('solr_adv_search', $id, '', array('do' => 'solr_adv_search'),
217      'get', $this->getLang('show_advsearch'), $this->getLang('btn_advsearch'));
218  }
219
220
221  /**
222   * Output advanced search form.
223   *
224   */
225  function htmlAdvancedSearchform()
226  {
227	  global $QUERY;
228	  $search_plus = empty($_REQUEST['search_plus']) ? $QUERY : $_REQUEST['search_plus'];
229	  ptln('<form action="'.DOKU_SCRIPT.'" accept-charset="utf-8" class="search" id="dw__solr_advsearch" name="dw__solr_advsearch"><div class="no">');
230		ptln('<input type="hidden" name="do" value="solr_adv_search" />');
231		ptln('<input type="hidden" name="id" value="'.$QUERY.'" />');
232		ptln('<table class="searchfields">');
233		ptln('	<tr>');
234		ptln('		<td class="advsearch-label1"><strong>'.$this->getLang('findresults').'</strong></td>');
235	  ptln('	</tr>');
236		ptln('	<tr>');
237		ptln('		<td class="label"><label for="search_plus">'.$this->getLang('allwords').'</label></td>');
238		ptln('		<td>	<input type="text" id="search_plus" name="search_plus" value="'.htmlspecialchars($search_plus).'" /> </td>');
239		ptln('	</tr>');
240		ptln('	<tr>');
241		ptln('		<td class="label"><label for="search_exact">'.$this->getLang('exactphrase').'</label></td>');
242		ptln('		<td>	<input type="text" id="search_exact" name="search_exact" value="'.htmlspecialchars($_REQUEST['search_exact']).'" /> </td>');
243		ptln('	</tr>');
244		ptln('	<tr>');
245		ptln('		<td class="label"><label for="search_minus">'.$this->getLang('withoutwords').'</label></td>');
246		ptln('		<td>	<input type="text" id="search_minus" name="search_minus" value="'.htmlspecialchars($_REQUEST['search_minus']).'" /> </td>');
247		ptln('	</tr>');
248		ptln('	<tr>');
249		ptln('		<td class="advsearch-label2">'.$this->getLang('in_namespace').'</td>');
250		ptln('		<td id="advsearch-nsselect">');
251    ptln($this->htmlNamespaceSelect(array(
252      'name' => 'search_ns[]',
253      'multiple' => true,
254      'selected' => empty($_REQUEST['search_ns'])?array():$_REQUEST['search_ns'],
255      'class' => 'search-ns',
256      'checkacl' => true
257    )));
258		ptln('		</td>');
259		ptln('	</tr>');
260		// TODO: Radio buttons for Wildcard yes/no
261		ptln('</table>');
262		// More search fields
263		ptln('<div id="disctinct_searchfields">');
264		$fields = array(
265		  'title' => array(
266		    'label' => $this->getLang('searchfield_title'),
267		    'field' => $this->htmlAdvSearchfield('title')
268		  ),
269		  'abstract' => array(
270		    'label' => $this->getLang('searchfield_abstract'),
271		    'field' => $this->htmlAdvSearchfield('abstract')
272		  ),
273		  'creator' => array(
274		    'label' => $this->getLang('searchfield_creator'),
275		    'field' => $this->htmlAdvSearchfield('creator')
276		  ),
277		  'contributor' => array(
278		    'label' => $this->getLang('searchfield_contributor'),
279		    'field' => $this->htmlAdvSearchfield('contributor')
280		  ),
281		);
282		trigger_event('SOLR_ADV_SEARCH_FIELDS', $fields);
283		ptln('  <table class="searchfields additional">');
284		foreach($fields as $field_id => $field) {
285		  ptln('    <tr><td class="label">');
286		  ptln('      <label for="search_field_'.$field_id.'">'.$field['label'].'</label></td><td>'.$field['field']);
287		  ptln('    <td></tr>');
288		}
289		ptln('  </table>');
290		ptln('</div>');
291		ptln('			<input type="submit" value="'.$this->getLang('btn_search').'" class="button" title="'.$this->getLang('btn_search').'" />');
292		ptln('	<br style="clear:both;" /></div>');
293		ptln('</form>');
294  }
295
296  function htmlNamespaceSelect($options)
297  {
298    global $conf;
299    $options = array_merge(array(
300      'selected' => array(),
301      'multiple' => false,
302      'name' => 'namespaces',
303      'class' => '',
304      'id' => '',
305      'size' => 8,
306      'depth_prefix' => 'nsDepth',
307      'depth_indent' => 5,
308      'depth_char' => '&nbsp;'
309      ), $options);
310
311    // Namespace selection
312		$s = sprintf('<select name="%s" size="%d" %s%s%s >',
313      $options['name'],
314      $options['size'],
315      ($options['multiple'] ? ' multiple="multiple"' : ''),
316      ($options['id'] ? " id=\"{$options['id']}\"" : ''),
317      ($options['class'] ? " class=\"{$options['class']}\"" : '')
318    );
319    $s .= '<option value=""'.(empty($options['selected']) || in_array('', $options['selected'])?' selected="selected"':'').'>'.$this->getLang('ns_all').'</option>';
320    $namespaces = array();
321		$opts=array();
322		require_once(DOKU_INC.'inc/search.php');
323		search($namespaces, $conf['datadir'],'search_namespaces', $opts);
324		sort($namespaces);
325		foreach($namespaces as $row) {
326
327			$depth = substr_count($row['id'], ':');
328			$s .= sprintf('  <option value="%s"%s%s>%s</option>',
329        $row['id'],
330        $options['depth_prefix'] ? ' class="'.$options['depth_prefix'].$depth.'"' : '',
331        in_array($row['id'], $options['selected']) ? ' selected="selected"' : '',
332				str_repeat($options['depth_char'], $depth * $options['depth_indent']).preg_replace('/[a-z0-9_]+:/', '', $row['id'])
333      );
334		}
335		$s .= '</select>';
336    return $s;
337  }
338
339  public function htmlAdvSearchfield($name){
340    $s = '<input type="text" name="search_fields['.$name.']" id="search_field_'.$name.'" ';
341    if(!empty($_REQUEST['search_fields'][$name])) {
342      $s .= ' value="'.htmlspecialchars($_REQUEST['search_fields'][$name]).'"';
343    }
344    $s .= '/>';
345    return $s;
346  }
347
348}
349