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' => ' ' 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